SSR control center: auto-purge webhook, dashboard tiles, backend module
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>
This commit is contained in:
1 parent
d52d49974f
commit
63f950f9db
14 files changed
+1003
No files matched your search
@@ -0,0 +1,176 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user