diff --git a/Classes/Caching/PageCacheTimeout.php b/Classes/Caching/PageCacheTimeout.php index 6e09c33..269c2d5 100644 --- a/Classes/Caching/PageCacheTimeout.php +++ b/Classes/Caching/PageCacheTimeout.php @@ -25,6 +25,9 @@ namespace Wrm\Events\Caching; use DateTime; use DateTimeImmutable; +use TYPO3\CMS\Core\Cache\CacheManager; +use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface; +use TYPO3\CMS\Core\Context\Context; use TYPO3\CMS\Core\SingletonInterface; use Wrm\Events\Events\Controller\DateListVariables; @@ -37,26 +40,45 @@ use Wrm\Events\Events\Controller\DateListVariables; class PageCacheTimeout implements SingletonInterface { /** - * @var int + * @var null|DateTimeImmutable */ - private $earliestTimeout = 0; + private $endOfEvent = null; + + /** + * @var FrontendInterface + */ + private $runtimeCache; + + /** + * @var Context + */ + private $context; + + public function __construct( + CacheManager $cacheManager, + Context $context + ) { + $this->runtimeCache = $cacheManager->getCache('runtime'); + $this->context = $context; + } public function calculateCacheTimout( array $parameters ): int { - $timeout = $parameters['cacheTimeout']; + $typo3Timeout = $parameters['cacheTimeout']; + $ourTimeout = $this->getTimeout(); - if ($this->earliestTimeout <= 0) { - return $timeout; + if ($ourTimeout === null) { + return $typo3Timeout; } - return min($timeout, $this->earliestTimeout); + return min($typo3Timeout, $ourTimeout); } public function trackDates(DateListVariables $event): void { if ($event->getDemand()->shouldShowFromMidnight()) { - $this->updateTimeout((int) (new DateTimeImmutable('tomorrow midnight'))->format('U')); + $this->updateTimeout((new DateTimeImmutable('tomorrow midnight'))); return; } @@ -66,23 +88,36 @@ class PageCacheTimeout implements SingletonInterface continue; } - $this->updateTimeout((int)DateTimeImmutable::createFromMutable($endDate)->format('U')); + $this->updateTimeout(DateTimeImmutable::createFromMutable($endDate)); } } - private function updateTimeout(int $timestamp): void + private function updateTimeout(DateTimeImmutable $end): void { - $newTimeout = $timestamp - time(); - if ($newTimeout <= 0) { + $now = new DateTimeImmutable(); + + if ( + $end <= $now + || ( + $this->endOfEvent instanceof DateTimeImmutable + && $this->endOfEvent >= $end + ) + ) { return; } - if ($this->earliestTimeout === 0) { - $this->earliestTimeout = $newTimeout; - return; + $this->runtimeCache->remove('core-tslib_fe-get_cache_timeout'); + $this->endOfEvent = $end; + } + + private function getTimeout(): ?int + { + if (!$this->endOfEvent instanceof DateTimeImmutable) { + return null; } - $this->earliestTimeout = min($this->earliestTimeout, $newTimeout); + $executionTime = $this->context->getPropertyFromAspect('date', 'timestamp'); + return ((int) $this->endOfEvent->format('U')) - $executionTime; } public static function register(): void diff --git a/Tests/Functional/Frontend/CacheTest.php b/Tests/Functional/Frontend/CacheTest.php index 0f1805f..fd2aff4 100644 --- a/Tests/Functional/Frontend/CacheTest.php +++ b/Tests/Functional/Frontend/CacheTest.php @@ -24,20 +24,18 @@ declare(strict_types=1); namespace Wrm\Events\Tests\Functional\Frontend; use Codappix\Typo3PhpDatasets\PhpDataSet; -use Codappix\Typo3PhpDatasets\TestingFramework; use DateTimeImmutable; use DateTimeZone; use Psr\Http\Message\ResponseInterface; +use TYPO3\CMS\Core\TypoScript\TemplateService; use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\InternalRequest; -use Wrm\Events\Tests\Functional\AbstractFunctionalTestCase; +use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\Internal\TypoScriptInstruction; /** * @covers \Wrm\Events\Caching\PageCacheTimeout */ -class CacheTest extends AbstractFunctionalTestCase +class CacheTest extends AbstractTestCase { - use TestingFramework; - protected $testExtensionsToLoad = [ 'typo3conf/ext/events', 'typo3conf/ext/events/Tests/Functional/Frontend/Fixtures/Extensions/example', @@ -71,9 +69,7 @@ class CacheTest extends AbstractFunctionalTestCase */ public function setupReturnsSystemDefaults(): void { - $request = new InternalRequest(); - $request = $request->withPageId(1); - $response = $this->executeFrontendRequest($request); + $response = $this->executeFrontendRequest($this->getRequestWithSleep()); self::assertSame(200, $response->getStatusCode()); self::assertSame('max-age=86400', $response->getHeaderLine('Cache-Control')); @@ -101,9 +97,7 @@ class CacheTest extends AbstractFunctionalTestCase ], ]); - $request = new InternalRequest(); - $request = $request->withPageId(1); - $response = $this->executeFrontendRequest($request); + $response = $this->executeFrontendRequest($this->getRequestWithSleep()); self::assertSame(200, $response->getStatusCode()); self::assertSame('max-age=86400', $response->getHeaderLine('Cache-Control')); @@ -115,6 +109,8 @@ class CacheTest extends AbstractFunctionalTestCase */ public function setupReturnsEarlierIfEventsChangeBeforeSystemDefault(): void { + $end = (new DateTimeImmutable('tomorrow midnight', new DateTimeZone('UTC')))->modify('+2 hours'); + (new PhpDataSet())->import([ 'tx_events_domain_model_event' => [ [ @@ -128,18 +124,15 @@ class CacheTest extends AbstractFunctionalTestCase 'pid' => '2', 'event' => '1', 'start' => time(), - 'end' => time() + 50, + 'end' => $end->format('U'), ], ], ]); - $request = new InternalRequest(); - $request = $request->withPageId(1); - $response = $this->executeFrontendRequest($request); + $response = $this->executeFrontendRequest($this->getRequestWithSleep()); self::assertSame(200, $response->getStatusCode()); - self::assertMaxAge(50, $response); - self::assertSame('public', $response->getHeaderLine('Pragma')); + self::assertCacheHeaders($end, $response); } /** @@ -147,6 +140,8 @@ class CacheTest extends AbstractFunctionalTestCase */ public function setupReturnsMidnightIfConfigured(): void { + $midnight = (new DateTimeImmutable('tomorrow midnight', new DateTimeZone('UTC'))); + (new PhpDataSet())->import([ 'tx_events_domain_model_event' => [ [ @@ -171,27 +166,31 @@ class CacheTest extends AbstractFunctionalTestCase ], ])); - $request = new InternalRequest(); - $request = $request->withPageId(1); - $response = $this->executeFrontendRequest($request); + $response = $this->executeFrontendRequest($this->getRequestWithSleep()); self::assertSame(200, $response->getStatusCode()); - $midnight = (int) (new DateTimeImmutable('tomorrow midnight', new DateTimeZone('UTC')))->format('U'); - $age = $midnight - time(); - self::assertMaxAge($age, $response); + self::assertCacheHeaders($midnight, $response); self::assertSame('public', $response->getHeaderLine('Pragma')); } - private static function assertMaxAge(int $age, ResponseInterface $response): void + private static function assertCacheHeaders(DateTimeImmutable $end, ResponseInterface $response): void { - [$prefix, $value] = explode('=', $response->getHeaderLine('Cache-Control')); + self::assertSame('public', $response->getHeaderLine('Pragma')); + $expectedExpires = $end + ->setTimezone(new DateTimeZone('GMT')) + ->format(DateTimeImmutable::RFC7231) + ; + self::assertSame($expectedExpires, $response->getHeaderLine('Expires')); + + [$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 + // We might be seconds off due to our created offset within the rendering. $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.'); + $age = ((int) $end->format('U')) - time(); + self::assertLessThanOrEqual($age + 3, $value, 'Max age of cached response is higher than expected.'); + self::assertGreaterThanOrEqual($age - 3, $value, 'Max age of cached response is less than expected.'); } private function getTypoScriptFiles(): array @@ -207,4 +206,24 @@ class CacheTest extends AbstractFunctionalTestCase ], ]; } + + private function getRequestWithSleep(): InternalRequest + { + $request = new InternalRequest(); + $request = $request->withPageId(1); + $request = $request->withInstructions([ + $this->getTypoScriptInstruction() + ->withTypoScript([ + 'page.' => [ + '30.' => [ + 'userFunc.' => [ + 'sleep' => '2', + ], + ], + ], + ]) + ]); + + return $request; + } } diff --git a/Tests/Functional/Frontend/DatesTest.php b/Tests/Functional/Frontend/DatesTest.php index 63e0543..76ae42e 100644 --- a/Tests/Functional/Frontend/DatesTest.php +++ b/Tests/Functional/Frontend/DatesTest.php @@ -25,12 +25,11 @@ namespace Wrm\Events\Tests\Functional\Frontend; use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\InternalRequest; use Wrm\Events\Frontend\Dates; -use Wrm\Events\Tests\Functional\AbstractFunctionalTestCase; /** * @covers \Wrm\Events\Frontend\Dates */ -class DatesTest extends AbstractFunctionalTestCase +class DatesTest extends AbstractTestCase { protected $testExtensionsToLoad = [ 'typo3conf/ext/events', diff --git a/Tests/Functional/Frontend/FilterTest.php b/Tests/Functional/Frontend/FilterTest.php index 7772919..a0fbc4e 100644 --- a/Tests/Functional/Frontend/FilterTest.php +++ b/Tests/Functional/Frontend/FilterTest.php @@ -5,13 +5,12 @@ declare(strict_types=1); namespace Wrm\Events\Tests\Functional\Frontend; use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\InternalRequest; -use Wrm\Events\Tests\Functional\AbstractFunctionalTestCase; /** * @covers \Wrm\Events\Controller\DateController * @covers \Wrm\Events\Domain\Repository\DateRepository */ -class FilterTest extends AbstractFunctionalTestCase +class FilterTest extends AbstractTestCase { protected $testExtensionsToLoad = [ 'typo3conf/ext/events', diff --git a/Tests/Functional/Frontend/Fixtures/Extensions/example/Classes/UserFunc.php b/Tests/Functional/Frontend/Fixtures/Extensions/example/Classes/UserFunc.php new file mode 100644 index 0000000..7c6d984 --- /dev/null +++ b/Tests/Functional/Frontend/Fixtures/Extensions/example/Classes/UserFunc.php @@ -0,0 +1,52 @@ + + * + * 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\EventsExample; + +use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer; +use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController; + +class UserFunc +{ + /** + * @var ContentObjectRenderer + */ + public $cObj; + + public function accessTsfeTimeout(): string + { + return 'get_cache_timeout: ' . $this->getTsfe()->get_cache_timeout(); + } + + public function sleep(string $content, array $configuration): string + { + $sleep = (int) $this->cObj->stdWrapValue('sleep', $configuration['userFunc.'], 0); + sleep($sleep); + return 'Sleep for ' . $sleep . ' seconds'; + } + + public function getTsfe(): TypoScriptFrontendController + { + return $GLOBALS['TSFE']; + } +} diff --git a/Tests/Functional/Frontend/Fixtures/Extensions/example/composer.json b/Tests/Functional/Frontend/Fixtures/Extensions/example/composer.json index 47331b8..3946a5f 100644 --- a/Tests/Functional/Frontend/Fixtures/Extensions/example/composer.json +++ b/Tests/Functional/Frontend/Fixtures/Extensions/example/composer.json @@ -5,6 +5,11 @@ "require": { "wrm/events": "*" }, + "autoload": { + "psr-4": { + "Wrm\\EventsExample\\": "Classes/" + } + }, "extra": { "typo3/cms": { "extension-key": "example" diff --git a/Tests/Functional/Frontend/Fixtures/TypoScript/Rendering.typoscript b/Tests/Functional/Frontend/Fixtures/TypoScript/Rendering.typoscript index d3a17a6..a01ba2f 100644 --- a/Tests/Functional/Frontend/Fixtures/TypoScript/Rendering.typoscript +++ b/Tests/Functional/Frontend/Fixtures/TypoScript/Rendering.typoscript @@ -6,7 +6,24 @@ config { page = PAGE page { - 10 < styles.content.get + 10 = USER + 10 { + // Simulates foreign access prior our rendering. + // TYPO3 has an internal cache in order to not recalculate timeout. + userFunc = Wrm\EventsExample\UserFunc->accessTsfeTimeout + } + + 20 < styles.content.get + + 30 = USER + 30 { + // Simulates further long running rendering. + // In order to test that our ttl is calculated as expected. + userFunc = Wrm\EventsExample\UserFunc->sleep + userFunc { + sleep = 0 + } + } } plugin.tx_events {