'*' (menus are embedded in EVERY page), * otherwise just that page's path. * - tt_content: the hosting page's path; the megamenu element -> '*'. * - tx_news_* and tx_vitec_*: targeted since 2026-11-12 - the pages that * show the record (SsrPurgeTargetResolver: detail URL, explicit * selections, list plugins, embedding records); '*' for URL/visibility * changes, the newest news, events and anything unknown. * - megamenu records (tx_evomegamenujson_*): '*' - the menu is on every page. * - delete/move/undelete commands: pages, records and megamenu -> '*', * content elements -> hosting page. * * Changes are queued per request and flushed ONCE in afterAllOperations, * so bulk operations (importer!) end up as a single purge call. * Runs only while the SSR routing flag is ON and the Site Set switch * vitec.ssr.autoPurge is enabled. */ final class DataHandlerSsrPurge { private const MEGAMENU_PREFIX = 'tx_evomegamenujson_'; private const NAV_FIELDS =['title', 'nav_title', 'slug', 'hidden', 'nav_hide', 'doktype', 'fe_group', 'starttime', 'endtime']; /** @var array */ private static array $queue = []; public function processDatamap_afterDatabaseOperations( string $status, string $table, $id, array $fieldArray, DataHandler $dataHandler ): void { if (!$this->isActive()) { return; } if ($table === 'pages') { if (array_intersect(array_keys($fieldArray), self::NAV_FIELDS) !== []) { self::$queue['*'] = true; return; } $pageId = $this->realId($id, $dataHandler); if ($pageId > 0) { $this->queuePagePath($pageId); } return; } if ($table === 'tt_content') { $recordId = $this->realId($id, $dataHandler); if ($this->isMegamenuElement($recordId)) { self::$queue['*'] = true; return; } $pid = $fieldArray['pid'] ?? null; if (!is_numeric($pid) || (int)$pid <= 0) { $pid = $this->pidOfRecord($table, $recordId); } if ((int)$pid > 0) { $this->queuePagePath((int)$pid); } return; } if (str_starts_with($table, self::MEGAMENU_PREFIX)) { self::$queue['*'] = true; return; } if (str_starts_with($table, 'tx_news_') || str_starts_with($table, 'tx_vitec_')) { $paths = GeneralUtility::makeInstance(SsrPurgeTargetResolver::class) ->resolve($table, $this->realId($id, $dataHandler), $status === 'new' ? ['pid'] : array_keys($fieldArray)); foreach ($paths as $path) { self::$queue[$path] = true; } } } public function processDatamap_afterAllOperations(DataHandler $dataHandler): void { $this->flush('editor-save'); } public function processCmdmap_postProcess( string $command, string $table, $id, $value, DataHandler $dataHandler ): void { if (!$this->isActive()) { return; } if ($table === 'pages') { // Delete/move/copy of a page changes menus everywhere. self::$queue['*'] = true; return; } if ($table === 'tt_content') { if ($this->isMegamenuElement((int)$id)) { self::$queue['*'] = true; return; } $pid = $this->pidOfRecord($table, (int)$id); if ($pid > 0) { $this->queuePagePath($pid); } return; } if (str_starts_with($table, 'tx_news_') || str_starts_with($table, 'tx_vitec_') || str_starts_with($table, self::MEGAMENU_PREFIX)) { // Delete, move, copy, undelete: lists change everywhere. self::$queue['*'] = true; } } /** * The megamenu element (CType evomegamenujson_megamenu) renders on * every page through the site setting megamenu.contentUid. */ private function isMegamenuElement(int $uid): bool { if ($uid <= 0) { return false; } try { $qb = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('tt_content'); $qb->getRestrictions()->removeAll(); $cType = $qb->select('CType')->from('tt_content') ->where($qb->expr()->eq('uid', $qb->createNamedParameter($uid, \Doctrine\DBAL\ParameterType::INTEGER))) ->executeQuery()->fetchOne(); return $cType === 'evomegamenujson_megamenu'; } catch (\Throwable) { return false; } } public function processCmdmap_afterFinish(DataHandler $dataHandler): void { $this->flush('editor-command'); } private function flush(string $trigger): void { if (self::$queue === []) { return; } $paths = isset(self::$queue['*']) ? ['*'] : array_map('strval', array_keys(self::$queue)); self::$queue = []; GeneralUtility::makeInstance(SsrPurgeService::class)->purgeQuietly($paths, $trigger); } private function isActive(): bool { if (!file_exists(Environment::getVarPath() . '/vitec-ssr-on.flag')) { return false; } try { foreach (GeneralUtility::makeInstance(SiteFinder::class)->getAllSites() as $site) { if ($site->getSettings()->get('vitec.ssr.autoPurge', true)) { return true; } } } catch (\Throwable) { return false; } return false; } private function queuePagePath(int $pageId): void { try { $site = GeneralUtility::makeInstance(SiteFinder::class)->getSiteByPageId($pageId); $uri = $site->getRouter()->generateUri($pageId); $path = $uri->getPath() ?: '/'; self::$queue[$path] = true; } catch (\Throwable) { // Page outside a site (folders etc.) - nothing cached for it. } } private function realId($id, DataHandler $dataHandler): int { if (is_numeric($id)) { return (int)$id; } return (int)($dataHandler->substNEWwithIDs[$id] ?? 0); } private function pidOfRecord(string $table, int $uid): int { if ($uid <= 0) { return 0; } try { $qb = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table); $qb->getRestrictions()->removeAll(); $pid = $qb->select('pid')->from($table) ->where($qb->expr()->eq('uid', $qb->createNamedParameter($uid, \Doctrine\DBAL\ParameterType::INTEGER))) ->executeQuery()->fetchOne(); return (int)$pid; } catch (\Throwable) { return 0; } } }