Repository navigation
feat(core)!: weave without creating aspects - #776
Conversation
The weaver collected aspects as objects: AspectLoader asked the container for every Aspect service, and AttributeAspectLoaderExtension bound each advice method to that instance. A deferred aspect stayed an uninitialized lazy proxy, but PHP can not make a class without instance properties lazy, so the factory of such an aspect ran as soon as weaving started, before an application container attached with setDelegate() existed. Integration bridges had to create it behind the back of the application container or fail (goaop/goaop-yii3-bridge#3). Aspects are now loaded by class. AspectContainer::getServiceIdsByInterface() lists the aspect ids without creating the services, loader extensions get the ReflectionClass only, and advices are built with the lazy form of the Interceptor facade. Its proxies carry the aspect class, the advice method, the order and the expression, which ordering, GeneratedInterceptor and the advisor cache compiler read without initializing them. Every aspect is created on the first call of one of its advices. BREAKING CHANGE: AspectLoaderExtension::load() takes the ReflectionClass of the aspect only, AbstractAspectLoaderExtension::parsePointcut() the aspect class name, AspectLoaderInterface works with aspect class names, and AspectContainer implementations must provide getServiceIdsByInterface(). Aspects must be registered under their class name. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FgdBdi3AXZ33wvVCFG3GPe
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #776 +/- ##
============================================
+ Coverage 98.20% 98.22% +0.02%
- Complexity 1913 1924 +11
============================================
Files 113 113
Lines 5000 5024 +24
============================================
+ Hits 4910 4935 +25
+ Misses 90 89 -1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
…oader Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FgdBdi3AXZ33wvVCFG3GPe
|
what is performance impact? of booting/weaving? |
|
Measured both, master 1. Aspect loading (the part this PR changes): a script that registers N generated aspects with
It's about 60 µs per aspect either way, dominated by pointcut parsing. The lazy interceptor plus its 4 raw property writes cost about the same as materializing the aspect and binding a closure did. The test aspects have no properties, so master ran their factories here too; with real aspects that have heavy constructors, master paid that cost during weaving and the branch doesn't. 2. End to end: the goaop-yii3-bridge boot benchmarks (
The hot path can't change by design: a warm request never runs the aspect loader, and generated proxies keep the same eager Generated by Claude Code |
…thod] The interceptor constructor takes the advice closure or the pair of aspect class and advice method, and keeps the pair in one aspectMethod property. NativeLazyProxy::create() sets the properties the lazy facade exposes, so the facade no longer handles reflection properties. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FgdBdi3AXZ33wvVCFG3GPe
getUnloadedAspects() returns one ReflectionClass per aspect, and load() and loadAndRegister() take it, so the loaders no longer reflect the same aspect again. The methods of AspectLoaderInterface are marked internal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FgdBdi3AXZ33wvVCFG3GPe
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FgdBdi3AXZ33wvVCFG3GPe
…Advice() Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FgdBdi3AXZ33wvVCFG3GPe
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FgdBdi3AXZ33wvVCFG3GPe
… exists goaop/framework#776 weaves without creating aspects, so an aspect without instance properties is no longer needed while the validating container is built: it is the instance of the Yii container in debug mode too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FgdBdi3AXZ33wvVCFG3GPe
What and why
Weaving used to collect aspects as objects.
AspectLoaderasked the container for everyAspectservice, andAttributeAspectLoaderExtensionbound each advice method to that instance (getClosure($aspect)). A deferred aspect stayed an uninitialized lazy proxy, but PHP can't make a class without instance properties lazy. The factory of such an aspect therefore ran as soon as weaving started, before an application container attached withsetDelegate()existed. Integration bridges then had to either create the aspect behind the application container's back or fail. The Yii3 bridge hit this in debug mode, where the container loads classes while it's being built: goaop/goaop-yii3-bridge#3.Aspects are now loaded by class, so weaving, warm-up and the advisor cache never create one:
AspectContainer::getServiceIdsByInterface()(new) lists service ids without creating the services.AspectLoaderInterface(all methods@internal):getUnloadedAspects()returns oneReflectionClassper unloaded aspect, andload()/loadAndRegister()take it, so the loaders and extensions share one reflection.AspectLoaderExtension::load()takes theReflectionClassof the aspect only, and so doAbstractAspectLoaderExtension::parsePointcut()andAttributeAspectLoaderExtension::getAdvice().Closure|array{class-string<Aspect>, string}. Given the pair, it resolves the aspect itself and keeps the pair inaspectMethod.AttributeAspectLoaderExtensionbuilds advices with the lazy form of theInterceptorfacade, the same form compiled advisor caches already use.NativeLazyProxy::create()takes the properties to set on the proxy without initializing it. The lazy interceptors exposeadviceOrder,pointcutExpressionandaspectMethod, so ordering,GeneratedInterceptor::fromAdvice()andcompileToPhp()read them without creating the aspect.debug:aspectreads aspects from their classes too.Every aspect is now created on the first call of one of its advices (
The::aspect()), including aspects without instance properties. Generated code and advised calls are unchanged: proxies keep the eagerThe::aspect(X::class)->method(...)form.Breaking changes (4.0 is unreleased; recorded in CHANGELOG.md, not in the upgrade guide): the extension signatures above, a new
AspectContainermethod for customcontainerClassimplementations, and aspects must be registered under their class name (generated code already resolves them that way).Testing:
ContainerTest::testServiceIdsByInterfaceNeverCreateServices;InterceptorTest::testLazyInterceptorDescribesItsAdviceWithoutCreatingTheAspect;AttributeAspectLoaderExtensionTest::testLoadsAdvicesWithoutCreatingTheAspect, with an aspect whose constructor throws;AspectLoaderTest::testDeferredAspectsAreLoadedWithoutCreatingThem;AdvisorIndexTest::testLoadsDeferredAspectsWithoutRunningTheirFactories, end to end with a property-less aspect whose factory throws;NativeLazyProxyTest::testTrustedCreateSetsRawValuesWithoutInitializing, covering an inheritedprivate(set)property.vendor/bin/phpunit: 3253 tests OK (PHP 8.5.11 withopcache.jit=disable, see the JIT note intests/AGENTS.md).composer analyzeandcomposer csare clean.Follow-up: once this lands, the Yii3 bridge drops its "stateless aspect needed before the container is attached" error (goaop/goaop-yii3-bridge#4).
Checklist
type(scope): summary)composer checkpassesGenerated by Claude Code