Skip to content

feat(aop)!: first-class callable advices - #620

Open
lisachenko wants to merge 7 commits into
masterfrom
claude/first-class-callable-advices-ohsfqj
Open

feat(aop)!: first-class callable advices#620
lisachenko wants to merge 7 commits into
masterfrom
claude/first-class-callable-advices-ohsfqj

Conversation

@lisachenko

@lisachenko lisachenko commented Aug 29, 2026

Copy link
Copy Markdown
Member

What

Brings first-class callable advices into the framework as the main way advices are woven into generated code (supersedes the feature/direct-first-class-advices WIP branch, rebased onto master and reorganized into logical commits).

Generated proxies now reference each advice as a closure created with first-class callable syntax directly on the aspect instance:

static $__joinPoint = InterceptorInjector::forMethod(
    self::class,
    'doSomething',
    [
        Interceptor::before(The::aspect(MonitorAspect::class)->beforeMethodExecution(...)),
    ],
    $this->__aop__doSomething(...),
);

Advices registered in the container as plain closures (not aspect methods) are resolved lazily through the new The::advice('advisorId') accessor, which unwraps Advisor/interceptor values down to the raw advice closure.

Commits

  1. feat(aop): generate first-class advice callables — the proxy generation switch: The/Interceptor facades, GeneratedInterceptor descriptors, InterceptorListGenerator, removal of LazyAdvisorAccessor (rebased onto current master, adapting master's newer fixtures/tests).
  2. feat(aop)!: replace advice marker interfaces with AdviceTypeEnum — drops AdviceBefore/AdviceAfter/AdviceAround; Advice::getType(): AdviceTypeEnum now carries the advice kind and its sorting priority.
  3. feat(aop)!: require aspect advice methods to be public — FCC wiring calls advice methods on the aspect instance from generated code, so the attribute loader rejects non-public advice methods with an AspectException (methods with only #[Pointcut] keep any visibility).
  4. feat(aop): resolve closure advices through The::advice() accessor — container-backed closure advices become weavable; joinpoint flattening rejects non-Advice advisor values loudly; GeneratedInterceptor::fromAdvisorId() removed.
  5. docs: document first-class callable advices — CHANGELOG 4.0.0 entries and README rework presenting FCC advices as the main approach.
  6. docs: refresh AGENTS.md for first-class callable advice wiring — agent guides updated to the new proxy dispatch and advice-wiring components.

BC breaks

  • Aspect advice methods (#[Before], #[After], #[Around], #[AfterThrowing]) must be public.
  • AdviceBefore/AdviceAfter/AdviceAround marker interfaces removed; every Advice implementation must implement getType(): AdviceTypeEnum.

Testing

  • ./vendor/bin/phpunit — 2609 tests, 3216 assertions, green (validated on every commit of the series).
  • ./vendor/bin/phpstan analyze --memory-limit=512M — level 10, no errors on every commit.

lisachenko and others added 6 commits August 29, 2026 18:52
Generate proxy interceptors with The::aspect() first-class callables and pass interceptor instances directly into joinpoint initialization.

Format generated joinpoint initialization across multiple lines and restore default interceptor state during unserialization.
Drop the AdviceBefore/AdviceAfter/AdviceAround marker interfaces and add a
required Advice::getType(): AdviceTypeEnum method instead. The new backed enum
carries the advice kind (before, after, afterThrowing, around, introduction)
together with its invocation priority, so joinpoint sorting and generated
interceptor factory resolution no longer rely on instanceof checks against
marker interfaces or a hard-coded interceptor class map.

BREAKING CHANGE: the AdviceBefore, AdviceAfter and AdviceAround interfaces are
removed and every Advice implementation must now implement getType().

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019M9gpum1Rgasc2KtBZ3UhD
Advice methods discovered through attributes must now be declared public.
Generated proxies reference advices as first-class callables on the aspect
instance (The::aspect(SomeAspect::class)->adviceMethod(...)), which is only
possible when the advice method is callable from the outside. The attribute
aspect loader now fails fast with an AspectException instead of producing a
proxy that would fatal at runtime. Methods holding only a #[Pointcut]
attribute may stay protected/private as before.

BREAKING CHANGE: aspects with protected or private advice methods are
rejected during aspect loading and must make those methods public.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019M9gpum1Rgasc2KtBZ3UhD
Aspect-method advices stay the primary path: proxies reference them directly
as first-class callables via The::aspect(SomeAspect::class)->method(...).
Advices registered in the container as plain closures (or interceptors whose
closure is not scoped to an Aspect class) can now be woven too — the
GeneratedInterceptor descriptor marks them as container-backed and the
generated code resolves them lazily with The::advice('advisorId'), which
unwraps Advisor and AbstractInterceptor values down to the raw closure.

The joinpoint flattening now rejects non-Advice advisor values loudly instead
of guessing an aspect method from the advisor id (fromAdvisorId is removed),
and InterceptorListGenerator only accepts generated interceptor descriptors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019M9gpum1Rgasc2KtBZ3UhD
Add 4.0.0 changelog entries for the first-class callable advices feature and
its two breaking changes (public advice methods, advice marker interfaces
replaced by AdviceTypeEnum). Rework the README aspect guide to present
first-class callable advices as the main approach: advice methods are public,
woven directly as closures on the aspect instance via The::aspect(), with
The::advice() covering container-registered closure advices.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019M9gpum1Rgasc2KtBZ3UhD
Update the agent guides to describe the new proxy dispatch: interceptor lists
built from first-class advice callables (The::aspect()/The::advice()), the
Interceptor factory facade, GeneratedInterceptor descriptors, AdviceTypeEnum
and the public advice method requirement.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019M9gpum1Rgasc2KtBZ3UhD
@lisachenko
lisachenko marked this pull request as ready for review August 30, 2026 14:44
Comment thread src/Proxy/FunctionProxyGenerator.php Outdated
Comment thread src/Proxy/FunctionProxyGenerator.php Outdated
…nerators

Address review feedback on #620: replace string-literal FQCNs in addUse()
calls with ::class references (imports added to the four proxy generators),
and give InterceptorListGenerator::generate() a documented default indent
constant so call sites no longer pass a bare whitespace string.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019M9gpum1Rgasc2KtBZ3UhD
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