Skip to content

Let extensions and projects register services through DI attributes in attributeServicesDirectories - #6283

Open
ondrejmirtes wants to merge 6 commits into
2.2.xfrom
attribute-services-directories
Open

Let extensions and projects register services through DI attributes in attributeServicesDirectories#6283
ondrejmirtes wants to merge 6 commits into
2.2.xfrom
attribute-services-directories

Conversation

@ondrejmirtes

Copy link
Copy Markdown
Member

Implements the design from the issue discussion (following the constraints laid out there, as an implementation to compare with #6269):

attributeServicesDirectories:
	- src
  • New top-level section (not a parameter), handled by a schema-only container extension; entries are absolutized relative to the declaring config file by NeonAdapter — also in ContainerFactory's pre-pass, which turned out to need no second config pass (NeonCachedFileReader caches raw decodes only).
  • Discovery via Composer's static autoload data + native runtime reflection — the PSR-4 path contract and vendor/composer/autoload_classmap.php; no file is ever parsed. A cheap content prefilter ('#[' + case-insensitive phpstan, 100% recall) keeps classes without PHPStan attributes from being autoloaded. A file that suggests a DI attribute but yields no autoloadable class is an explicit error, not a fallback.
  • Public allowlist: AutowiredService, NonAutowiredService, RegisteredRule, RegisteredCollector, GenerateFactory, and AutowiredParameter on constructor parameters — now @api, enforced by the new ApiAttributeRule (fires for every attribute position, reports non-@api PHPStan attributes outside PHPStan code). #[ContainerExtension], #[ExtensionInterface], #[AutowiredExtensions] and the other internal attributes are explicit discovery errors, so getInterfaceTagMapping() stays process-static.
  • Validation against Composer metadata: each directory must be covered by a psr-4 or classmap rule of the owning package (installed.php install paths) — vendor packages count only autoload (Composer never installs a dependency's autoload-dev), the root project also counts autoload-dev unless Composer ran with --no-dev (dedicated error); psr-0/files-only coverage errors too. On PHP < 8.0 a non-empty section is a clean configuration error.
  • Container cache key: directories of regularly installed vendor packages contribute their package's version@reference token — a warm run reads nothing under them; project-own directories and path repositories contribute per-file content hashes (same mechanism as config files). A changed package owning a configured directory invalidates the whole result cache (its rules can affect every file). Editing a discovered class rebuilds the container.
  • Reusable registration seam: AttributeServicesRegistrar + AttributeTargetsProvider serve both vendor/attributes.php and the discovered targets; discovery is lazy, so a container-cache hit never autoloads a third-party class. The stub-validator derivative container re-runs discovery through ContainerFactory::create() (regression-tested).
  • neon2attributes command: deterministically converts services:/rules: entries into the attributes, declares the edited directories in the section, reports everything it keeps in NEON with the reason (setup, literal arguments, underivable or partial tags, multi-registration, vendor classes, unusual constructor layouts), and verifies itself: it compiles the original and the converted configuration in fresh subprocesses and requires an identical tagged-services fingerprint (tag → classes → multiplicities), rolling everything back otherwise.

Real-world check on calebdw/phpstan-laravel: neon2attributes extension.neon converts 133 entries and keeps 22 with reasons — the plan matches the hand-made conversion in calebdw/phpstan-laravel#10 exactly (118 #[AutowiredService] + 14 autoTag: false + 4 rules; 3 later kept because their constructors put parameters on one line) — and the fingerprint verification confirms identical tagged services.

Compatibility note for extension authors: a migrated extension can only require PHPStan versions that know the section — older PHPStan fails with Found section 'attributeServicesDirectories'.

Closes phpstan/phpstan#15114

🤖 Generated with Claude Code

https://claude.ai/code/session_01Vw4RxhEifXdnTPw57hG3Vw

ondrejmirtes and others added 6 commits August 26, 2026 23:34
…ttribute usage

AutowiredService, NonAutowiredService, RegisteredRule, RegisteredCollector,
GenerateFactory and AutowiredParameter are the attributes third parties will
be able to use through attributeServicesDirectories. The new ApiAttributeRule
listens on every attribute position and reports non-@api PHPStan attributes
used outside PHPStan code.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vw4RxhEifXdnTPw57hG3Vw
…ces seam

AttributeServicesRegistrar owns the target-to-definition logic and
AttributeTargetsProvider is the single merged view over vendor/attributes.php
and (soon) targets discovered in attributeServicesDirectories. No behavior
change; DiscoveredAttributeTargets is empty until discovery lands.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vw4RxhEifXdnTPw57hG3Vw
…ner-cache keying

The top-level section lists directories whose classes may carry PHPStan's DI
attributes. Entries are absolutized relative to the declaring config file by
NeonAdapter (also in ContainerFactory's pre-pass, so the resolver sees final
paths before the container compiles). The resolver validates each directory
against the owning Composer package's (or the root project's) autoload rules,
gates the feature to PHP 8.0+, and computes the directory's contribution to
the container cache key: a package version token for regularly installed
vendor packages, per-file content hashes for project-own directories and
path repositories.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vw4RxhEifXdnTPw57hG3Vw
…utoload data

AttributeServicesDiscoverer enumerates candidate classes from the PSR-4 path
contract and vendor/composer/autoload_classmap.php - no file is parsed - and
reads the allowlisted attributes (AutowiredService, NonAutowiredService,
RegisteredRule, RegisteredCollector, GenerateFactory, AutowiredParameter on
constructor parameters) with native reflection. A cheap content prefilter
keeps classes without PHPStan attributes from being autoloaded. Files that
suggest a DI attribute but yield no autoloadable class, and PHPStan-internal
attributes (#[ContainerExtension], #[ExtensionInterface], #[AutowiredExtensions],
...), are explicit errors. ContainerFactory seeds the discovery context before
each container build; AttributeTargetsProvider merges the discovered targets
with vendor/attributes.php lazily, so a container-cache hit loads nothing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vw4RxhEifXdnTPw57hG3Vw
…services changes

A directory listed in attributeServicesDirectories registers services straight
from its classes, so a composer update of the owning package can affect the
analysis of every file - the file-granular package re-seed is not enough.
PackageDependencyResolver::resolveDirectoryPackage() also matches a directory
that is a package's install path. The derivative (stub validator) container
re-runs discovery through ContainerFactory::create(), covered by a regression
test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vw4RxhEifXdnTPw57hG3Vw
…to attributes

Deterministically converts services:/rules: entries into #[AutowiredService],
#[NonAutowiredService], #[RegisteredRule(level: 0)], #[RegisteredCollector] and
#[AutowiredParameter] on the classes, declares the edited directories in
attributeServicesDirectories, and leaves everything it cannot express (setup,
literal arguments, underivable tags, multi-registration, vendor classes) in
place with the reason reported. PHP edits are position-guided line insertions
(php-parser locates, plain text edits), NEON edits are order-mapped line
surgery that aborts on any layout it cannot map. Before keeping the changes
the command compiles the original and the converted configuration in fresh
subprocesses (the current process has the classes loaded without the new
attributes) and requires an identical tagged-services fingerprint, rolling
everything back otherwise.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vw4RxhEifXdnTPw57hG3Vw
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.

Allow third-party extensions to use the DI attributes

1 participant