Skip to content

Track result cache dependencies instead of a site-wide hash - #1071

Merged
mglaman merged 2 commits into
mainfrom
result-cache-value-tracking
Nov 5, 2026
Merged

mglaman merged 2 commits into
mainfrom
result-cache-value-tracking

Conversation

@mglaman

@mglaman mglaman commented Nov 5, 2026 •

Copy link
Copy Markdown
Owner

Fixes #1061. Targets 2.3.0: this raises phpstan/phpstan to ^2.3.

A bug this also fixes

On PHPStan 2.3, BootstrapResultCacheMetaExtension never invalidates the cache, so users get stale results until they clear it. PHPStan 2.3 defers bootstrap files and doesn't run them when restoring the cache. ServiceMap, ExtensionMap, and ConfigSchemaData are empty at that point, so the extension always hashed the same empty state.

Reproduced on the 2.2.3 code with PHPStan 2.3.5:

Change Files re-analyzed Result
Deprecate a service 0 Deprecation not reported
Add a module providing a missing class 0 class.notFound still reported
Clear the cache All Both correct

What changed

The cache now tracks what each file reads:

Change Before, as designed Now
A service definition Everything Files asking about that service
arguments or tags of a service Everything Nothing
A new service nothing asks about Everything Nothing
A schema file Everything Files reading config schema
An .info.yml edit Everything Nothing
A loadInclude() file created or changed Nothing Files loading it
An extension added, removed, or moved Everything Everything
$scope->trackValueDependency(ServiceResultCacheValueExtension::class, $serviceId);
$service = $this->serviceMap->getService($serviceId);
  • DrupalSite reads extensions, service definitions, and schema directories from disk without the bootstrap. The bootstrap uses it too, so the two can't disagree. This is its own commit with no behavior change.
  • Value extensions read DrupalSite, not the bootstrap-populated maps. A service's value covers its alias target, parent, and decorators.
  • ExtensionListDependencyRule makes every file depend on the extension list. It never reports an error. A new extension changes which classes exist, and a file whose error is a missing class has no other dependency to re-analyze it.
  • Core updates don't need tracking. PHPStan already re-analyzes the files depending on a changed Composer package.

phpcs.xml now sets searchAnnotations on UnusedUses. phpcbf was deleting the DependencyTracker imports, since the docs say that type belongs only in the PHPDoc.

Known limits

  • Schema tracking is one value for all schema files. Schema types reference each other across files, so mapping a config object to its files means parsing all of them. The config schema features are off by default.
  • service_provider.* entries and the synthetic services set in DrupalAutoloader aren't part of DrupalSite, so their value is always missing. They're derived from the extension list, which is tracked separately, or are hardcoded.

Testing

ResultCacheInvalidationTest runs real PHPStan processes with two workers against a site it changes, and asserts how many files get re-analyzed. It's the only kind of test that could have caught this, because the main process and workers see different state. It adds about 40 seconds per test job. An earlier version of this branch read the bootstrap-populated maps and re-analyzed all files on every run, which is the failure it now guards against.

ResultCacheValueExtensionsTest covers the values themselves.

Lint and the full suite pass locally on PHPStan 2.3.5 with 890 tests. I haven't measured the cost of the scan on a large site's warm run.

🤖 Generated with Claude Code

mglaman and others added 2 commits November 5, 2026 12:13
Extract extension discovery, services.yml parsing, and schema directories from
DrupalAutoloader into DrupalSite. PHPStan's result cache extensions run before
bootstrap files, so they need to read the site without running the bootstrap.

No change in behavior: the bootstrap reads the same data from the same place.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replace BootstrapResultCacheMetaExtension with value and file dependencies from
PHPStan 2.3, and require phpstan/phpstan ^2.3. Changing a service re-analyzes the
files asking about it, and changing schema re-analyzes the files reading config.
Adding or removing an extension still re-analyzes everything, because the
bootstrap registers an autoload namespace for each one.

This also fixes stale results on PHPStan 2.3. Restoring the cache runs before
bootstrap files, so the old extension hashed empty state and never invalidated.

ResultCacheInvalidationTest runs real PHPStan processes against a changing site,
because only a real run has the main process and workers disagree this way.

Fixes #1061

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mglaman
mglaman merged commit 517b460 into main Nov 5, 2026
19 of 20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

use more fine grained result-cache invalidation

1 participant