Extend TYPO3 page cache timout calculation based on rendered dates (#9)

Relates: #10349
This commit is contained in:
Daniel Siepmann 2023-05-04 15:23:23 +02:00 committed by GitHub
parent afd8c59c9e
commit adc8b30e6d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 470 additions and 10 deletions

View file

@ -47,7 +47,7 @@ jobs:
tools: composer:v2
- name: Install xmllint
run: sudo apt-get install libxml2-utils
run: sudo apt update && sudo apt install libxml2-utils
- name: Install dependencies
run: composer install --prefer-dist --no-progress --no-suggest

View file

@ -0,0 +1,92 @@
<?php
declare(strict_types=1);
/*
* Copyright (C) 2023 Daniel Siepmann <coding@daniel-siepmann.de>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*/
namespace Wrm\Events\Caching;
use DateTime;
use DateTimeImmutable;
use TYPO3\CMS\Core\SingletonInterface;
use Wrm\Events\Events\Controller\DateListVariables;
/**
* Teaches TYPO3 to set proper timeout for page cache.
*
* Takes timings of rendered dates into account.
* Reduced timeout of page cache in case events (dates) might change before already calculated timeout.
*/
class PageCacheTimeout implements SingletonInterface
{
/**
* @var int
*/
private $earliestTimeout = 0;
public function calculateCacheTimout(
array $parameters
): int {
$timeout = $parameters['cacheTimeout'];
if ($this->earliestTimeout <= 0) {
return $timeout;
}
return min($timeout, $this->earliestTimeout);
}
public function trackDates(DateListVariables $event): void
{
if ($event->getDemand()->shouldShowFromMidnight()) {
$this->updateTimeout((int) (new DateTimeImmutable('tomorrow midnight'))->format('U'));
return;
}
foreach ($event->getDates() as $date) {
$endDate = $date->getEnd();
if (!$endDate instanceof DateTime) {
continue;
}
$this->updateTimeout((int)DateTimeImmutable::createFromMutable($endDate)->format('U'));
}
}
private function updateTimeout(int $timestamp): void
{
$newTimeout = $timestamp - time();
if ($newTimeout <= 0) {
return;
}
if ($this->earliestTimeout === 0) {
$this->earliestTimeout = $newTimeout;
return;
}
$this->earliestTimeout = min($this->earliestTimeout, $newTimeout);
}
public static function register(): void
{
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['get_cache_timeout']['events'] = self::class . '->calculateCacheTimout';
}
}

View file

@ -39,3 +39,9 @@ services:
Wrm\Events\Updates\MigrateOldLocations:
public: true
Wrm\Events\Caching\PageCacheTimeout:
tags:
- name: event.listener
event: Wrm\Events\Events\Controller\DateListVariables
method: 'trackDates'

View file

@ -0,0 +1,33 @@
3.4.0
=====
Breaking
--------
Nothing
Features
--------
* Adjust TYPO3 page cache timeout based on rendered dates.
The end time of each rendered date will be used.
The lowest end date will be used to calculate the maximum time to life for the page cache.
This is compared to the already calculated time to life.
The lower value is then used by TYPO3.
That allows visitors to always see the next dates.
Fixes
-----
Nothing
Tasks
-----
Nothing
Deprecation
-----------
Nothing

View file

@ -0,0 +1,207 @@
<?php
declare(strict_types=1);
/*
* Copyright (C) 2023 Daniel Siepmann <coding@daniel-siepmann.de>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*/
namespace Wrm\Events\Tests\Functional\Frontend;
use Codappix\Typo3PhpDatasets\PhpDataSet;
use DateTimeImmutable;
use DateTimeZone;
use Psr\Http\Message\ResponseInterface;
use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\InternalRequest;
use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
/**
* @covers \Wrm\Events\Caching\PageCacheTimeout
*/
class CacheTest extends FunctionalTestCase
{
protected $testExtensionsToLoad = [
'typo3conf/ext/events',
'typo3conf/ext/events/Tests/Functional/Frontend/Fixtures/Extensions/example',
];
protected $coreExtensionsToLoad = [
'fluid_styled_content',
];
protected $pathsToProvideInTestInstance = [
'typo3conf/ext/events/Tests/Functional/Frontend/Fixtures/Sites/' => 'typo3conf/sites',
];
protected function setUp(): void
{
parent::setUp();
$this->importCSVDataSet(__DIR__ . '/Fixtures/Database/SiteStructure.csv');
(new PhpDataSet())->import(['tt_content' => [[
'uid' => '1',
'pid' => '1',
'CType' => 'list',
'list_type' => 'events_datelisttest',
'header' => 'All Dates',
]]]);
$this->setUpFrontendRootPage(1, $this->getTypoScriptFiles());
}
/**
* @test
*/
public function setupReturnsSystemDefaults(): void
{
$request = new InternalRequest();
$request = $request->withPageId(1);
$response = $this->executeFrontendRequest($request);
self::assertSame(200, $response->getStatusCode());
self::assertSame('max-age=86400', $response->getHeaderLine('Cache-Control'));
self::assertSame('public', $response->getHeaderLine('Pragma'));
}
/**
* @test
*/
public function setupReturnsDefaultsIfEventsEndLater(): void
{
(new PhpDataSet())->import([
'tx_events_domain_model_event' => [
[
'uid' => '1',
'title' => 'Test Event 1',
],
],
'tx_events_domain_model_date' => [
[
'event' => '1',
'start' => time(),
'end' => time() + 86400 + 50,
],
],
]);
$request = new InternalRequest();
$request = $request->withPageId(1);
$response = $this->executeFrontendRequest($request);
self::assertSame(200, $response->getStatusCode());
self::assertSame('max-age=86400', $response->getHeaderLine('Cache-Control'));
self::assertSame('public', $response->getHeaderLine('Pragma'));
}
/**
* @test
*/
public function setupReturnsEarlierIfEventsChangeBeforeSystemDefault(): void
{
(new PhpDataSet())->import([
'tx_events_domain_model_event' => [
[
'uid' => '1',
'pid' => '2',
'title' => 'Test Event 1',
],
],
'tx_events_domain_model_date' => [
[
'pid' => '2',
'event' => '1',
'start' => time(),
'end' => time() + 50,
],
],
]);
$request = new InternalRequest();
$request = $request->withPageId(1);
$response = $this->executeFrontendRequest($request);
self::assertSame(200, $response->getStatusCode());
self::assertMaxAge(50, $response);
self::assertSame('public', $response->getHeaderLine('Pragma'));
}
/**
* @test
*/
public function setupReturnsMidnightIfConfigured(): void
{
(new PhpDataSet())->import([
'tx_events_domain_model_event' => [
[
'uid' => '1',
'pid' => '2',
'title' => 'Test Event 1',
],
],
'tx_events_domain_model_date' => [
[
'pid' => '2',
'event' => '1',
'start' => time(),
'end' => time() + 50,
],
],
]);
$this->setUpFrontendRootPage(1, array_merge_recursive($this->getTypoScriptFiles(), [
'setup' => [
'EXT:events/Tests/Functional/Frontend/Fixtures/TypoScript/CachingMidnight.typoscript'
],
]));
$request = new InternalRequest();
$request = $request->withPageId(1);
$response = $this->executeFrontendRequest($request);
self::assertSame(200, $response->getStatusCode());
$midnight = (int) (new DateTimeImmutable('tomorrow midnight', new DateTimeZone('UTC')))->format('U');
$age = $midnight - time();
self::assertMaxAge($age, $response);
self::assertSame('public', $response->getHeaderLine('Pragma'));
}
private static function assertMaxAge(int $age, ResponseInterface $response): void
{
[$prefix, $value] = explode('=', $response->getHeaderLine('Cache-Control'));
self::assertSame('max-age', $prefix);
// We might be one sec off due to how fast code is executed, so add a small offset
$value = (int)$value;
self::assertLessThanOrEqual($age + 1, $value, 'Max age of cached response is higher than expected.');
self::assertGreaterThanOrEqual($age - 1, $value, 'Max age of cached response is less than expected.');
}
private function getTypoScriptFiles(): array
{
return [
'constants' => [
'EXT:events/Configuration/TypoScript/constants.typoscript',
],
'setup' => [
'EXT:fluid_styled_content/Configuration/TypoScript/setup.typoscript',
'EXT:events/Configuration/TypoScript/setup.typoscript',
'EXT:events/Tests/Functional/Frontend/Fixtures/TypoScript/Rendering.typoscript'
],
];
}
}

View file

@ -0,0 +1,13 @@
{
"name": "wrm/events-example",
"type": "typo3-cms-extension",
"license": "GPL-2.0-or-later",
"require": {
"wrm/events": "*"
},
"extra": {
"typo3/cms": {
"extension-key": "example"
}
}
}

View file

@ -0,0 +1,17 @@
<?php
$EM_CONF['example'] = [
'title' => 'Events Test',
'category' => 'plugin',
'description' => 'Example for tests',
'state' => 'alpha',
'version' => '1.0.0',
'constraints' => [
'depends' => [
'events' => '',
'typo3' => '',
],
'conflicts' => [],
'suggests' => [],
],
];

View file

@ -0,0 +1,7 @@
<?php
\TYPO3\CMS\Extbase\Utility\ExtensionUtility::configurePlugin(
'Events',
'DateListTest',
[\Wrm\Events\Controller\DateController::class => 'list']
);

View file

@ -0,0 +1,6 @@
plugin.tx_events {
settings {
start >
useMidnight = 1
}
}

View file

@ -1,3 +1,9 @@
config {
cache_period = 86400
no_cache = 0
sendCacheHeaders = 1
}
page = PAGE
page {
10 < styles.content.get

View file

@ -46,6 +46,7 @@
}
},
"require-dev": {
"codappix/typo3-php-datasets": "^1.2",
"guzzlehttp/guzzle": "^6.3 || ^7.3",
"jangregor/phpstan-prophecy": "1.0.0",
"phpspec/prophecy-phpunit": "^1.0 || ^2.0",

View file

@ -9,7 +9,7 @@ $EM_CONF['events'] = [
'state' => 'alpha',
'createDirs' => '',
'clearCacheOnLoad' => 0,
'version' => '3.3.0',
'version' => '3.4.0',
'constraints' => [
'depends' => [
'typo3' => '10.4.00-11.5.99',

View file

@ -51,5 +51,6 @@ call_user_func(function () {
['source' => 'EXT:events/Resources/Public/Icons/Folder.svg']
);
\Wrm\Events\Caching\PageCacheTimeout::register();
\Wrm\Events\Updates\MigrateOldLocations::register();
});

View file

@ -75,3 +75,8 @@ parameters:
count: 1
path: Classes/Updates/MigrateOldLocations.php
-
message: "#^Cannot call method getEnd\\(\\) on mixed\\.$#"
count: 1
path: Classes/Caching/PageCacheTimeout.php

View file

@ -1,7 +1,18 @@
{ pkgs ? import <nixpkgs> { } }:
{
pkgs ? import <nixpkgs> { }
,phps ? import <phps>
}:
let
php = pkgs.php82;
php = phps.packages.x86_64-linux.php82.buildEnv {
extensions = { enabled, all }: enabled ++ (with all; [
xdebug
]);
extraConfig = ''
xdebug.mode = debug
memory_limit = 4G
'';
};
inherit(pkgs.php82Packages) composer;
projectInstall = pkgs.writeShellApplication {
@ -12,9 +23,10 @@ let
];
text = ''
rm -rf vendor/ composer.lock .Build/
composer install --prefer-dist --no-progress --working-dir="$PROJECT_ROOT"
composer update --prefer-dist --no-progress --working-dir="$PROJECT_ROOT"
'';
};
projectValidateComposer = pkgs.writeShellApplication {
name = "project-validate-composer";
runtimeInputs = [
@ -25,6 +37,7 @@ let
composer validate
'';
};
projectValidateXml = pkgs.writeShellApplication {
name = "project-validate-xml";
runtimeInputs = [
@ -40,25 +53,78 @@ let
xmllint --schema xliff-core-1.2-strict.xsd --noout $(find Resources -name '*.xlf')
'';
};
projectCodingGuideline = pkgs.writeShellApplication {
name = "project-coding-guideline";
projectPhpstan = pkgs.writeShellApplication {
name = "project-phpstan";
runtimeInputs = [
php
projectInstall
];
text = ''
./vendor/bin/phpstan
'';
};
projectCgl = pkgs.writeShellApplication {
name = "project-cgl";
runtimeInputs = [
php
];
text = ''
project-install
./vendor/bin/ecs check --no-progress-bar --clear-cache
'';
};
projectCglFix = pkgs.writeShellApplication {
name = "project-cgl-fix";
runtimeInputs = [
php
];
text = ''
./vendor/bin/ecs check --fix --no-progress-bar --clear-cache
'';
};
projectTestsUnit = pkgs.writeShellApplication {
name = "project-tests-unit";
runtimeInputs = [
php
];
text = ''
./vendor/bin/phpunit --testsuite unit --color --testdox
'';
};
projectTestsFunctional = pkgs.writeShellApplication {
name = "project-tests-functional";
runtimeInputs = [
php
];
text = ''
./vendor/bin/phpunit --testsuite functional --color --testdox
'';
};
in pkgs.mkShell {
name = "TYPO3 Extension Watchlist";
buildInputs = [
projectInstall
projectValidateComposer
projectValidateXml
projectCodingGuideline
projectPhpstan
projectCgl
projectCglFix
projectTestsUnit
projectTestsFunctional
php
composer
];