Skip to content

feat(core): take aspects from the application's PSR-11 container - #769

Draft
lisachenko wants to merge 1 commit into
masterfrom
feat/container-delegate
Draft

lisachenko wants to merge 1 commit into
masterfrom
feat/container-delegate

Conversation

@lisachenko

Copy link
Copy Markdown
Member

What and why

Follow-up to #768: aspects (and their dependencies) can now come from the application's own PSR-11 container (Symfony, Laravel, PHP-DI, ...).

The AOP kernel is initialized before the application builds its container, so registration and lookup are split:

// configureAop(): the aspect is known by class name, its instance comes from the app container
$container->addDelegatedService(MonitorAspect::class);            // or ..., 'app.monitor_aspect'

// later, once the application container exists
ApplicationAspectKernel::getInstance()->getContainer()->setDelegate($psrContainer);
  • AspectContainer::setDelegate(ContainerInterface $delegate): get()/has() fall back to the delegate on a miss only (the woven-code path The::advice() / The::aspect() always hits local entries, so it's unchanged). Delegated values are not stored locally; the delegate owns their lifecycle. A re-entrancy guard stops a composite delegate that contains this container (the PSR-11 delegate-lookup pattern) from recursing forever. Self-delegation is rejected.
  • AspectContainer::addDelegatedService(class-string $id, ?string $delegateId = null) is addLazyService() whose factory fetches from the delegate. The service is tagged and collected as usual, but materializes as a native lazy proxy, so weaving, compiled advisor caches and bin/aspect cache:warmup:aop never need the delegate. The instance is fetched on the first advice call and must be an instance of $id.
  • Errors: used before setDelegate(), or the delegate returns the wrong type → ContainerException with a message saying what to do.
  • Known limits (documented):
    • Property-less aspects: PHP can't make a lazy object of a class without instance properties, so such an aspect is built when aspects are collected and fails without a delegate. It has nothing to inject anyway; the error message says to use add()/addLazyService() instead.
    • No discovery: aspects are never discovered in the delegate, because PSR-11 can't enumerate services.
    • Identity: the aspect container hands out a lazy proxy that forwards to the app's instance. State is shared, === is not.

Docs: new "Aspects from the application container" section in docs/configuration.md, plus README, CHANGELOG, UPGRADE-4.0.md (custom containerClass implementations add the two methods) and src/Core/AGENTS.md.

Transformer wiring is untouched (no AspectKernel/FrameworkServices/transformer changes), so the transformation performance group isn't affected.

Checklist

  • The title follows Conventional Commits (type(scope): summary)
  • Tests cover the change. ContainerTest: fallback, local entries win, self-delegation rejected, composite delegate doesn't loop (verified it recurses without the guard), delegated service built only on first use after the aspects were collected, custom delegate id, use before setDelegate(), wrong type, property-less class. Functional DelegatedServiceTest / ProductionDelegatedServiceTest: the fixture kernel registers GreetingAspect via addDelegatedService(), the cache is warmed up without any app container, and bin/delegated-aspect.php builds a small PSR-11 ApplicationContainer, attaches it and runs the woven Greeter ("Hello, World!"), in debug and production mode.
  • composer check passes (cs, phpstan level 10, phpunit 3128 tests)
  • User-facing or breaking changes are in CHANGELOG.md (and the upgrade guide for breaking changes)

Generated by Claude Code

AspectContainer::setDelegate() attaches the PSR-11 container of the
application once it exists: get()/has() fall back to it for ids that are
not registered here, guarded against composite containers that ask back.
addDelegatedService() registers a lazy service whose instance is fetched
from the delegate on first use, so aspects with dependencies are declared
in configureAop() while weaving and cache warm-up never need the delegate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JzyTe7njn5wEgo3sGxfjYR
@codecov

codecov Bot commented Oct 17, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.83%. Comparing base (93f645a) to head (88972ba).

Additional details and impacted files
@@             Coverage Diff              @@
##             master     #769      +/-   ##
============================================
+ Coverage     97.82%   97.83%   +0.01%     
- Complexity     1847     1858      +11     
============================================
  Files           105      105              
  Lines          4867     4898      +31     
============================================
+ Hits           4761     4792      +31     
  Misses          106      106              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

This branch has not been deployed

No deployments
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