Package 1: SsrPurgeService posts to the node /__purge (endpoint+secret in var/vitec-ssr-purge.env, outside git) and logs every attempt to tx_vitec_ssrpurgelog. DataHandler hooks queue per request and flush once: pages with nav-relevant field changes and all tx_news_/tx_vitec_ records purge everything, other page/content edits purge the hosting page path. Gated by the routing flag and the new Site Set switch vitec.ssr.autoPurge. Flipping the SSR switch ON purges everything once. CLI: vitec:ssr-purge, vitec:ssr-health (samples /__health into tx_vitec_ssrhealthlog, 14 days kept, scheduler-ready). Package 2: dashboard widgets vitecSsrStatus (live health, release, switch, last purge) and vitecSsrStats (purge counts, failures, uptime, response time). Package 3: backend module web_vitecssr with manual purge (paths or everything) and both protocols. Tested end to end against the running node (purge single/all 200, health sample logged). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
177 lines
5.6 KiB
PHP
177 lines
5.6 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Evomedien\Vitec\Hooks;
|
|
|
|
use TYPO3\CMS\Core\Core\Environment;
|
|
use Evomedien\Vitec\Service\SsrPurgeService;
|
|
use TYPO3\CMS\Core\DataHandling\DataHandler;
|
|
use TYPO3\CMS\Core\Site\SiteFinder;
|
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
|
use TYPO3\CMS\Core\Database\ConnectionPool;
|
|
|
|
/**
|
|
* Auto-purge: tells the SSR node which pages to drop from its full-page
|
|
* cache after editors change content (ticket 0054-FE section 3).
|
|
*
|
|
* Mapping rules (v1, deliberately conservative):
|
|
* - 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.
|
|
*
|
|
* 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 NAV_FIELDS = ['title', 'nav_title', 'slug', 'hidden', 'nav_hide', 'doktype', 'fe_group', 'starttime', 'endtime'];
|
|
|
|
/** @var array<string,true> */
|
|
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);
|
|
$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, 'tx_news_') || str_starts_with($table, 'tx_vitec_')) {
|
|
self::$queue['*'] = 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') {
|
|
$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_')) {
|
|
self::$queue['*'] = true;
|
|
}
|
|
}
|
|
|
|
public function processCmdmap_afterFinish(DataHandler $dataHandler): void
|
|
{
|
|
$this->flush('editor-command');
|
|
}
|
|
|
|
private function flush(string $trigger): void
|
|
{
|
|
if (self::$queue === []) {
|
|
return;
|
|
}
|
|
$paths = 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;
|
|
}
|
|
}
|
|
}
|