Skip to content

Add the attributeServicesDirectories section - #6918

Merged
ondrejmirtes merged 5 commits into
2.3.xfrom
attribute-services-section
Nov 2, 2026
Merged

ondrejmirtes merged 5 commits into
2.3.xfrom
attribute-services-section

Conversation

@ondrejmirtes

Copy link
Copy Markdown
Member

Implements the attribute-services part of #6283 step by step, on top of the merged Conditional (#6914) and @api attributes (#6916).

New top-level section:

attributeServicesDirectories:
	- src

Classes in these directories can use all @api DI attributes: AutowiredService, NonAutowiredService, RegisteredRule, RegisteredCollector, GenerateFactory, Conditional, and AutowiredParameter on constructor parameters.

Commits:

  1. Extract AttributeServicesRegistrar and AttributeTargetsProvider from AutowiredAttributeServicesExtension. No behavior change, tagged services for levels 0/2/5/9/max with bleeding edge are identical.
  2. The section, its resolver and the container cache key. Directories must be covered by psr-4 or classmap autoload rules and need PHP 8.0+. Installed packages contribute their version to the key, the project's own directories and path repositories contribute hashes of files that can carry the attributes.
  3. Discovery from Composer autoload data and native reflection, no file parsing. A content prefilter keeps classes without PHPStan attributes from being autoloaded. On PHP 8.0, Conditional takes the NewExpression marker array instead of new.
  4. Result cache invalidation. Registered discovered classes go to the attributeServicesClasses parameter and are treated like classes from services:. A Composer update of a package owning a directory invalidates the whole result cache. Covered by a new e2e test.

Performance, with phpstan-strict-rules and phpstan-deprecation-rules converted to attributes in vendor/ (56 services): cold container compile 684 ms with neon vs. 681 ms with attributes, cache hit 119 ms in both. Discovery takes 5.5 ms only while compiling, the resolver 3.4 ms on every run.

Docs: phpstan/phpstan#15648

AttributeServicesRegistrar owns turning attribute targets into service
definitions, including the Conditional handling. AttributeTargetsProvider
is the single view over vendor/attributes.php and the targets that will
be discovered in attributeServicesDirectories. No behavior change.

Amp-Thread-ID: https://ampcode.com/threads/T-01a19cd4-be31-75b4-bd15-67786eff5983
…ntainer cache key

The top-level section lists directories whose classes may carry PHPStan's
DI attributes. Paths are relative to the declaring config file. The resolver
checks each directory against the psr-4 and classmap autoload rules of the
owning Composer package or the root project and requires PHP 8.0+. It also
computes the directory's part of the container cache key: the package version
for installed vendor packages, file hashes for the project's own directories
and path repositories.

Amp-Thread-ID: https://ampcode.com/threads/T-01a19cd4-be31-75b4-bd15-67786eff5983
AttributeServicesDiscoverer finds candidate classes through the psr-4 path
contract and vendor/composer/autoload_classmap.php and reads the @api
attributes with native reflection: AutowiredService, NonAutowiredService,
RegisteredRule, RegisteredCollector, GenerateFactory, Conditional, and
AutowiredParameter on constructor parameters. No file is parsed. A content
prefilter keeps classes without PHPStan attributes from being autoloaded.
PHPStan-internal attributes, and files that mention a DI attribute but
yield no autoloadable class, are errors.

PHP 8.0 has no new in attribute arguments, so Conditional there takes
[NewExpression marker => [Class::class, [arguments]]], the same array
simple-downgrader writes and composer-attribute-collector understands.

Discovery runs lazily while the container compiles, so a container cache
hit loads no third-party class. The container cache key of the project's
own directories hashes only the files the same prefilter accepts, so
editing a class without PHPStan's attributes does not recompile the
container.

Amp-Thread-ID: https://ampcode.com/threads/T-01a19cd4-be31-75b4-bd15-67786eff5983
Classes registered from attributeServicesDirectories are services, so a
change in them can affect the analysis of every file.

AutowiredAttributeServicesExtension stores the registered discovered classes
in the attributeServicesClasses parameter. ResultCacheManager treats them
like the classes from the services section: editing one of them, or one of
their dependencies, invalidates the result cache. Other classes in the
same directories do not.

A Composer update of a package owning one of the directories invalidates the
whole result cache, like it does for packages with services in the config.
PackageDependencyResolver::resolveDirectoryPackage() also matches a directory
that is a package's install path.

Amp-Thread-ID: https://ampcode.com/threads/T-01a19cd4-be31-75b4-bd15-67786eff5983
The fixtures with attributes are not valid PHP 7.4 and a class with new in
attribute arguments cannot be loaded on PHP 8.0, so lint skips the fixtures,
the integration test requires PHP 8.0 and the new fixture lives in its own
directory discovered only on PHP 8.1+. The resolver tests pass PHP 8.0 to the
resolver so that they run everywhere. Expected paths use forward slashes like
the resolver does.

Amp-Thread-ID: https://ampcode.com/threads/T-01a19cd4-be31-75b4-bd15-67786eff5983
@ondrejmirtes
ondrejmirtes merged commit 4d60ec7 into 2.3.x Nov 2, 2026
1034 of 1036 checks passed
@ondrejmirtes
ondrejmirtes deleted the attribute-services-section branch November 2, 2026 19:51
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.

1 participant