From a2937709fb40f3799d48d6a9b048a158ac6e9496 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20H=C3=A4u=C3=9Fler?= Date: Thu, 14 Nov 2024 08:00:24 +0100 Subject: [PATCH] [FEATURE] Add support for TYPO3 13.4 LTS --- .gitattributes | 35 +++++------ .github/workflows/cgl.yaml | 3 +- .github/workflows/tests.yaml | 4 +- .../Expiration/CacheExpirationCalculator.php | 37 +++++++++++- .../Expiration/CacheLifetimeCalculator.php | 59 +++++++++++++++++++ .../PageCacheBagRegisteredEventListener.php | 35 ++++++++++- .../PageCacheLifetimeEventListener.php | 16 +++-- Classes/Helper/FrontendHelper.php | 12 ++++ README.md | 2 +- composer.json | 9 +-- composer.lock | 2 +- dependency-checker.json | 9 +++ ext_emconf.php | 2 +- packaging_exclude.php | 1 + phpstan-baseline.neon | 45 ++++++++++++++ 15 files changed, 237 insertions(+), 34 deletions(-) create mode 100644 Classes/Cache/Expiration/CacheLifetimeCalculator.php create mode 100644 dependency-checker.json diff --git a/.gitattributes b/.gitattributes index b61ffe6..b102333 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,17 +1,18 @@ -* text=auto -/.github export-ignore -/Tests export-ignore -/.editorconfig export-ignore -/.gitattributes export-ignore -/.gitignore export-ignore -/.php-cs-fixer.php export-ignore -/CODE_OF_CONDUCT.md export-ignore -/composer.lock export-ignore -/CONTRIBUTING.md export-ignore -/packaging_exclude.php export-ignore -/phpstan.neon export-ignore -/phpstan-baseline.neon export-ignore -/phpunit.functional.xml export-ignore -/phpunit.unit.xml export-ignore -/rector.php export-ignore -/renovate.json export-ignore +* text=auto +/.github export-ignore +/Tests export-ignore +/.editorconfig export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore +/.php-cs-fixer.php export-ignore +/CODE_OF_CONDUCT.md export-ignore +/composer.lock export-ignore +/CONTRIBUTING.md export-ignore +/dependency-checker.json export-ignore +/packaging_exclude.php export-ignore +/phpstan.neon export-ignore +/phpstan-baseline.neon export-ignore +/phpunit.functional.xml export-ignore +/phpunit.unit.xml export-ignore +/rector.php export-ignore +/renovate.json export-ignore diff --git a/.github/workflows/cgl.yaml b/.github/workflows/cgl.yaml index b8a1626..095bbaf 100644 --- a/.github/workflows/cgl.yaml +++ b/.github/workflows/cgl.yaml @@ -36,7 +36,8 @@ jobs: # Check Composer dependencies - name: Check dependencies - run: composer-require-checker check + # @todo Remove config file once support for TYPO3 v11 and v12 is dropped + run: composer-require-checker check --config-file dependency-checker.json - name: Re-install Composer dependencies uses: ramsey/composer-install@v3 - name: Check for unused dependencies diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index ad438d8..68a3c9d 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -15,11 +15,11 @@ jobs: fail-fast: false matrix: php-version: ["8.1", "8.2", "8.3", "8.4"] - typo3-version: ["11.5", "12.4", "13.1"] + typo3-version: ["11.5", "12.4", "13.4"] dependencies: ["highest", "lowest"] exclude: - php-version: "8.1" - typo3-version: "13.1" + typo3-version: "13.4" - php-version: "8.4" typo3-version: "11.5" env: diff --git a/Classes/Cache/Expiration/CacheExpirationCalculator.php b/Classes/Cache/Expiration/CacheExpirationCalculator.php index 177d898..ac583d0 100644 --- a/Classes/Cache/Expiration/CacheExpirationCalculator.php +++ b/Classes/Cache/Expiration/CacheExpirationCalculator.php @@ -30,6 +30,10 @@ use TYPO3\CMS\Core\Database\Query\Restriction\EndTimeRestriction; use TYPO3\CMS\Core\Database\Query\Restriction\StartTimeRestriction; use TYPO3\CMS\Core\Database\RelationHandler; +use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability; +use TYPO3\CMS\Core\Schema\TcaSchema; +use TYPO3\CMS\Core\Schema\TcaSchemaFactory; +use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Extbase\DomainObject\AbstractEntity; use TYPO3\CMS\Extbase\Persistence\QueryInterface; use TYPO3\CMS\Extbase\Persistence\QueryResultInterface; @@ -164,10 +168,15 @@ public function forRelationHandler(RelationHandler $relationHandler): ?\DateTime /** * @param non-empty-string $tableName - * @return array, non-empty-string|null> + * @return array, string|null> */ protected function getConfiguredEnableFields(string $tableName): array { + if (\class_exists(TcaSchema::class)) { + return $this->getConfiguredEnableFieldsFromTcaSchema($tableName); + } + + // @todo Remove once support for TYPO3 v11 and v12 is dropped $configuration = $GLOBALS['TCA'][$tableName]['ctrl']['enablecolumns'] ?? []; $enableFields = [ EnableField::StartTime->value, @@ -177,6 +186,32 @@ protected function getConfiguredEnableFields(string $tableName): array return array_intersect_key($configuration, array_flip($enableFields)); } + /** + * @param non-empty-string $tableName + * @return array, string|null> + */ + protected function getConfiguredEnableFieldsFromTcaSchema(string $tableName): array + { + // @todo Use DI once support for TYPO3 v11 and v12 is dropped + $tcaSchemaFactory = GeneralUtility::makeInstance(TcaSchemaFactory::class); + + // Early return if schema does not exist + if (!$tcaSchemaFactory->has($tableName)) { + return []; + } + + $tcaSchema = $tcaSchemaFactory->get($tableName); + $capabilities = [ + EnableField::StartTime->value => TcaSchemaCapability::RestrictionStartTime, + EnableField::EndTime->value => TcaSchemaCapability::RestrictionEndTime, + ]; + + return \array_map( + static fn(TcaSchemaCapability $capability) => $tcaSchema->hasCapability($capability) ? $tcaSchema->getCapability($capability)->getFieldName() : null, + $capabilities, + ); + } + protected function calculateExpirationDate( \DateTimeInterface|int|null $startTime, \DateTimeInterface|int|null $endTime, diff --git a/Classes/Cache/Expiration/CacheLifetimeCalculator.php b/Classes/Cache/Expiration/CacheLifetimeCalculator.php new file mode 100644 index 0000000..a498299 --- /dev/null +++ b/Classes/Cache/Expiration/CacheLifetimeCalculator.php @@ -0,0 +1,59 @@ + + * + * 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, see . + */ + +namespace CPSIT\Typo3CacheBags\Cache\Expiration; + +use CPSIT\Typo3CacheBags\Cache\Bag\CacheBag; +use TYPO3\CMS\Core\Context\Context; + +/** + * CacheLifetimeCalculator + * + * @author Elias Häußler + * @license GPL-2.0-or-later + */ +class CacheLifetimeCalculator +{ + public function __construct( + protected readonly Context $context, + ) {} + + public function forCacheBag(CacheBag $cacheBag): ?int + { + $expirationDate = $cacheBag->getExpirationDate(); + + if ($expirationDate !== null) { + return $this->forExpirationDate($expirationDate); + } + + return null; + } + + public function forExpirationDate(\DateTimeInterface $expirationDate): int + { + /** @var non-negative-int $now */ + $now = $this->context->getPropertyFromAspect('date', 'accessTime', 0); + + return \max(0, $expirationDate->getTimestamp() - $now); + } +} diff --git a/Classes/EventListener/PageCacheBagRegisteredEventListener.php b/Classes/EventListener/PageCacheBagRegisteredEventListener.php index 4202610..60ac4e8 100644 --- a/Classes/EventListener/PageCacheBagRegisteredEventListener.php +++ b/Classes/EventListener/PageCacheBagRegisteredEventListener.php @@ -23,9 +23,13 @@ namespace CPSIT\Typo3CacheBags\EventListener; +use CPSIT\Typo3CacheBags\Cache\Bag\CacheBag; +use CPSIT\Typo3CacheBags\Cache\Expiration\CacheLifetimeCalculator; use CPSIT\Typo3CacheBags\Enum\CacheScope; use CPSIT\Typo3CacheBags\Event\CacheBagRegisteredEvent; use CPSIT\Typo3CacheBags\Helper\FrontendHelper; +use TYPO3\CMS\Core\Cache\CacheDataCollector; +use TYPO3\CMS\Core\Cache\CacheTag; /** * PageCacheBagRegisteredEventListener @@ -35,10 +39,39 @@ */ final class PageCacheBagRegisteredEventListener { + public function __construct( + private readonly CacheLifetimeCalculator $cacheLifetimeCalculator, + ) {} + public function __invoke(CacheBagRegisteredEvent $event): void { if ($event->cacheBag->getScope() === CacheScope::Pages) { - FrontendHelper::getTypoScriptFrontendController()->addCacheTags($event->cacheBag->getCacheTags()); + $this->addCacheTags($event->cacheBag); + } + } + + private function addCacheTags(CacheBag $cacheBag): void + { + if (\class_exists(CacheDataCollector::class)) { + /** @var CacheDataCollector $cacheDataCollector */ + $cacheDataCollector = FrontendHelper::getServerRequest()->getAttribute('frontend.cache.collector'); + $cacheDataCollector->addCacheTags(...$this->convertCacheTagsToObjects($cacheBag)); + } else { + // @todo Remove once support for TYPO3 v11 and v12 is dropped + FrontendHelper::getTypoScriptFrontendController()->addCacheTags($cacheBag->getCacheTags()); } } + + /** + * @return list + */ + private function convertCacheTagsToObjects(CacheBag $cacheBag): array + { + $lifetime = $this->cacheLifetimeCalculator->forCacheBag($cacheBag) ?? PHP_INT_MAX; + + return \array_map( + static fn(string $cacheTag) => new CacheTag($cacheTag, $lifetime), + $cacheBag->getCacheTags(), + ); + } } diff --git a/Classes/EventListener/PageCacheLifetimeEventListener.php b/Classes/EventListener/PageCacheLifetimeEventListener.php index 21dfc0f..9a6da5a 100644 --- a/Classes/EventListener/PageCacheLifetimeEventListener.php +++ b/Classes/EventListener/PageCacheLifetimeEventListener.php @@ -24,8 +24,9 @@ namespace CPSIT\Typo3CacheBags\EventListener; use CPSIT\Typo3CacheBags\Cache\Bag\CacheBagRegistry; +use CPSIT\Typo3CacheBags\Cache\Expiration\CacheLifetimeCalculator; use CPSIT\Typo3CacheBags\Enum\CacheScope; -use TYPO3\CMS\Core\Context\Context; +use TYPO3\CMS\Core\Cache\CacheDataCollector; use TYPO3\CMS\Frontend\Event\ModifyCacheLifetimeForPageEvent; /** @@ -33,23 +34,28 @@ * * @author Elias Häußler * @license GPL-2.0-or-later + * + * @todo Remove once support for TYPO3 v11 and v12 is dropped */ final class PageCacheLifetimeEventListener { public function __construct( - private readonly Context $context, private readonly CacheBagRegistry $cacheBagRegistry, + private readonly CacheLifetimeCalculator $cacheLifetimeCalculator, ) {} public function __invoke(ModifyCacheLifetimeForPageEvent $event): void { + // Lifetime modification for TYPO3 >= 13.4 is already done when cache bags are registered + if (class_exists(CacheDataCollector::class)) { + return; + } + $expirationDate = $this->cacheBagRegistry->getExpirationDate(CacheScope::Pages); - /** @var non-negative-int $now */ - $now = $this->context->getPropertyFromAspect('date', 'accessTime', 0); if ($expirationDate !== null) { $event->setCacheLifetime( - \max(0, $expirationDate->getTimestamp() - $now) + $this->cacheLifetimeCalculator->forExpirationDate($expirationDate), ); } } diff --git a/Classes/Helper/FrontendHelper.php b/Classes/Helper/FrontendHelper.php index 4cbd904..731a668 100644 --- a/Classes/Helper/FrontendHelper.php +++ b/Classes/Helper/FrontendHelper.php @@ -24,6 +24,7 @@ namespace CPSIT\Typo3CacheBags\Helper; use CPSIT\Typo3CacheBags\Exception\FrontendIsNotInitialized; +use Psr\Http\Message\ServerRequestInterface; use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController; /** @@ -44,4 +45,15 @@ public static function getTypoScriptFrontendController(): TypoScriptFrontendCont return $typoScriptFrontendController; } + + public static function getServerRequest(): ServerRequestInterface + { + $serverRequest = $GLOBALS['TYPO3_REQUEST'] ?? null; + + if (!($serverRequest instanceof ServerRequestInterface)) { + throw new FrontendIsNotInitialized(); + } + + return $serverRequest; + } } diff --git a/README.md b/README.md index a2f43b1..7bb00e6 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ database contents or short-living API requests. * Cache bag registry to handle generated cache bags * Cache expiration calculator for various use cases (query builder, query result etc.) * Event listener to override page cache expiration, based on registered page cache bags -* Compatible with TYPO3 11.5 LTS, 12.4 LTS and 13.1 +* Compatible with TYPO3 11.5 LTS, 12.4 LTS and 13.4 LTS ## 🔥 Installation diff --git a/composer.json b/composer.json index 0f643fe..d6e3b1b 100644 --- a/composer.json +++ b/composer.json @@ -14,9 +14,10 @@ "require": { "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0", "psr/event-dispatcher": "^1.0", - "typo3/cms-core": "~11.5.0 || ~12.4.0 || ~13.1.0", - "typo3/cms-extbase": "~11.5.0 || ~12.4.0 || ~13.1.0", - "typo3/cms-frontend": "~11.5.0 || ~12.4.0 || ~13.1.0" + "psr/http-message": "^1.0 || ^2.0", + "typo3/cms-core": "~11.5.0 || ~12.4.0 || ~13.4.0", + "typo3/cms-extbase": "~11.5.0 || ~12.4.0 || ~13.4.0", + "typo3/cms-frontend": "~11.5.0 || ~12.4.0 || ~13.4.0" }, "require-dev": { "armin/editorconfig-cli": "^1.8 || ^2.0", @@ -30,7 +31,7 @@ "saschaegerer/phpstan-typo3": "^1.10", "ssch/typo3-rector": "^2.6", "typo3/coding-standards": "^0.7.0 || ^0.8.0", - "typo3/testing-framework": "^7.0.2 || ^8.0.9" + "typo3/testing-framework": "^7.0.2 || ^8.0.9 || ^9.0.1" }, "autoload": { "psr-4": { diff --git a/composer.lock b/composer.lock index 6041d37..54d156b 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "3a594eaf500a78ddc4a3f4d50217030e", + "content-hash": "33bee9ff6c7c6f0505d0cc583a86cff4", "packages": [ { "name": "bacon/bacon-qr-code", diff --git a/dependency-checker.json b/dependency-checker.json new file mode 100644 index 0000000..429b826 --- /dev/null +++ b/dependency-checker.json @@ -0,0 +1,9 @@ +{ + "symbol-whitelist": [ + "TYPO3\\CMS\\Core\\Cache\\CacheDataCollector", + "TYPO3\\CMS\\Core\\Cache\\CacheTag", + "TYPO3\\CMS\\Core\\Schema\\Capability\\TcaSchemaCapability", + "TYPO3\\CMS\\Core\\Schema\\TcaSchema", + "TYPO3\\CMS\\Core\\Schema\\TcaSchemaFactory" + ] +} diff --git a/ext_emconf.php b/ext_emconf.php index 09d6aa5..4e8eed2 100644 --- a/ext_emconf.php +++ b/ext_emconf.php @@ -32,7 +32,7 @@ 'author_company' => 'coding. powerful. systems. CPS GmbH', 'constraints' => [ 'depends' => [ - 'typo3' => '11.5.0-13.1.99', + 'typo3' => '11.5.0-13.4.99', 'php' => '8.1.0-8.4.99', ], ], diff --git a/packaging_exclude.php b/packaging_exclude.php index 4f87ac7..8b9d0d0 100644 --- a/packaging_exclude.php +++ b/packaging_exclude.php @@ -38,6 +38,7 @@ 'CODE_OF_CONDUCT.md', 'composer.lock', 'CONTRIBUTING.md', + 'dependency-checker.json', 'editorconfig', 'gitattributes', 'gitignore', diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 21e9990..2f918b8 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,6 +1,51 @@ parameters: ignoreErrors: + - + message: "#^Access to constant RestrictionEndTime on an unknown class TYPO3\\\\CMS\\\\Core\\\\Schema\\\\Capability\\\\TcaSchemaCapability\\.$#" + count: 1 + path: Classes/Cache/Expiration/CacheExpirationCalculator.php + + - + message: "#^Access to constant RestrictionStartTime on an unknown class TYPO3\\\\CMS\\\\Core\\\\Schema\\\\Capability\\\\TcaSchemaCapability\\.$#" + count: 1 + path: Classes/Cache/Expiration/CacheExpirationCalculator.php + + - + message: "#^Call to method get\\(\\) on an unknown class TYPO3\\\\CMS\\\\Core\\\\Schema\\\\TcaSchemaFactory\\.$#" + count: 1 + path: Classes/Cache/Expiration/CacheExpirationCalculator.php + + - + message: "#^Call to method has\\(\\) on an unknown class TYPO3\\\\CMS\\\\Core\\\\Schema\\\\TcaSchemaFactory\\.$#" + count: 1 + path: Classes/Cache/Expiration/CacheExpirationCalculator.php + + - + message: "#^Class TYPO3\\\\CMS\\\\Core\\\\Schema\\\\TcaSchemaFactory not found\\.$#" + count: 1 + path: Classes/Cache/Expiration/CacheExpirationCalculator.php + + - + message: "#^Parameter \\$capability of anonymous function has invalid type TYPO3\\\\CMS\\\\Core\\\\Schema\\\\Capability\\\\TcaSchemaCapability\\.$#" + count: 1 + path: Classes/Cache/Expiration/CacheExpirationCalculator.php + - message: "#^Method CPSIT\\\\Typo3CacheBags\\\\Database\\\\Query\\\\QueriedTableAwareQueryBuilder\\:\\:getQueriedTables\\(\\) should return array\\ but returns array\\\\.$#" count: 1 path: Classes/Database/Query/QueriedTableAwareQueryBuilder.php + + - + message: "#^Instantiated class TYPO3\\\\CMS\\\\Core\\\\Cache\\\\CacheTag not found\\.$#" + count: 1 + path: Classes/EventListener/PageCacheBagRegisteredEventListener.php + + - + message: "#^Method CPSIT\\\\Typo3CacheBags\\\\EventListener\\\\PageCacheBagRegisteredEventListener\\:\\:convertCacheTagsToObjects\\(\\) has invalid return type TYPO3\\\\CMS\\\\Core\\\\Cache\\\\CacheTag\\.$#" + count: 1 + path: Classes/EventListener/PageCacheBagRegisteredEventListener.php + + - + message: "#^There is no request attribute \"frontend\\.cache\\.collector\" configured so we can't figure out the exact type to return when calling Psr\\\\Http\\\\Message\\\\ServerRequestInterface\\:\\:getAttribute$#" + count: 1 + path: Classes/EventListener/PageCacheBagRegisteredEventListener.php