From a5f0ff4b76ae6eb1b28b533bf6bdb6e037063c36 Mon Sep 17 00:00:00 2001 From: Lamm Date: Tue, 7 Oct 2025 09:55:32 +0200 Subject: [PATCH 1/3] Adds the possibility to declare a cache as immutable --- Classes/Cache/CacheManager.php | 131 +++++++++++++++++++++ Documentation/system-information.png | Bin 0 -> 42355 bytes README.md | 64 ++++++++++- Tests/ImmutableCacheTagTest.php | 166 +++++++++++++++++++++++++++ 4 files changed, 360 insertions(+), 1 deletion(-) create mode 100644 Documentation/system-information.png create mode 100644 Tests/ImmutableCacheTagTest.php diff --git a/Classes/Cache/CacheManager.php b/Classes/Cache/CacheManager.php index a2dc1b4..81430bc 100644 --- a/Classes/Cache/CacheManager.php +++ b/Classes/Cache/CacheManager.php @@ -4,6 +4,7 @@ namespace Weakbit\FallbackCache\Cache; +use Override; use Psr\EventDispatcher\EventDispatcherInterface; use Psr\Log\LoggerAwareInterface; use Psr\Log\LoggerAwareTrait; @@ -13,6 +14,7 @@ use TYPO3\CMS\Core\Cache\Exception\InvalidBackendException; use TYPO3\CMS\Core\Cache\Exception\InvalidCacheException; use TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException; +use TYPO3\CMS\Core\Cache\Exception\NoSuchCacheGroupException; use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface; use TYPO3\CMS\Core\Cache\Frontend\VariableFrontend; use TYPO3\CMS\Core\Utility\GeneralUtility; @@ -154,6 +156,11 @@ private function isStatusRed(string $identifier): bool }; } + private function isImmutable(string $identifier): bool + { + return (bool)($this->cacheConfigurations[$identifier]['tags'][0]['immutable'] ?? false); + } + /** * @throws DuplicateIdentifierException * @throws InvalidBackendException @@ -203,4 +210,128 @@ private function registerFallback(string $identifier, string $fallback): void $this->logger?->warning('Registering fallback cache ' . $fallback . ' for ' . $identifier); $this->fallbacks[$identifier] = $fallback; } + + /** + * @inheritdoc + */ + #[Override] + public function flushCaches(): void + { + $this->createAllCaches(); + foreach ($this->caches as $cache) { + if ($this->isImmutable($cache->getIdentifier())) { + continue; + } + + $cache->flush(); + } + } + + /** + * @inheritdoc + */ + #[Override] + public function flushCachesInGroup($groupIdentifier): void + { + $this->createAllCaches(); + if (!isset($this->cacheGroups[$groupIdentifier])) { + throw new NoSuchCacheGroupException("No cache in the specified group '" . $groupIdentifier . "'", 1390334120); + } + + foreach ($this->cacheGroups[$groupIdentifier] as $cacheIdentifier) { + if (isset($this->caches[$cacheIdentifier])) { + if ($this->isImmutable($cacheIdentifier)) { + continue; + } + + $this->caches[$cacheIdentifier]->flush(); + } + } + } + + /** + * @param string $groupIdentifier + * @param string $tag Tag to search for + * @inheritdoc + */ + #[Override] + public function flushCachesInGroupByTag($groupIdentifier, $tag): void + { + if (empty($tag)) { + return; + } + + $this->createAllCaches(); + if (!isset($this->cacheGroups[$groupIdentifier])) { + throw new NoSuchCacheGroupException("No cache in the specified group '" . $groupIdentifier . "'", 1390337129); + } + + foreach ($this->cacheGroups[$groupIdentifier] as $cacheIdentifier) { + if (isset($this->caches[$cacheIdentifier])) { + if ($this->isImmutable($cacheIdentifier)) { + continue; + } + + $this->caches[$cacheIdentifier]->flushByTag($tag); + } + } + } + + /** + * @inheritdoc + */ + #[Override] + public function flushCachesInGroupByTags($groupIdentifier, array $tags): void + { + if ($tags === []) { + return; + } + + $this->createAllCaches(); + if (!isset($this->cacheGroups[$groupIdentifier])) { + throw new NoSuchCacheGroupException("No cache in the specified group '" . $groupIdentifier . "'", 1390337130); + } + + foreach ($this->cacheGroups[$groupIdentifier] as $cacheIdentifier) { + if (isset($this->caches[$cacheIdentifier])) { + if ($this->isImmutable($cacheIdentifier)) { + continue; + } + + $this->caches[$cacheIdentifier]->flushByTags($tags); + } + } + } + + /** + * @inheritdoc + */ + #[Override] + public function flushCachesByTag($tag): void + { + $this->createAllCaches(); + foreach ($this->caches as $cache) { + if ($this->isImmutable($cache->getIdentifier())) { + continue; + } + + $cache->flushByTag($tag); + } + } + + /** + * @inheritdoc + */ + #[Override] + public function flushCachesByTags(array $tags): void + { + $this->createAllCaches(); + foreach ($this->caches as $cache) { + if ($this->isImmutable($cache->getIdentifier())) { + continue; + } + + $cache->flushByTags($tags); + } + } } diff --git a/Documentation/system-information.png b/Documentation/system-information.png new file mode 100644 index 0000000000000000000000000000000000000000..9565de979598f041c74bbdef4f0decc768ba92ae GIT binary patch literal 42355 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�<9lu|}miBB1x{ zpFhfC-gKYsP}#+Fz*_IxhHrV|da{sY22dDK+>yL?>>(4)^o*{DXL3`a%*yrqb?xBu zt?+qa;luKpcm7r54z}{tuR6v3gFZm_Wg03 zZWc<q@z?$o5*vD|f;EfRge48Z=8R);7S z(sgnyjxAI^@NFJkH?9hyi+5F}{(ii0C6XJuRS!2e8Z-T2sL_yq-BETY9Ak4!$x+$v za2IjPtV0KeGRwnHvqmR-N!d;o?JWbiaMfoqUP-634r|ubiknr8W>QW7d)PJ&PJFGJ zR8M(X&^%UX{#5+4?b3zl?)M9Pu9n|EKj&2Y*F!JWe65+Uu{`OxwNBBc#pZr~O?=I} z&U%Nl<}zPpt%IL`w;gvSg9Kxz7ozpkewV|v0o+nbF8Zy4EewZlYMxni{%6^aVW~Cu zdWkaI>)z$y3`8BOK5l!kX(`{@|C5u%ktdjZ*;-j0fFPqz1Bv+B?-U4-!-W@wa_5&K zi>8jhl#-aaY$dV4w~DqG)%lRr(b7U0VJdm($oI23}i}yYHfY%Yl@`xHKE>CDeLed_|7TK>XKLeQohU>@9{PzpQk zctc#45R{k_O*QRF_=kdxcI7Z$a+L#%V9`eHq+nhn?9c^4rtr>Qw1b; zfb4_*M>hNrw{Iq#Gl}H6;Q2&OM0ro&u3LNy%!;`Q2#=KSF&O8*d=yD4_0wxKQqEHa*$k~GcyYd3YL6VOkXk_^qdVuQxwfu6ti@} zO|r>Ttm{9*g=@3NKytX($Sgpu_?6yF1y+et1s&!JUJf`wWrq z<(-WR6ql;g;u2@7+uIlNKMz*x{_U4K%ou<>g(wxvqB)L&uq?fH)TsW=Jn)D8zWlXo z@=td4mH%MSaI2~4XMFHSb>oHzi#52E9oWSOq}HGo9Cctw^dmT%aT)J&JN3fRX4b&H zaH4FSCSO0UDiuJ1ec$KQ*ESSf-uQ~1y}p5Hhu$Z?BKc6kZtxk z{<0DkR$WPk)Y(+3Y10bEU*zKHbrB@Hs}|oDYv@2K6hJOpDaDWdY5(= z=?o`bxTBKAVo1`}O?T4OnYGu#-_I;k`#|oC6-f>kRGj-^MIm=i;y|vB<*FYu!#t)& zlKywx1^ym;NMR<|a*Vz5_4z}0+&%i#HGE#AqN%nebpRj{?F@BqK(Le$_*%v?lA3{c z>#@^v)s&`=mRfR~ZBIpMTb|0d(NQ+D6m%aQ(>^~XDmE#;CfCyI^yEnG3L+YxvhJn3 qtJWsV@J~j3M#aH \TYPO3\CMS\Core\Cache\Frontend\VariableFrontend::class, + 'backend' => \TYPO3\CMS\Core\Cache\Backend\FileBackend::class, + 'options' => [ + 'defaultLifetime' => 604800, + ], + 'groups' => [ + 'system', + ], + 'tags' => [ + ['name' => 'cache', 'identifier' => 'my_immutable_cache', 'immutable' => true] + ] +]; +``` + +### Behavior of Immutable Caches + +When a cache is marked as immutable: + +1. It will **not** be cleared when `flushCaches()` is called +2. It will **not** be cleared when `flushCachesByTag()` is called with any tags +3. It will **not** be cleared when `flushCachesInGroup()` is called, even if the cache belongs to that group + +This feature ensures that important cache entries remain available even during maintenance operations or when other parts of the system trigger cache flushes. + +### When to Use Immutable Caches + +Consider using immutable caches for: + +- Compiled templates or CSS/JS assets that rarely change +- Code caches that are expensive to regenerate +- Core configuration data that is only updated during system upgrades +- Any cache data where regeneration would cause significant load on the system + +When you need to update an immutable cache, you'll need to manually clear it using direct backend operations or by temporarily removing the immutable flag. + +# How to Access the Cache Status + +1. Log in to your TYPO3 backend +2. Look at the top toolbar (the black bar at the top of the screen) +3. Find the system information icon (typically shows system details like TYPO3 version) +4. Click on this icon to see a dropdown menu +5. The cache status will be displayed + +![Cache Status in System Information Toolbar](Documentation/system-information.png) + # TODO -- [ ] Give a possibility to see the actual state of the caches + - [ ] Refactor addCacheStatus to comply with external calls +# Credits Inspired by https://packagist.org/packages/b13/graceful-cache + +Uses code from https://github.com/marketing-factory/typo3_prometheus \ No newline at end of file diff --git a/Tests/ImmutableCacheTagTest.php b/Tests/ImmutableCacheTagTest.php new file mode 100644 index 0000000..9f93974 --- /dev/null +++ b/Tests/ImmutableCacheTagTest.php @@ -0,0 +1,166 @@ + [ + 'Objects' => [ + CacheManager::class => [ + 'className' => \Weakbit\FallbackCache\Cache\CacheManager::class, + ], + ], + 'caching' => [ + 'cacheConfigurations' => [ + 'mutable_cache' => [ + 'backend' => TransientMemoryBackend::class, + ], + 'immutable_cache' => [ + 'backend' => TransientMemoryBackend::class, + ], + ], + ], + ], + ]; + + protected function setUp(): void + { + // Testing framework needed env vars + putenv('typo3DatabaseUsername='); + putenv('typo3DatabasePassword='); + putenv('typo3DatabaseHost='); + putenv('typo3DatabaseName='); + putenv('typo3DatabaseDriver=pdo_sqlite'); + + parent::setUp(); + + $cacheManager = GeneralUtility::makeInstance(CacheManager::class); + assert($cacheManager instanceof \Weakbit\FallbackCache\Cache\CacheManager); + GeneralUtility::setSingletonInstance(CacheManager::class, $cacheManager); + + $cacheManager->setCacheConfigurations([ + 'mutable_cache' => [ + 'frontend' => VariableFrontend::class, + 'backend' => TransientMemoryBackend::class, + 'groups' => ['system'], + 'tags' => [ + ['name' => 'cache', 'identifier' => 'mutable_cache', 'immutable' => false] + ] + ], + 'immutable_cache' => [ + 'frontend' => VariableFrontend::class, + 'backend' => TransientMemoryBackend::class, + 'groups' => ['system'], + 'tags' => [ + ['name' => 'cache', 'identifier' => 'immutable_cache', 'immutable' => true] + ] + ], + ]); + } + + /** + * @test + * @throws NoSuchCacheException + */ + public function flushMethodDoesNotFlushImmutableCaches(): void + { + $cacheManager = GeneralUtility::makeInstance(CacheManager::class); + assert($cacheManager instanceof \Weakbit\FallbackCache\Cache\CacheManager); + + // Store values in both mutable and immutable caches (with and without tags) + $mutableCache = $cacheManager->getCache('mutable_cache'); + $immutableCache = $cacheManager->getCache('immutable_cache'); + + $mutableCache->set('test_key', 'test_value'); + $mutableCache->set('tagged_key', 'tagged_value', ['flush_tag']); + + $immutableCache->set('test_key', 'test_value'); + $immutableCache->set('tagged_key', 'tagged_value', ['flush_tag']); + + // Verify values are set + $this->assertEquals('test_value', $mutableCache->get('test_key')); + $this->assertEquals('tagged_value', $mutableCache->get('tagged_key')); + $this->assertEquals('test_value', $immutableCache->get('test_key')); + $this->assertEquals('tagged_value', $immutableCache->get('tagged_key')); + + // First test tag-based flushing + $cacheManager->flushCachesByTag('flush_tag'); + + // Only mutable cache entry with the tag should be flushed + $this->assertEquals('test_value', $mutableCache->get('test_key')); + $this->assertFalse($mutableCache->get('tagged_key')); + $this->assertEquals('test_value', $immutableCache->get('test_key')); + $this->assertEquals('tagged_value', $immutableCache->get('tagged_key')); + + // Reset tagged entry + $mutableCache->set('tagged_key', 'tagged_value', ['flush_tag']); + + // Now flush all caches + $cacheManager->flushCaches(); + + // Mutable cache should be empty, immutable cache should still have values + $this->assertFalse($mutableCache->get('test_key')); + $this->assertFalse($mutableCache->get('tagged_key')); + $this->assertEquals('test_value', $immutableCache->get('test_key')); + $this->assertEquals('tagged_value', $immutableCache->get('tagged_key')); + } + + /** + * @test + * @throws NoSuchCacheException|NoSuchCacheGroupException + */ + public function flushCachesInGroupDoesNotFlushImmutableCaches(): void + { + $cacheManager = GeneralUtility::makeInstance(CacheManager::class); + assert($cacheManager instanceof \Weakbit\FallbackCache\Cache\CacheManager); + + // Store values in both mutable and immutable caches (with and without tags) + $mutableCache = $cacheManager->getCache('mutable_cache'); + $immutableCache = $cacheManager->getCache('immutable_cache'); + + $mutableCache->set('test_key', 'test_value'); + $mutableCache->set('tagged_key', 'tagged_value', ['group_tag']); + + $immutableCache->set('test_key', 'test_value'); + $immutableCache->set('tagged_key', 'tagged_value', ['group_tag']); + + // First test tag-based flushing + $cacheManager->flushCachesByTag('group_tag'); + + // Only mutable cache entry with the tag should be flushed + $this->assertEquals('test_value', $mutableCache->get('test_key')); + $this->assertFalse($mutableCache->get('tagged_key')); + $this->assertEquals('test_value', $immutableCache->get('test_key')); + $this->assertEquals('tagged_value', $immutableCache->get('tagged_key')); + + // Reset tagged entry + $mutableCache->set('tagged_key', 'tagged_value', ['group_tag']); + + // Flush caches in 'system' group + $cacheManager->flushCachesInGroup('system'); + + // Mutable cache should be empty, immutable cache should still have values + $this->assertFalse($mutableCache->get('test_key')); + $this->assertFalse($mutableCache->get('tagged_key')); + $this->assertEquals('test_value', $immutableCache->get('test_key')); + $this->assertEquals('tagged_value', $immutableCache->get('tagged_key')); + } +} From cf7b8d7a26f10957400bddf1e63ba3828625325a Mon Sep 17 00:00:00 2001 From: Lamm Date: Mon, 2 Mar 2026 15:39:36 +0100 Subject: [PATCH 2/3] Add test attributes --- Tests/FallbackCacheTest.php | 6 ++++++ Tests/ImmutableCacheTagTest.php | 3 +++ composer.json | 2 +- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/Tests/FallbackCacheTest.php b/Tests/FallbackCacheTest.php index 0ad5863..d407811 100644 --- a/Tests/FallbackCacheTest.php +++ b/Tests/FallbackCacheTest.php @@ -4,6 +4,7 @@ namespace Weakbit\FallbackCache\Tests; +use PHPUnit\Framework\Attributes\Test; use Psr\EventDispatcher\EventDispatcherInterface; use TYPO3\CMS\Core\Cache\Backend\FileBackend; use TYPO3\CMS\Core\Cache\Backend\NullBackend; @@ -114,6 +115,7 @@ protected function setUp(): void * @test * @throws NoSuchCacheException */ + #[Test] public function testGetGoodCacheReturnsCache(): void { $cacheManager = GeneralUtility::makeInstance(CacheManager::class); @@ -126,6 +128,7 @@ public function testGetGoodCacheReturnsCache(): void * @test * @throws NoSuchCacheException */ + #[Test] public function testGetBadCacheThrowsException(): void { $cacheManager = GeneralUtility::makeInstance(CacheManager::class); @@ -138,6 +141,7 @@ public function testGetBadCacheThrowsException(): void * @test * @throws NoSuchCacheException */ + #[Test] public function testGetBrokenCacheReturnsFallbackCache(): void { $cacheManager = GeneralUtility::makeInstance(CacheManager::class); @@ -150,6 +154,7 @@ public function testGetBrokenCacheReturnsFallbackCache(): void * @test * @throws NoSuchCacheException */ + #[Test] public function testGetStatusRedCacheReturnsFallbackCache(): void { $cacheManager = GeneralUtility::makeInstance(CacheManager::class); @@ -170,6 +175,7 @@ public function testGetStatusRedCacheReturnsFallbackCache(): void * @test * @throws NoSuchCacheException */ + #[Test] public function testCacheBackendStateVerificationAfterFallback(): void { $cacheManager = GeneralUtility::makeInstance(CacheManager::class); diff --git a/Tests/ImmutableCacheTagTest.php b/Tests/ImmutableCacheTagTest.php index 9f93974..65e4daa 100644 --- a/Tests/ImmutableCacheTagTest.php +++ b/Tests/ImmutableCacheTagTest.php @@ -4,6 +4,7 @@ namespace Weakbit\FallbackCache\Tests; +use PHPUnit\Framework\Attributes\Test; use TYPO3\CMS\Core\Cache\Backend\TransientMemoryBackend; use TYPO3\CMS\Core\Cache\CacheManager; use TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException; @@ -80,6 +81,7 @@ protected function setUp(): void * @test * @throws NoSuchCacheException */ + #[Test] public function flushMethodDoesNotFlushImmutableCaches(): void { $cacheManager = GeneralUtility::makeInstance(CacheManager::class); @@ -127,6 +129,7 @@ public function flushMethodDoesNotFlushImmutableCaches(): void * @test * @throws NoSuchCacheException|NoSuchCacheGroupException */ + #[Test] public function flushCachesInGroupDoesNotFlushImmutableCaches(): void { $cacheManager = GeneralUtility::makeInstance(CacheManager::class); diff --git a/composer.json b/composer.json index 13b9ac6..a86afe8 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,7 @@ "phpstan/extension-installer": "^1.1", "pluswerk/grumphp-config": "*", "rybakit/msgpack": "*", - "saschaegerer/phpstan-typo3": "*", + "saschaegerer/phpstan-typo3": "^2.0.0 || ^3.0.1", "ssch/typo3-rector": "*", "typo3/testing-framework": "*" }, From 1ec4a31b53c3a933a68317a080bef725a8317788 Mon Sep 17 00:00:00 2001 From: Lamm Date: Mon, 2 Mar 2026 16:22:34 +0100 Subject: [PATCH 3/3] Fix rector changes --- .github/workflows/tasks.yml | 20 +++++++++++++------ .../CacheStatusEventListener.php | 2 +- ...formationToolbarCollectorEventListener.php | 2 +- composer.json | 2 +- rector.php | 2 ++ 5 files changed, 19 insertions(+), 9 deletions(-) diff --git a/.github/workflows/tasks.yml b/.github/workflows/tasks.yml index 93e655c..a618522 100644 --- a/.github/workflows/tasks.yml +++ b/.github/workflows/tasks.yml @@ -13,13 +13,12 @@ jobs: fail-fast: false max-parallel: 2 matrix: - php: [ '81', '82', '83', '84' ] + php: [ '82', '83', '84' ] typo3: [ '11', '12', '13' ] exclude: - php: '84' typo3: '11' - - php: '81' - typo3: '13' + outputs: result: ${{ steps.set-result.outputs.result }} php: ${{ matrix.php }} @@ -32,13 +31,16 @@ jobs: - name: Install Node.js (Alpine) run: apk add --no-cache nodejs npm - uses: actions/checkout@v4 + - name: Get composer project name + id: composer-name + run: echo "name=$(php -r "echo str_replace('/', '-', json_decode(file_get_contents('composer.json'))->name);")" >> $GITHUB_OUTPUT - uses: actions/cache@v4 with: path: | ~/.composer/cache/files vendor node_modules - key: ${{ matrix.typo3 }}-${{ matrix.php }}-composer-${{ hashFiles('**/composer.json') }} + key: ${{ matrix.typo3 }}-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }} restore-keys: | ${{ matrix.typo3 }}-${{ matrix.php }}-composer- - run: git config --global --add safe.directory $GITHUB_WORKSPACE @@ -54,17 +56,23 @@ jobs: - uses: actions/upload-artifact@v4 if: always() with: - name: ${{ matrix.typo3 }}-${{ matrix.php }}-composer-${{ hashFiles('**/composer.json') }} + name: ${{ steps.composer-name.outputs.name }}-${{ matrix.typo3 }}-${{ matrix.php }} path: summary/results.txt summary: runs-on: ubuntu-latest needs: build if: always() steps: + - uses: actions/checkout@v4 + - name: Get composer project name + id: composer-name + run: echo "name=$(python3 -c "import json; d=json.load(open('composer.json')); print(d['name'].replace('/', '-'))")" >> $GITHUB_OUTPUT - uses: actions/download-artifact@v4 with: path: summary + pattern: ${{ steps.composer-name.outputs.name }}-* + merge-multiple: false - name: Show results run: | echo "### Matrix results" - cat summary/**/results.txt + find summary -name results.txt | sort | xargs cat diff --git a/Classes/EventListener/CacheStatusEventListener.php b/Classes/EventListener/CacheStatusEventListener.php index cd7e7e7..8d6ced3 100644 --- a/Classes/EventListener/CacheStatusEventListener.php +++ b/Classes/EventListener/CacheStatusEventListener.php @@ -12,7 +12,7 @@ use Weakbit\FallbackCache\Event\CacheStatusEvent; #[AsEventListener( - identifier: \Weakbit\FallbackCache\EventListener\CacheStatusEventListener::class, + identifier: CacheStatusEventListener::class, event: CacheStatusEvent::class, )] class CacheStatusEventListener diff --git a/Classes/EventListener/SystemInformationToolbarCollectorEventListener.php b/Classes/EventListener/SystemInformationToolbarCollectorEventListener.php index 58a2d56..145e372 100644 --- a/Classes/EventListener/SystemInformationToolbarCollectorEventListener.php +++ b/Classes/EventListener/SystemInformationToolbarCollectorEventListener.php @@ -19,7 +19,7 @@ * displays some information about caches in the system information toolbar */ #[AsEventListener( - identifier: \Weakbit\FallbackCache\EventListener\SystemInformationToolbarCollectorEventListener::class, + identifier: SystemInformationToolbarCollectorEventListener::class, event: SystemInformationToolbarCollectorEvent::class, )] class SystemInformationToolbarCollectorEventListener diff --git a/composer.json b/composer.json index a86afe8..b218d27 100644 --- a/composer.json +++ b/composer.json @@ -6,7 +6,7 @@ ], "type": "typo3-cms-extension", "require": { - "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0", + "php": "~8.2.0 || ~8.3.0 || ~8.4.0", "typo3/cms-core": "~11.5.0 || ~12.4.0 || ~13.4.0" }, "require-dev": { diff --git a/rector.php b/rector.php index a2f0201..a852f25 100644 --- a/rector.php +++ b/rector.php @@ -11,6 +11,7 @@ use Rector\Privatization\Rector\Class_\FinalizeClassesWithoutChildrenRector; use Rector\Privatization\Rector\ClassMethod\PrivatizeFinalClassMethodRector; use Rector\Privatization\Rector\Property\PrivatizeFinalClassPropertyRector; +use Ssch\TYPO3Rector\CodeQuality\General\GeneralUtilityMakeInstanceToConstructorPropertyRector; use Ssch\TYPO3Rector\Rector\v11\v0\DateTimeAspectInsteadOfGlobalsExecTimeRector; return static function (RectorConfig $rectorConfig): void { @@ -56,6 +57,7 @@ static function ($path): bool { //DateTimeAspectInsteadOfGlobalsExecTimeRector::class, RemoveExtraParametersRector::class, RemoveUnusedPrivateMethodRector::class, + GeneralUtilityMakeInstanceToConstructorPropertyRector::class, /** * rector should not touch these files