Files
orascheandClaude Fable 5.2 8f8ae039d4 Real JSON null for empty product feature; SSR warm-cache button and CLI
Ticket 0068-FE: ProductFeatureJsonRenderer returned json_encode(null), which headless passes on as the STRING null (it only decodes object/array-looking strings) and crashed the front end. It now returns an empty string and the TypoScript field sets ifEmptyReturnNull = 1, giving a real JSON null. No other renderer used the pattern. Ticket 0066-FE: SsrPurgeService::warm() posts to the node /__warm endpoint (derived from SSR_PURGE_ENDPOINT, same secret), exposed as a Warm cache button with confirmation in the VITEC SSR module, a Cache warming progress row fed by /__health, and the CLI vitec:ssr-warm [paths]. Warm runs are deliberately not written to the purge log. Verified: two-path run 2/2 done, affected page delivers productfeature null.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 10:59:24 +01:00

65 lines
2.6 KiB
PHP

<?php
declare(strict_types=1);
namespace Evomedien\Vitec\UserFunc;
use Doctrine\DBAL\ParameterType;
use TYPO3\CMS\Core\Attribute\AsAllowedCallable;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Service\FlexFormService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Featured product card - sub-market template section 5 (spec 2026-10-01).
*
* One referenced product, emitted as a single full product card under the
* payload key `productfeature`. All copy comes from the product record
* (decision Olli 2026-10-01: dropdown only, no free text) - the front end
* renders image, title and teaser from the same card shape the product
* list delivers, so no second contract exists.
*
* Extends ProductListJsonRenderer purely to reuse its serializeProduct()
* and image handling - same filters, same shape, one source of truth.
*
* "No product" returns an EMPTY STRING, never json_encode(null): headless
* only decodes strings that look like objects/arrays, so the text "null"
* went out as the string "null" and crashed the front end (ticket
* 0068-FE). The TypoScript sets ifEmptyReturnNull = 1 on this field,
* which turns the empty string into a real JSON null.
*/
final class ProductFeatureJsonRenderer extends ProductListJsonRenderer
{
#[AsAllowedCallable]
public function render(string $content = '', array $conf = []): string
{
try {
$row = $this->cObj?->data ?? [];
$flexFormData = GeneralUtility::makeInstance(FlexFormService::class)
->convertFlexFormContentToArray((string)($row['pi_flexform'] ?? ''));
$productUid = (int)($flexFormData['settings']['product'] ?? 0);
if ($productUid <= 0) {
return '';
}
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('tx_vitec_domain_model_product');
$product = $queryBuilder->select('*')->from('tx_vitec_domain_model_product')
->where(
$queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($productUid, ParameterType::INTEGER)),
$queryBuilder->expr()->eq('deleted', 0),
$queryBuilder->expr()->eq('hidden', 0),
$queryBuilder->expr()->eq('hideonwebsite', 0)
)
->executeQuery()->fetchAssociative();
if (!$product) {
return '';
}
return (string)json_encode($this->serializeProduct($product), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
} catch (\Throwable) {
return '';
}
}
}