Introduce ExtensionsCollection and #[AutowiredExtensions], replace extension providers (by Claude Fable 5)#6098
Open
ondrejmirtes wants to merge 2 commits into
Open
Introduce ExtensionsCollection and #[AutowiredExtensions], replace extension providers (by Claude Fable 5)#6098ondrejmirtes wants to merge 2 commits into
ondrejmirtes wants to merge 2 commits into
Conversation
ondrejmirtes
force-pushed
the
fable-extensions-collection
branch
from
July 25, 2026 17:57
ce830d3 to
b1cdebd
Compare
…:getExtensionsCollection(), replace extension providers Every interface marked with #[ExtensionInterface] now gets a compiled LazyExtensionsCollection service registered by AutowiredExtensionsExtension. Extensions can be looked up by interface name via Container::getExtensionsCollection(), and injected into constructors with #[AutowiredExtensions(of: SomeExtension::class)] above an ExtensionsCollection<T> parameter. Tests substitute extensions with DirectExtensionsCollection. This replaces the hand-written provider layer whose only job was lazily pulling tagged services out of the container: - ParameterClosureThisExtensionProvider + Lazy - ParameterClosureTypeExtensionProvider + Lazy - ParameterOutTypeExtensionProvider + Lazy - DynamicThrowTypeExtensionProvider + Lazy - ReadWritePropertiesExtensionProvider + Lazy + Direct - AlwaysUsedClassConstantsExtensionProvider + Lazy + Direct - AlwaysUsedMethodExtensionProvider + Lazy + Direct - IgnoreErrorExtensionProvider - DynamicReturnTypeExtensionRegistryProvider + Lazy - ExpressionTypeResolverExtensionRegistryProvider + Lazy - OperatorTypeSpecifyingExtensionRegistryProvider + Lazy - UnaryOperatorTypeSpecifyingExtensionRegistryProvider + Lazy The four *Registry classes stay but are now autowired services holding lazy collections, so they are injected directly. Tag constants moved from the deleted providers onto the extension interfaces themselves. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NPozex9wKR84GgzFQ6Vx5o
… to extension collections Services whose Container dependency existed only to pull a tagged extension list now inject an ExtensionsCollection via #[AutowiredExtensions]: DeprecationProvider (seven deprecation extension collections), the ten RestrictedUsage rules, InstantiationRule, ClassNameCheck, ClassForbiddenNameCheck, ConstructorsHelper, DefaultStubFilesProvider, LazyRegistry, Collectors\RegistryFactory, ResultCacheManager and RichParser. Callers that keep the Container for other reasons (TypeSpecifierFactory, PromoteParameterRule, ExprHandlerRegistry, AnalyseCommand, DiagnoseCommand, and the kept ClassReflection/TypeNodeResolver registry providers) switch from getServicesByTag() to the typed Container::getExtensions(). AdditionalConstructorsExtension, ConstantDeprecationExtension and ForbiddenClassNameExtension now carry #[ExtensionInterface] - they declared service tags but were missing from the interface-tag mapping, so their tagged services were never validated. AutowiredExtensionsExtension now skips definitions whose creator is not the class itself - a service aliasing another (factory: @otherService), like TestCase.neon's currentPhpVersionSimpleParser, resolves getType() to the aliased class and must not receive constructor arguments. The only remaining getServicesByTag() caller is StubValidator, whose stub-validation rule tag is a second tag on Rule services and cannot appear in the one-to-one interface-tag mapping. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NPozex9wKR84GgzFQ6Vx5o
ondrejmirtes
force-pushed
the
fable-extensions-collection
branch
from
July 25, 2026 18:27
b1cdebd to
0ac067c
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Note
This PR was created by Claude Fable 5 in a Claude Code session driven by @ondrejmirtes.
Extension lookup by interface is now a first-class DI concept, replacing the hand-written provider layer.
What's new
Container::getExtensions(SomeExtension::class)andgetExtensionsCollection()— grab all registered extensions by an interface marked with#[ExtensionInterface]. Baked into the container at compile time:AutowiredExtensionsExtensionregisters oneLazyExtensionsCollectionservice per extension interface (prefixed service name derived from the FQCN), with the tag as a compile-time argument. No runtime attribute scanning; unknown interfaces throw a descriptiveMissingServiceException.#[AutowiredExtensions(of: SomeExtension::class)]above a constructor parameter typedExtensionsCollection<T>injects the lazy collection. Wired inbeforeCompileafter type resolution, so it works for both attribute-registered and neon-registered services. The pass only touches definitions whose creator is the class itself — aliases (factory: @otherService) and method factories are skipped (covered by a regression test; TestCase.neon'scurrentPhpVersionSimpleParseralias found this the hard way).ExtensionsCollection<T>withgetAll(): list<T>, implemented byLazyExtensionsCollection(container + tag, memoized) andDirectExtensionsCollection(plain array, for tests).Deleted (all non-
@api)Lazy/Directvariants:ParameterClosureThisExtensionProvider,ParameterClosureTypeExtensionProvider,ParameterOutTypeExtensionProvider,DynamicThrowTypeExtensionProvider,ReadWritePropertiesExtensionProvider,AlwaysUsedClassConstantsExtensionProvider,AlwaysUsedMethodExtensionProvider,IgnoreErrorExtensionProviderDynamicReturnTypeExtensionRegistryProvider,ExpressionTypeResolverExtensionRegistryProvider,OperatorTypeSpecifyingExtensionRegistryProvider,UnaryOperatorTypeSpecifyingExtensionRegistryProvider— the registries survive as autowired services holding lazy collections and are injected directlyEXTENSION_TAG, referenced by their own#[ExtensionInterface]attribute); tag string values unchangedSecond commit: DeprecationProvider + every inline getServicesByTag() caller
DeprecationProviderinjects seven lazy collections instead of the containerInstantiationRule,ClassNameCheck,ClassForbiddenNameCheck,ConstructorsHelper,DefaultStubFilesProvider,LazyRegistry,Collectors\RegistryFactory,ResultCacheManager,RichParsergetExtensions():TypeSpecifierFactory,PromoteParameterRule,ExprHandlerRegistry,AnalyseCommand/DiagnoseCommand, and the kept ClassReflection/TypeNodeResolver registry providersAdditionalConstructorsExtension,ConstantDeprecationExtension,ForbiddenClassNameExtensionnow carry#[ExtensionInterface]— they declared tags but were missing from the interface-tag mapping, so their tagged services were never validatedgetServicesByTag()caller isStubValidator(its stub-validation rule tag is a second tag onRuleservices; the one-to-one interface-tag mapping can't cover it)Not converted (follow-ups)
ClassReflectionExtensionRegistryProvider(~30 call sites inClassReflectionplus the deliberate drop-the-container memory optimization) andTypeNodeResolverExtensionRegistryProvider(setTypeNodeResolver()back-injection cycle) keep their provider shape; only their tag pulls were typed.Notes
composer-attribute-collectoronly scanssrc/, so#[AutowiredExtensions]is for PHPStan's own services; third-party extensions keep registering via tags, which are unchanged.ReflectionProviderProvideras the canonical non-@apiinterface specimen (wasDynamicThrowTypeExtensionProvider).make phpstan,make cs,make lintall green.🤖 Generated with Claude Code
https://claude.ai/code/session_01NPozex9wKR84GgzFQ6Vx5o