Reject aspect and class map registration after proxy generation

Merged
BI
3d ago
+646-79

This makes late aspect and class-map registration fail immediately, with an error directing the caller to the service provider's register() method. It follows up on the registration gap raised in the review of #656.

Why

Aspect registration already belongs in register(), before proxy generation. That requirement was documented but not enforced. Registering an aspect in boot() could do nothing, or it could appear to work if another aspect had already caused the same method to be proxied and its dispatch chain had not yet been cached. The result depended on which code had run first.

Class-map overrides have the same deadline: an override registered after proxy generation can bypass a proxy or be ignored because of autoloader ordering. Both APIs now reject late registration before changing their registries.

Behavior

The framework manages the registration window automatically:

  1. Open registration before providers run register().
  2. Generate proxies after every provider has registered.
  3. Close registration before providers run boot().

Registration closes even when there are no aspects or no matching targets. Applications and packages do not need to call seal() or unseal(). They continue using $this->aspects(...) and $this->classMap(...) from register().

The bootstrap order is unchanged. This does not add an ordering requirement between providers' aspect registrations. Code that registers during boot() or later now gets a clear exception instead of depending on incidental proxy coverage or cache state.

Implementation and testing support

Each registry has a boolean seal, checked only when registering. There are no new checks in aspect dispatch and no added per-request work. Aspect rule changes clear compiled chains so subsequent calls cannot use an outdated list or priority order.

Repeated application bootstraps reopen registration without discarding existing entries, then close it again after generation. Standalone GenerateProxies::generate() remains reusable without sealing registration.

Focused tests that register aspects after application bootstrap explicitly call AspectCollector::unseal(). This preserves existing application aspects and works with callWithAspects(); it cannot rewrite classes PHP has already loaded. Normal test cleanup resets the registry. The PHPUnit extension also reopens registration while preparing its temporary Guzzle proxy rules and restores saved rules through its existing cleanup path.

This also fixes a failure exposed while testing application refresh. When replacement application creation throws, Testbench retains the previous application but could leave container and facade globals pointing at the flushed replacement. It now restores those globals, clears resolved facades, and rethrows the original exception. Successful refresh behavior is unchanged.

The AOP, provider, and package documentation describe the enforced deadline and focused-testing API.

Validation

Regression coverage includes late registration from provider boot(), rejection without registry mutation, empty and unmatched proxy generation, chain invalidation, repeated application bootstrap with reversed provider order, PHPUnit preparation failures, and failed Testbench refresh.

Formatting, full static analysis, the framework suite, package-mode Testbench, and dogfood checks passed before merging the latest 0.4. After the merge, targeted DI/AOP, provider-bootstrap, AOP helper, PHPUnit extension, and Testbench refresh checks passed. The full suite was not rerun after the merge.

One earlier full-suite run stalled in a native worker. An isolated rerun completed and a fresh full-suite retry passed unchanged; the stall was not reproduced and its cause remains unconfirmed.

[!NOTE]

Reject aspect and class-map registration after proxy generation

  • AspectCollector and ClassMapManager gain seal(), unseal(), and invalidation of compiled chains; GenerateProxies::bootstrap() seals both registries after proxy generation, even when nothing is generated. Late registration from boot() or later now throws (InvalidDefinitionException for aspects, RuntimeException for class-map overrides).
  • RegisterProviders::bootstrap() reopens both registries so a new application boot can register providers, and flushState() on both managers reopens registration.
  • AspectManager::clear() discards cached chains; accepted rule changes and forgetAspect() invalidate compiled chains automatically.
  • Test support: InteractsWithAop setup calls AspectCollector::unseal(), the PHPUnit AfterEachTestExtension reopens registration before adding temporary Guzzle aspects, and TestCase::refreshApplication() restores the prior application, container, and facades if replacement creation fails.
  • Behavioral Change: providers must register aspects and class-map overrides in register(); calls from boot() or later throw after proxy generation. Direct rule registration in focused tests requires AspectCollector::unseal(). See AspectCollector.php and ClassMapManager.php.

Macroscope summarized 1ada627.

Summary by CodeRabbit

  • New Features
    • Aspect and class-map registration now closes after proxy generation. Register rules and overrides during provider registration; attempts made later are rejected.
    • Application restarts reopen registration while retaining existing rules and overrides.
  • Bug Fixes
    • Changes to aspect rules now take effect immediately, without requiring a full reset.
    • If creating a replacement application fails, the previous application remains available.
  • Documentation
    • Updated guidance on registration timing and test setup.