SSR auto-purge: purge only the pages that show a saved record, megamenu purges everything

SsrPurgeTargetResolver finds the paths of a saved VITEC or news record: its detail URL, the pages that select it (sys_refindex), product lists by category or all products, list plugins of its type and the records embedding it one level deep. URL, visibility and timing changes, the newest news, events, unknown tables and more than 150 paths still purge everything. Megamenu records and the megamenu element now purge everything (before: only its storage page). New read-only command vitec:ssr-purge-targets shows what a save would purge. Reference index rebuilt (117 updates).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
orascheandClaude Opus 5.5 committed 2026-11-12 12:16:44 +01:00
1 parent 06b9bc8293
commit e14f94ba63
4 files changed
+404 -9

No files matched your search

@@ -6,6 +6,7 @@ namespace Evomedien\Vitec\Hooks;
use TYPO3\CMS\Core\Core\Environment;
use Evomedien\Vitec\Service\SsrPurgeService;
use Evomedien\Vitec\Service\SsrPurgeTargetResolver;
use TYPO3\CMS\Core\DataHandling\DataHandler;
use TYPO3\CMS\Core\Site\SiteFinder;
use TYPO3\CMS\Core\Utility\GeneralUtility;
@@ -19,10 +20,14 @@ use TYPO3\CMS\Core\Database\ConnectionPool;
* - pages: navigation-relevant field changed (title, nav_title, slug,
* hidden, nav_hide, doktype) -> '*' (menus are embedded in EVERY page),
* otherwise just that page's path.
* - tt_content: the hosting page's path.
* - tx_news_* and tx_vitec_*: '*' (news feed the megamenu spotlight,
* domain models render on many pages).
* - delete/move/undelete commands: pages -> '*', records -> hosting page.
* - 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.
@@ -31,7 +36,9 @@ use TYPO3\CMS\Core\Database\ConnectionPool;
*/
final class DataHandlerSsrPurge
{
private const NAV_FIELDS = ['title', 'nav_title', 'slug', 'hidden', 'nav_hide', 'doktype', 'fe_group', 'starttime', 'endtime'];
private const MEGAMENU_PREFIX = 'tx_evomegamenujson_';
private const NAV_FIELDS =['title', 'nav_title', 'slug', 'hidden', 'nav_hide', 'doktype', 'fe_group', 'starttime', 'endtime'];
/** @var array<string,true> */
private static array $queue = [];
@@ -59,6 +66,10 @@ final class DataHandlerSsrPurge
}
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);
@@ -68,8 +79,16 @@ final class DataHandlerSsrPurge
}
return;
}
if (str_starts_with($table, 'tx_news_') || str_starts_with($table, 'tx_vitec_')) {
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;
}
}
}
@@ -94,17 +113,44 @@ final class DataHandlerSsrPurge
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_')) {
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');
@@ -115,7 +161,7 @@ final class DataHandlerSsrPurge
if (self::$queue === []) {
return;
}
$paths = array_keys(self::$queue);
$paths = isset(self::$queue['*']) ? ['*'] : array_map('strval', array_keys(self::$queue));
self::$queue = [];
GeneralUtility::makeInstance(SsrPurgeService::class)->purgeQuietly($paths, $trigger);
}