cObj stays null and the element * path cannot see which record it is rendering. * * Cache (2026-11-02): the menu is part of every cached page, but its records * live on a storage folder - saving them cleared only that folder, and every * page cached before kept the old menu until a full flush. The page is * therefore tagged with the menu tables; the DataHandler flushes the tags * `` and `
_` of every record it writes, so any save in the * menu refreshes all pages carrying it. Data the panels read from the page * tree (pagetree, catalog, spotlight) is not covered and still needs a flush. */ final class MegamenuJsonRenderer { private const CTYPE = 'evomegamenujson_megamenu'; private const RECORD_TABLES = [ 'tx_evomegamenujson_entry', 'tx_evomegamenujson_column', 'tx_evomegamenujson_item', 'tx_evomegamenujson_teaser', ]; protected ?ContentObjectRenderer $cObj = null; public function setContentObjectRenderer(ContentObjectRenderer $cObj): void { $this->cObj = $cObj; } /** * @param array $conf TypoScript: contentUid, storagePid */ #[AsAllowedCallable] public function render(string $content, array $conf): string { try { $cObj = $this->cObj ?? GeneralUtility::makeInstance(ContentObjectRenderer::class); $row = null; $uid = (int)($conf['contentUid'] ?? 0); if ($uid <= 0) { $data = is_array($cObj->data ?? null) ? $cObj->data : []; if ((string)($data['CType'] ?? '') === self::CTYPE) { // rendering the element itself $row = $data; $uid = (int)($data['uid'] ?? 0); } } if ($uid <= 0 && (int)($conf['storagePid'] ?? 0) > 0) { $uid = $this->firstElementOn((int)$conf['storagePid']); } if ($uid <= 0) { return ''; } $menu = GeneralUtility::makeInstance(MenuBuilder::class)->build($uid, $cObj, $row); $this->tagPageCache($cObj, $uid); return (string)json_encode($menu, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); } catch (\Throwable) { // A broken menu must never take the page payload down with it. return ''; } } /** Tags the page cache with the menu element and tables (see class comment). */ private function tagPageCache(ContentObjectRenderer $cObj, int $contentUid): void { try { $collector = $cObj->getRequest()->getAttribute('frontend.cache.collector'); if (!$collector instanceof CacheDataCollectorInterface) { return; } $tags = array_map(static fn(string $table): CacheTag => new CacheTag($table), self::RECORD_TABLES); $collector->addCacheTags(new CacheTag('tt_content_' . $contentUid), ...$tags); } catch (\Throwable) { // without tags the menu still renders, it just ages until a flush } } /** uid of the first megamenu element on a storage folder, 0 when none. */ private function firstElementOn(int $pid): int { $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('tt_content'); $queryBuilder->setRestrictions(GeneralUtility::makeInstance(FrontendRestrictionContainer::class)); $uid = $queryBuilder->select('uid')->from('tt_content') ->where( $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pid, ParameterType::INTEGER)), $queryBuilder->expr()->eq('CType', $queryBuilder->createNamedParameter(self::CTYPE)) ) ->orderBy('sorting', 'ASC') ->setMaxResults(1) ->executeQuery()->fetchOne(); return is_numeric($uid) ? (int)$uid : 0; } }