Skip to content

[Feature] AOP-unaware container: aspects as typical services, engine-probed lazy proxies - #658

Merged
lisachenko merged 3 commits into
masterfrom
claude/container-aop-refactor-0d706r
Sep 3, 2026
Merged

[Feature] AOP-unaware container: aspects as typical services, engine-probed lazy proxies#658
lisachenko merged 3 commits into
masterfrom
claude/container-aop-refactor-0d706r

Conversation

@lisachenko

Copy link
Copy Markdown
Member

Summary

Makes Go\Core\Container a genuinely generic, AOP-unaware DI container and removes the hot-path overhead flagged in review: the per-class isLazyProxyCompatible() reflection walk on the service read path, and validateAspectRegistration() running twice (plus a partial third time in debug) per aspect initialization.

Aspects are typical services

registerAspect(), materializeAspect(), validateAspectRegistration(), the $factoryValidators map and isDebug() are removed from the container and its interface. Aspects register through the existing generic API from configureAop():

$container->add(MonitorAspect::class, new MonitorAspect());                       // eager
$container->addLazyService(LoggingAspect::class, static fn() => new LoggingAspect($logger)); // deferred

Aspect enumeration on the weaving path is unchanged — getServicesByInterface(Aspect::class) already finds deferred aspects by id. Since a factory closure is now always present, the old "default-constructible without factory" validation is moot; a factory returning the wrong type is caught once by the container's instanceof guard on first use.

Interface-keyed registration listeners (onRegistration)

The one thing that genuinely could not wait for materialization — debug-mode tracking of aspect source files for cache freshness — becomes a generic container feature: onRegistration(Aspect::class, $listener) fires with the id (never the value, so laziness is fully preserved) whenever a matching deferred service is registered. The kernel arms a single listener in debug mode only; in production no listener exists and addLazyService() is a pure array write with zero autoloading. addResource() joins the interface as public for this.

Engine-probed lazy proxies (Go\Core\NativeLazyProxy)

The 30-line isLazyProxyCompatible() predicate (property enumeration + parent-chain reflection walk, run on first touch of every service, every request) is deleted. The engine already knows every incompatibility: newLazyProxy() throws for classes PHP cannot make lazy (memoized for worker runtimes), and one isUninitializedLazyObject() call detects property-less classes whose initializer would never run. In the common compatible case the whole check is one engine-level boolean. Interceptor::createLazily() now uses the same helper via its trusted create() path, unifying the two previously diverged lazy-proxy sites. The probe also retires the stale readonly-before-8.5 version gate — current PHP 8.4.x already supports readonly lazy proxies.

Also

  • Framework service wiring moved out of the container constructor into FrameworkServices::register(), called by the kernel during init()new Container() registers nothing.
  • getService() delegates to getValue() + instanceof guard instead of duplicating the materialize-or-throw block.
  • Container throws SPL exceptions (InvalidArgumentException, UnexpectedValueException) instead of Go\Aop\AspectException — no Go\Aop imports remain in Container.

BC breaks (4.0-dev)

  1. AspectContainer::registerAspect() removed — use add() / addLazyService().
  2. AspectContainer gains onRegistration() and public addResource(); custom containerClass implementations must follow.
  3. Registration-time aspect validation errors are gone; wrong factories fail on first use.
  4. Bare Container construction registers no framework services — call FrameworkServices::register() (the kernel does).
  5. Container exception types changed to SPL.

Validation

  • composer cs — clean (PER-CS)
  • composer analyze — clean (PHPStan level 10)
  • composer test under PHP 8.4 — 2666 tests, 3312 assertions, 0 failures (10 pre-existing skips). The functional suite fails under this environment's PHP 8.5.10 identically on master (pointcut parser issue unrelated to this change).

🤖 Generated with Claude Code

https://claude.ai/code/session_01W6rjmxdekGoJdXwwxDjhD1


Generated by Claude Code

Add Go\Core\NativeLazyProxy, a shared helper that creates PHP native lazy
proxies by asking the engine instead of re-deriving compatibility rules in
userland: newLazyProxy() throws for classes PHP cannot make lazy (memoized
for long-running workers), and one isUninitializedLazyObject() probe detects
property-less classes whose initializer would never run. The trusted create()
variant serves framework-owned classes; Interceptor::createLazily() switches
to it, unifying the two previously diverged lazy-proxy sites.

Asking the engine also retires the stale readonly-before-8.5 version gate:
current PHP 8.4.x already creates lazy proxies of readonly classes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6rjmxdekGoJdXwwxDjhD1
The container is now a generic DI implementation with zero aspect
awareness, and every aspect-specific code path it carried is gone:

* registerAspect(), materializeAspect(), validateAspectRegistration(),
  the per-aspect factory validators map and isDebug() are removed.
  Aspects register through the generic API from configureAop():
  add(Foo::class, new Foo()) eagerly, or
  addLazyService(Foo::class, static fn() => new Foo(...)) deferred.
  This also ends the double validation pass (validator hook plus
  materializeAspect re-check) that ran per aspect initialization -
  with a factory closure always present, the old constructibility
  validation is moot, and a wrong factory result is caught by the
  container's instanceof guard on first use.

* New interface-keyed registration listeners:
  onRegistration(Aspect::class, $listener) fires with the id (never the
  value, preserving laziness) when a matching deferred service is
  registered. The kernel arms one listener in debug mode only to track
  aspect source files for cache freshness at registration time;
  production registration stays a pure array write. addResource() joins
  the AspectContainer interface as public for that listener.

* Framework service wiring moves out of the container constructor into
  FrameworkServices::register(), called by the kernel during init().

* isLazyProxyCompatible() is deleted from the service read path - the
  full property enumeration and parent-chain reflection walk per first
  touch of every service is replaced by the NativeLazyProxy engine
  probe, one engine-level boolean in the compatible case.

* getService() now delegates to getValue() plus the instanceof guard
  instead of duplicating the materialize-or-throw block, and the
  container throws SPL exceptions (InvalidArgumentException,
  UnexpectedValueException) instead of Go\Aop\AspectException.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6rjmxdekGoJdXwwxDjhD1
@codecov

codecov Bot commented Sep 3, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 85.40%. Comparing base (57221fa) to head (e672fa2).
⚠️ Report is 4 commits behind head on master.

Additional details and impacted files
@@             Coverage Diff              @@
##             master     #658      +/-   ##
============================================
+ Coverage     85.32%   85.40%   +0.08%     
+ Complexity     1656     1636      -20     
============================================
  Files            97       99       +2     
  Lines          4545     4509      -36     
============================================
- Hits           3878     3851      -27     
+ Misses          667      658       -9     

☔ 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.

@lisachenko lisachenko changed the title AOP-unaware container: aspects as typical services, engine-probed lazy proxies [Feature] AOP-unaware container: aspects as typical services, engine-probed lazy proxies Sep 3, 2026
Comment thread CHANGELOG.md Outdated
Comment thread CHANGELOG.md Outdated
Comment thread CHANGELOG.md Outdated
NativeLazyProxy, the container's SPL exception types and the
FrameworkServices wiring are internal parts, not user-facing changes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6rjmxdekGoJdXwwxDjhD1
@lisachenko
lisachenko marked this pull request as ready for review September 3, 2026 08:58
@lisachenko
lisachenko merged commit 3cbf646 into master Sep 3, 2026
12 checks passed
@lisachenko
lisachenko deleted the claude/container-aop-refactor-0d706r branch September 3, 2026 08:58
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