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>
65 lines
2.6 KiB
PHP
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 '';
|
|
}
|
|
}
|
|
}
|