Skip to content

perf(container): defer aspect construction with class-name registration - #591

Merged
lisachenko merged 2 commits into
masterfrom
claude/goaop-i3-lazy-aspect-registration
Aug 25, 2026
Merged

perf(container): defer aspect construction with class-name registration#591
lisachenko merged 2 commits into
masterfrom
claude/goaop-i3-lazy-aspect-registration

Conversation

@lisachenko

Copy link
Copy Markdown
Member

What

registerAspect() now also accepts an aspect class-name with an optional factory closure, deferring aspect construction to first use (first advice hit, or aspect enumeration during weaving). Today configureAop() constructs every aspect on every request — plus a ReflectionObject, interface scan and is_readable() stat per aspect in add() — even when the request never triggers a single advice.

// zero-dependency aspect
$container->registerAspect(DoSomethingAspect::class);

// aspect with constructor dependencies — factory required (per review of the design):
$container->registerAspect(LoggingAspect::class, fn() => new LoggingAspect(new NullLogger()));

// instances keep working unchanged (and stay the right choice when construction
// side effects are intentional — see InconsistentlyWeavingAspectKernel)
$container->registerAspect(new LoggingAspect(new NullLogger()));

Details:

  • Deferred aspects ride on the factory map from perf(container): defer service construction with factory map instead of eager lazy proxies #589; getServicesByInterface(Aspect::class) materializes them, so AspectLoader, LazyAdvisorAccessor and the debug commands observe the same aspects as before.
  • Default construction (no factory) is validated at materialization: a required constructor argument raises a clear AspectException telling you to pass a factory. A non-Aspect class is rejected likewise.
  • Debug mode registers the aspect's source file as a tracked resource at registration (reflection only, no instantiation), preserving the cache-freshness semantics that eager construction used to provide. Production skips it — the warm path never checks freshness, and a cache miss materializes all aspects during weaving anyway.
  • Re-entrancy hardening in getServicesByInterface() / materializeService(): autoloading an aspect class inside the materialization loop goes through the weaving pipeline, which re-enters the same enumeration and consumes pending factories — found by a real cold-boot run, covered by testInterfaceQuerySurvivesReentrantMaterialization.

Why (measured)

Fixture project (9 aspects), warm cache, debug=false, opcache file cache, median of 15 (kernel init + 6 woven class autoloads + first intercepted call), PHP 8.5:

master (pre-#589) after #589 this PR
AspectKernel::init() 1.88 ms 1.14 ms 0.78 ms
full request 3.42 ms 2.90 ms 2.63 ms
included files 142 129 123

BC note (4.0)

AspectContainer::registerAspect() signature widens to Aspect|string $aspectOrClassName, ?Closure $aspectFactory = null. Existing instance-based calls compile and behave identically. Custom AspectContainer implementations must update the signature.

Part of the boot-time series

Second of five sequential PRs (container laziness → lazy aspects → lazy transformers → trusted prebuilt cache → cache-state split).

🤖 Generated with Claude Code

https://claude.ai/code/session_01V1Z87HZ2iz23WPTtTYqFxE


Generated by Claude Code

claude added 2 commits August 25, 2026 19:44
registerAspect() now also accepts an aspect class-name with an optional
factory closure. The aspect is constructed on first use (first advice hit,
or aspect enumeration during weaving) instead of on every request during
configureAop(), so a hot-cache request no longer pays construction,
reflection and resource tracking for aspects it never touches.

- Instance registration keeps working unchanged, and remains the right
  choice when construction side effects are part of the design (see
  InconsistentlyWeavingAspectKernel).
- An aspect with required constructor arguments must pass a factory;
  default construction is validated when the aspect materializes.
- In debug mode the aspect source file is registered as a tracked resource
  at registration time (via reflection, no instantiation), preserving
  cache-freshness semantics that eager registration provided.

BC note (4.0): AspectContainer::registerAspect() signature widened to
Aspect|string plus optional factory closure.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V1Z87HZ2iz23WPTtTYqFxE
@lisachenko
lisachenko marked this pull request as ready for review August 25, 2026 19:50
@lisachenko
lisachenko merged commit bbe07e1 into master Aug 25, 2026
7 checks passed
@lisachenko
lisachenko deleted the claude/goaop-i3-lazy-aspect-registration branch August 25, 2026 19:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants