Introduce ExtensionsCollection and #[AutowiredExtensions], replace extension providers (by Claude Opus 5)#6099
Open
ondrejmirtes wants to merge 4 commits into
Open
Introduce ExtensionsCollection and #[AutowiredExtensions], replace extension providers (by Claude Opus 5)#6099ondrejmirtes wants to merge 4 commits into
ondrejmirtes wants to merge 4 commits into
Conversation
Container::getExtensions() looks up extensions by their interface instead of by a service tag. The #[ExtensionInterface] mapping is baked into the compiled container through the new ExtensionInterfaceTags service, so vendor/attributes.php is not needed at runtime. Services ask for extensions with #[AutowiredExtensions] above a constructor parameter typed as ExtensionsCollection. AutowiredAttributeServicesExtension wires those parameters in beforeCompile() - services from NEON files are only in the builder at that point - with an inline LazyExtensionsCollection, which keeps the resolution deferred the way the Lazy*ExtensionProvider classes did. LazyExtensionsCollection releases its Container reference once the extensions are resolved so that long-lived holders don't retain the whole container. IgnoreErrorExtensionProvider is the first one replaced by this. ForbiddenClassNameExtension, AdditionalConstructorsExtension and ConstantDeprecationExtension were missing #[ExtensionInterface] and get it here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XLPJxFJDzLPsieqEwJKpkn
The Direct*/Lazy* provider pairs existed only to defer getServicesByTag() past container construction and to let tests hand extensions in directly - both of which ExtensionsCollection now does. Deleted: - ReadWritePropertiesExtensionProvider - AlwaysUsedClassConstantsExtensionProvider - AlwaysUsedMethodExtensionProvider - ParameterClosureThisExtensionProvider - ParameterClosureTypeExtensionProvider - ParameterOutTypeExtensionProvider - DynamicThrowTypeExtensionProvider The service tag constants they carried move onto the extension interfaces themselves, next to the #[ExtensionInterface] attribute that references them. Providers with several getters (three per parameter/throw-type family) become one ExtensionsCollection parameter each, so consumers no longer go through a lookup method to reach a single list. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XLPJxFJDzLPsieqEwJKpkn
Rules, checks and factories that held a Container only to call getServicesByTag() now declare what they need with #[AutowiredExtensions], which also drops their per-class memo fields and the tag constants from their bodies. Where the Container is still needed for other reasons (TypeSpecifierFactory, PromoteParameterRule, ExprHandlerRegistry's static dispatch, the diagnose commands) the tag lookup becomes the typed Container::getExtensions(). #[AutowiredExtensions] arguments are only set on definitions that instantiate the class themselves - an alias like TestCase.neon's currentPhpVersionSimpleParser delegates to another service's creator, where the extra argument would become an unknown named parameter. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XLPJxFJDzLPsieqEwJKpkn
ExpressionTypeResolverExtensionRegistry, OperatorTypeSpecifyingExtensionRegistry, UnaryOperatorTypeSpecifyingExtensionRegistry and DynamicReturnTypeExtensionRegistry take ExtensionsCollection instead of plain arrays, which makes them cheap enough to be #[AutowiredService]s that consumers depend on directly. Their *RegistryProvider interfaces and Lazy* implementations are gone. Two providers stay: TypeNodeResolverExtensionRegistryProvider and ClassReflectionExtensionRegistryProvider both break a real cycle - their registries need TypeNodeResolver and PhpClassReflectionExtension, which reach back to the registry through ReflectionProvider/ClassReflection. Their tag lookups still switch to Container::getExtensions(). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XLPJxFJDzLPsieqEwJKpkn
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.
Built by Claude Opus 5. This is an alternative implementation of the same feature as #6098 (built by Claude Fable 5) — see Differences from #6098 at the bottom before picking one.
Since #6091 every extension interface declares its own service tag via
#[ExtensionInterface(tag: ...)], so the completeinterface => tagmap is derivable. That makes a whole family of classes redundant: whenever a service needed "all extensions of type X" it either took aContainerand calledgetServicesByTag(SomeExtension::EXTENSION_TAG)(~25 call sites), or went through a hand-written*ExtensionProviderwhose only jobs were deferring the lookup so the container doesn't hit a circular reference, and offering aDirect*twin for tests.The mechanism
Container::getExtensions()looks extensions up by interface:The
#[ExtensionInterface]map is baked into the compiled container through a smallExtensionInterfaceTagsservice, sovendor/attributes.phpis not needed at runtime.MemoizingContainermemoizes per interface.Services declare what they need on the constructor parameter:
AutowiredAttributeServicesExtension::beforeCompile()wires those parameters. It has to bebeforeCompile()and notloadConfiguration(): Nette registers NEONservices:after every other extension'sloadConfiguration(), so NEON-registered consumers (RichParserinconf/parsers.neon) are not in the builder yet at that point.ContainerBuilder::resolve()runs beforebeforeCompile()and argument autowiring runs after, so setting arguments there is safe.No extra service definitions are registered. The argument becomes an inline
Statement, which Nette renders at the use site:LazyExtensionsCollectionresolves on the firstgetAll()— so the deferral theLazy*providers existed for is preserved — and then nulls itsContainerreference, the trickLazyClassReflectionExtensionRegistryProviderdoes by hand today. Long-lived holders likeClassPropertiesNodetherefore don't become transitive handles on the whole DI container, which means "holds the container" is no longer an argument against replacing a provider with a collection.DirectExtensionsCollectionis the test seam thatDirect*ExtensionProviderused to be.RuleTestCase::getReadWritePropertiesExtensions()keeps its array signature; only the wrapping changed.Passing an interface without
#[ExtensionInterface]throwsNotAnExtensionInterfaceExceptionduring container compilation.Deleted
All eight extension-list providers with their
Direct/Lazytwins:ReadWritePropertiesExtensionProvider,AlwaysUsedClassConstantsExtensionProvider,AlwaysUsedMethodExtensionProvider,IgnoreErrorExtensionProvider,ParameterClosureThisExtensionProvider,ParameterClosureTypeExtensionProvider,ParameterOutTypeExtensionProvider,DynamicThrowTypeExtensionProvider…and four registry providers, whose registries became
#[AutowiredService]s holding collections:ExpressionTypeResolverExtensionRegistryProvider,OperatorTypeSpecifyingExtensionRegistryProvider,UnaryOperatorTypeSpecifyingExtensionRegistryProvider,DynamicReturnTypeExtensionRegistryProvider26 files gone, 6 added. The service tag constants they carried moved onto the extension interfaces themselves, next to the
#[ExtensionInterface]attribute that references them.The ~25 inline
getServicesByTag()call sites are migrated too — the 10RestrictedUsagerules,ClassNameCheck,ClassForbiddenNameCheck,InstantiationRule,ConstructorsHelper,DeprecationProvider(7 collections),DefaultStubFilesProvider,LazyRegistry,Collectors\RegistryFactory,ResultCacheManager,RichParserall drop theirContainerand their memo fields. Where theContaineris still needed for other reasons (TypeSpecifierFactory,PromoteParameterRule,ExprHandlerRegistry's static dispatch, the diagnose commands) the tag lookup becomes the typedContainer::getExtensions().ForbiddenClassNameExtension,AdditionalConstructorsExtensionandConstantDeprecationExtensionwere missing#[ExtensionInterface]and get it here.Kept, on purpose
TypeNodeResolverExtensionRegistryProviderandClassReflectionExtensionRegistryProviderboth break a real cycle — their registries needTypeNodeResolverandPhpClassReflectionExtension, which reach back to the registry throughReflectionProvider/ClassReflection. Only their tag pulls are typed now.The single remaining
getServicesByTag()caller isStubValidator:phpstan.stubValidator.ruleis a second tag onRule, which a 1:1 interface→tag map cannot express.Notable trap
#[AutowiredExtensions]arguments are only set on definitions that instantiate the class themselves.src/Testing/TestCase.neonhaswhose resolved type is
RichParser, so a naive type match sets the argument on the alias too and Nette generates$this->createServiceCurrentPhpVersionRichParser(visitors: ...)— "Unknown named parameter $visitors" at runtime, in every test. The guard is that the creator entity has to be the class itself.Differences from #6098
Both branches were built independently from
a7dfaa7a50and reached the same scope, the same alias trap, the same three missing#[ExtensionInterface]attributes and the same two kept providers. Where they differ:AutowiredAttributeServicesExtension::beforeCompile()and emits an inlineStatement. Introduce ExtensionsCollection and #[AutowiredExtensions], replace extension providers (by Claude Fable 5) #6098 adds a dedicatedAutowiredExtensionsExtensionthat registers one non-autowiredphpstan.extensionsCollection.<FQCN>service per extension interface.interface:, notof:.Containergains onlygetExtensions(); Introduce ExtensionsCollection and #[AutowiredExtensions], replace extension providers (by Claude Fable 5) #6098 also addsgetExtensionsCollection().LazyExtensionsCollectionreleases itsContainerafter the firstgetAll().Verification
make phpstan,make cs,make tests(17678 tests) andmake tests-integrationare green. Newtests/PHPStan/DependencyInjection/ExtensionsCollectionTest.phpcoversgetExtensions()against the equivalentgetServicesByTag()result, the unknown-interface error, the container release, and that a real#[AutowiredExtensions]consumer resolves from the container.🤖 Generated with Claude Code
https://claude.ai/code/session_01XLPJxFJDzLPsieqEwJKpkn