cObj stays null and the element cannot read its own FlexForm. */ final class ProductFinderJsonRenderer { private const CATEGORY_TABLE = 'sys_category'; /** Fallback when no root is configured: the category tree the products use. */ private const DEFAULT_ROOT_TITLE = 'Product'; protected ?ContentObjectRenderer $cObj = null; public function setContentObjectRenderer(ContentObjectRenderer $cObj): void { $this->cObj = $cObj; } #[AsAllowedCallable] public function render(string $content, array $conf): string { try { $settings = $this->settings(); $root = (int)($settings['categoryRoot'] ?? 0); if ($root <= 0) { $root = $this->rootByTitle(self::DEFAULT_ROOT_TITLE); } if ($root <= 0) { return (string)json_encode(['categories' => []]); } $order = (string)($settings['sorting'] ?? 'sorting'); $categories = []; foreach ($this->childrenOf($root, $order) as $main) { $categories[] = [ 'uid' => (int)$main['uid'], 'title' => (string)$main['title'], 'subcategories' => array_map( static fn(array $sub): array => [ 'uid' => (int)$sub['uid'], 'title' => (string)$sub['title'], ], $this->childrenOf((int)$main['uid'], $order) ), ]; } return (string)json_encode(['categories' => $categories], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); } catch (\Throwable) { // A broken filter must never take the page payload down with it. return (string)json_encode(['categories' => []]); } } /** * FlexForm settings of the element being rendered. * * @return array */ private function settings(): array { $flexform = (string)($this->cObj->data['pi_flexform'] ?? ''); if ($flexform === '') { return []; } $parsed = GeneralUtility::makeInstance(FlexFormService::class) ->convertFlexFormContentToArray($flexform); return is_array($parsed['settings'] ?? null) ? $parsed['settings'] : []; } /** * Direct children of a category, honouring the frontend restrictions * (hidden, start/end time, deleted). * * @return array> */ private function childrenOf(int $parent, string $order): array { $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) ->getQueryBuilderForTable(self::CATEGORY_TABLE); $queryBuilder->setRestrictions(GeneralUtility::makeInstance(FrontendRestrictionContainer::class)); $query = $queryBuilder->select('uid', 'title', 'sorting') ->from(self::CATEGORY_TABLE) ->where( $queryBuilder->expr()->eq('parent', $queryBuilder->createNamedParameter($parent, ParameterType::INTEGER)), $queryBuilder->expr()->in('sys_language_uid', [-1, 0]) ); match ($order) { 'title' => $query->orderBy('title', 'ASC'), 'uid' => $query->orderBy('uid', 'ASC'), default => $query->orderBy('sorting', 'ASC')->addOrderBy('title', 'ASC'), }; return $query->executeQuery()->fetchAllAssociative(); } /** Uid of a root-level category by title, 0 when it does not exist. */ private function rootByTitle(string $title): int { $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) ->getQueryBuilderForTable(self::CATEGORY_TABLE); $queryBuilder->setRestrictions(GeneralUtility::makeInstance(FrontendRestrictionContainer::class)); $uid = $queryBuilder->select('uid')->from(self::CATEGORY_TABLE) ->where( $queryBuilder->expr()->eq('parent', 0), $queryBuilder->expr()->eq('title', $queryBuilder->createNamedParameter($title)) ) ->setMaxResults(1) ->executeQuery()->fetchOne(); return is_numeric($uid) ? (int)$uid : 0; } }