Skip to content

Register services from DI attributes found in configured directories - #6269

Open
calebdw wants to merge 1 commit into
phpstan:2.2.xfrom
calebdw:calebdw/push-uyvxnxupmuxp
Open

Register services from DI attributes found in configured directories#6269
calebdw wants to merge 1 commit into
phpstan:2.2.xfrom
calebdw:calebdw/push-uyvxnxupmuxp

Conversation

@calebdw

@calebdw calebdw commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Hello!

I really love the DI attributes and how much boilerplate they removed from the neon files! Unfortunately this mechanism was only usable in this repo so it couldn't be used to say clean up the Larastan extension.neon---until now

This adds a new autowiredServiceDirectories parameter that extensions can configure like so:

parameters:
  autowiredServiceDirectories: [src]

and then all of their classes will be automatically picked up by phpstan when running. The files are hashed by their contents, so if they change then the directory is re-walked

Additionally, this also allows conditionally registering rules with #[RegisteredRule(level: 4, enabledBy: '%featureToggles.finiteTypesInHaystack%')] and removes a ton of boilerplate from the conditionalTags config

Here's an example in a third-party extension of how much it cleans up: calebdw/phpstan-laravel#10

Closes phpstan/phpstan#15114

Thanks!

@AJenbo

AJenbo commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

this would indeed take away some of the very annoying parts of setting up extensions, I would love to apply this to Bladestan aswell.

@ondrejmirtes

ondrejmirtes commented Aug 26, 2026

Copy link
Copy Markdown
Member

Hi, I'd love this (I love attributes and have been using them more and more ever since May 2025 where I found out I can use composer-attribute-collector to even support PHP 7.4) but I'd like to do this right.

I think this is an ideal candidate to write out a spec in an issue by discussing together and coming up with all the concerns one might have with a feature like this. Then this while issue discussion can be fed like a prompt to a coding agent in our CI which will implement it.

There's a lot of open questions about this feature and we need to discuss them first.

Can you please start by opening an issue and describe what you'd like this feature to do, I'll comment on it, we'll brainstorm it and once we're happy with it, I'll fire off the agent and it will implement the code.

Also this is yet another 1,000+ LoC PR from you, and we have one (you know which one ;)) which hasn't been merged yet so I'd like to avoid your frustration with multiple unmerged PR (I know how it feels).

Thanks!

PHPStan's dependency injection attributes were only usable from
phpstan-src itself. They are collected at composer install time into
vendor/attributes.php, which only ever covers phpstan-src's own src
directory, and inside the PHAR the collector lives under a
build-specific namespace prefix. Extensions shipped as separate
packages therefore had no way in, and had to list every rule, type
extension and collector by hand in a services: section.

Add an autowiredServiceDirectories parameter. Each configuration file
contributes its own directories, and because the parameter is a list
the entries merge rather than overwrite, so one extension adding to it
cannot hide another's classes. AutowiredServiceDiscoverer scans those
directories, using the bundled parser only as a filter for classes
carrying a known attribute, then reflects on the survivors to build the
same TargetClass and TargetMethodParameter objects the collector
produces. The compiler extensions merge both sources, so discovered
classes go through the existing registration paths unchanged, including
the auto-tagging derived from #[ExtensionInterface].

The scanned files feed the container cache key - otherwise editing a
discovered rule would keep being served from the cached container.
Parsing is deferred to the first attribute lookup, so a container that
never asks does no work beyond listing the files, and the pre-filter
matches the namespace the attributes live in rather than their names,
which is both a single needle per file and immune to grouped or
aliased use statements.

getInterfaceTagMapping() now takes the ContainerBuilder, since the
mapping depends on the configured directories and two containers in
one process can disagree about them. Hoist its result out of the loop
over autowired services in AutowiredAttributeServicesExtension: it used
to be a free static memo, and calling it per service would otherwise
re-derive the discoverer several hundred times per compile.

#[ContainerExtension] is rejected with an explicit message. Nette
snapshots its extension list before any of this runs, so compiler
extensions still need an extensions: section.

Mark the attributes extensions are now expected to reach for as @api.
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

3 participants