Skip to content

Commit bb143db

Browse files
ruudksamsonasik
authored andcommitted
[Caching] Add CacheMetaExtensionInterface for custom cache invalidation (#7933)
* [Caching] Add CacheMetaExtensionInterface for custom cache invalidation Same mechanism as PHPStan's ResultCacheMetaExtension — extensions implement getKey() and getHash() to provide additional metadata that is folded into the file cache key. When any extension's hash changes, all cached files are reprocessed. * Add e2e test for CacheMetaExtensionInterface cache invalidation Proves the extension actually invalidates cache by using a conditional rule that only triggers when an external file changes value.
1 parent 08d3175 commit bb143db

File tree

15 files changed

+343
-1
lines changed

15 files changed

+343
-1
lines changed

.github/workflows/e2e_with_cache.yaml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,25 @@ jobs:
5252
# this tests that a 2nd run with cache and "--dry-run" gives same results, see https://github.com/rectorphp/rector-src/pull/3614#issuecomment-1507742338
5353
- run: php ../e2eTestRunnerWithCache.php
5454
working-directory: ${{ matrix.directory }}
55+
56+
cache_meta_extension:
57+
runs-on: ubuntu-latest
58+
timeout-minutes: 3
59+
60+
name: End to end test - e2e/cache-meta-extension
61+
62+
steps:
63+
- uses: actions/checkout@v4
64+
65+
- uses: shivammathur/setup-php@v2
66+
with:
67+
php-version: '8.3'
68+
coverage: none
69+
70+
- run: composer install --ansi
71+
72+
- run: composer install --ansi
73+
working-directory: e2e/cache-meta-extension
74+
75+
- run: php e2eTestRunnerCacheInvalidation.php
76+
working-directory: e2e/cache-meta-extension
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/vendor
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"require": {
3+
"php": "^8.1"
4+
},
5+
"autoload": {
6+
"psr-4": {
7+
"App\\": "src/"
8+
}
9+
},
10+
"minimum-stability": "dev",
11+
"prefer-stable": true
12+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
#!/usr/bin/env php
2+
<?php
3+
4+
// Tests that CacheMetaExtensionInterface invalidates cache when the hash changes.
5+
//
6+
// Step 1: Run Rector with enabled.txt=false and --clear-cache → no changes, cache populated
7+
// Step 2: Change enabled.txt to true → cache invalidated → rule triggers → changes reported
8+
9+
use Rector\Console\Formatter\ColorConsoleDiffFormatter;
10+
use Rector\Console\Style\SymfonyStyleFactory;
11+
use Rector\Differ\DefaultDiffer;
12+
use Rector\Util\Reflection\PrivatesAccessor;
13+
use Symfony\Component\Console\Command\Command;
14+
15+
$projectRoot = __DIR__ .'/..';
16+
$rectorBin = $projectRoot . '/../bin/rector';
17+
$autoloadFile = $projectRoot . '/../vendor/autoload.php';
18+
19+
require_once __DIR__ . '/../../vendor/autoload.php';
20+
21+
$symfonyStyleFactory = new SymfonyStyleFactory(new PrivatesAccessor());
22+
$symfonyStyle = $symfonyStyleFactory->create();
23+
24+
$e2eCommand = 'php '. $rectorBin .' process --dry-run --no-ansi -a '. $autoloadFile;
25+
26+
// Step 1: enabled=false, clear cache → no changes
27+
file_put_contents(__DIR__ . '/enabled.txt', "false\n");
28+
29+
$output = [];
30+
exec($e2eCommand . ' --clear-cache', $output, $exitCode);
31+
$outputString = trim(implode("\n", $output));
32+
33+
if (! str_contains($outputString, '[OK] Rector is done!')) {
34+
$symfonyStyle->error('Step 1 failed: Expected no changes with enabled=false');
35+
$symfonyStyle->writeln($outputString);
36+
exit(Command::FAILURE);
37+
}
38+
39+
$symfonyStyle->success('Step 1 passed: No changes with enabled=false');
40+
41+
// Step 2: enabled=true, no --clear-cache → cache meta invalidated → rule triggers
42+
file_put_contents(__DIR__ . '/enabled.txt', "true\n");
43+
44+
$output = [];
45+
exec($e2eCommand, $output, $exitCode);
46+
$outputString = trim(implode("\n", $output));
47+
$outputString = str_replace(__DIR__, '.', $outputString);
48+
49+
$expectedOutput = trim((string) file_get_contents(__DIR__ . '/expected-output.diff'));
50+
51+
// Restore enabled.txt
52+
file_put_contents(__DIR__ . '/enabled.txt', "false\n");
53+
54+
if ($outputString === $expectedOutput) {
55+
$symfonyStyle->success('Step 2 passed: Cache invalidated, rule triggered');
56+
exit(Command::SUCCESS);
57+
}
58+
59+
$symfonyStyle->error('Step 2 failed: Expected cache invalidation to trigger the rule');
60+
61+
$defaultDiffer = new DefaultDiffer();
62+
$colorConsoleDiffFormatter = new ColorConsoleDiffFormatter();
63+
$diff = $colorConsoleDiffFormatter->format($defaultDiffer->diff($outputString, $expectedOutput));
64+
$symfonyStyle->writeln($diff);
65+
66+
exit(Command::FAILURE);
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
false
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
1 file with changes
2+
===================
3+
4+
1) src/DeadConstructor.php:2
5+
6+
---------- begin diff ----------
7+
@@ @@
8+
9+
final class DeadConstructor
10+
{
11+
- public function __construct()
12+
- {
13+
- }
14+
}
15+
----------- end diff -----------
16+
17+
Applied rules:
18+
* ConditionalEmptyConstructorRector
19+
20+
21+
[OK] 1 file would have been changed (dry-run) by Rector
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use App\ConditionalEmptyConstructorRector;
6+
use App\EnabledFlagCacheMetaExtension;
7+
use Rector\Caching\ValueObject\Storage\FileCacheStorage;
8+
use Rector\Config\RectorConfig;
9+
10+
require_once __DIR__ . '/vendor/autoload.php';
11+
12+
return static function (RectorConfig $rectorConfig): void {
13+
$rectorConfig->cacheClass(FileCacheStorage::class);
14+
15+
$rectorConfig->paths([
16+
__DIR__ . '/src/DeadConstructor.php',
17+
]);
18+
19+
$rectorConfig->rule(ConditionalEmptyConstructorRector::class);
20+
$rectorConfig->cacheMetaExtension(EnabledFlagCacheMetaExtension::class);
21+
};
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App;
6+
7+
use PhpParser\Node;
8+
use PhpParser\Node\Stmt\Class_;
9+
use PhpParser\Node\Stmt\ClassMethod;
10+
use Rector\Rector\AbstractRector;
11+
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
12+
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
13+
14+
/**
15+
* Removes empty constructors only when enabled.txt contains "true".
16+
* Used to test CacheMetaExtensionInterface e2e.
17+
*/
18+
final class ConditionalEmptyConstructorRector extends AbstractRector
19+
{
20+
public function getRuleDefinition(): RuleDefinition
21+
{
22+
return new RuleDefinition('Remove empty constructors conditionally', [
23+
new CodeSample(
24+
<<<'CODE_SAMPLE'
25+
class SomeClass
26+
{
27+
public function __construct()
28+
{
29+
}
30+
}
31+
CODE_SAMPLE
32+
,
33+
<<<'CODE_SAMPLE'
34+
class SomeClass
35+
{
36+
}
37+
CODE_SAMPLE
38+
),
39+
]);
40+
}
41+
42+
/**
43+
* @return array<class-string<Node>>
44+
*/
45+
public function getNodeTypes(): array
46+
{
47+
return [Class_::class];
48+
}
49+
50+
/**
51+
* @param Class_ $node
52+
*/
53+
public function refactor(Node $node): ?Class_
54+
{
55+
$enabled = trim((string) file_get_contents(__DIR__ . '/../enabled.txt'));
56+
57+
if ($enabled !== 'true') {
58+
return null;
59+
}
60+
61+
$hasChanged = false;
62+
63+
foreach ($node->stmts as $key => $stmt) {
64+
if (! $stmt instanceof ClassMethod) {
65+
continue;
66+
}
67+
68+
if (! $this->isName($stmt, '__construct')) {
69+
continue;
70+
}
71+
72+
if ($stmt->stmts !== null && $stmt->stmts !== []) {
73+
continue;
74+
}
75+
76+
unset($node->stmts[$key]);
77+
$hasChanged = true;
78+
}
79+
80+
if ($hasChanged) {
81+
return $node;
82+
}
83+
84+
return null;
85+
}
86+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
final class DeadConstructor
4+
{
5+
public function __construct()
6+
{
7+
}
8+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App;
6+
7+
use Rector\Caching\Contract\CacheMetaExtensionInterface;
8+
9+
final class EnabledFlagCacheMetaExtension implements CacheMetaExtensionInterface
10+
{
11+
public function getKey(): string
12+
{
13+
return 'enabled-flag';
14+
}
15+
16+
public function getHash(): string
17+
{
18+
return (string) file_get_contents(__DIR__ . '/../enabled.txt');
19+
}
20+
}

0 commit comments

Comments
 (0)