diff --git a/AGENTS.md b/AGENTS.md index 22f67e14..00371319 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,7 +19,7 @@ AOP via source transformation at load time (stream filter, no PECL, no eval). Intercepts PHP class loading pipeline: source stream filter transforms source → injects interception hooks → caches result. - Init: AspectKernel::init() → stream filter → transformers → configureAop() - Main transformer: WeavingTransformer (class→trait, proxy class re-inherits parent+interfaces) -- Proxy dispatch: per-method static $__joinPoint → InterceptorInjector → advisor chain +- Proxy dispatch: per-method static $__joinPoint → InterceptorInjector → interceptor chain of first-class advice callables (The::aspect(X::class)->m(...), The::advice('id') for closure advices) ## Directory → AGENTS.md map | Directory | Sub-AGENTS.md | Covers | diff --git a/CHANGELOG.md b/CHANGELOG.md index 97cb0e06..21624886 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ Changelog ====== 4.0.0 (unreleased) * [BC BREAK] Requires PHP 8.4+ +* [Feature] **First-class callable advices** — the main way advices are now wired into woven code. Generated proxies reference each advice as a closure created with first-class callable syntax directly on the aspect instance, e.g. `Interceptor::before(The::aspect(MonitorAspect::class)->beforeMethodExecution(...))`, so the advice chain is plain, readable, IDE-navigable PHP with no lazy advisor indirection (`LazyAdvisorAccessor` is removed). Advices registered in the container as plain closures (not aspect methods) are resolved lazily through the new `The::advice('advisorId')` accessor, which unwraps `Advisor` and interceptor values down to the raw advice closure. +* [BC BREAK] **Aspect advice methods must be public.** Because generated proxies call advices as first-class callables on the aspect instance, an advice method annotated with `#[Before]`, `#[After]`, `#[Around]` or `#[AfterThrowing]` can no longer be `protected` or `private` — the aspect loader now throws an `AspectException` for non-public advice methods. Methods holding only a `#[Pointcut]` attribute may keep any visibility. +* [BC BREAK] Removed the `AdviceBefore`, `AdviceAfter` and `AdviceAround` marker interfaces. The `Advice` interface now requires `getType(): AdviceTypeEnum`, and the new `AdviceTypeEnum` backed enum (`Before`, `After`, `AfterThrowing`, `Around`, `Introduction`) carries both the advice kind and its invocation priority used for joinpoint sorting. * [BC BREAK] Proxy engine switched from inheritance-based to **trait-based**: the original class body is converted to a PHP trait (`Foo__AopProxied`) and the proxy class uses it via `use` with private method aliases instead of extending the renamed class. This removes the `__AopProxied` parent from the inheritance chain. * [BC BREAK] All invocation class constructors (`DynamicTraitAliasMethodInvocation`, `StaticTraitAliasMethodInvocation`, `ReflectionFunctionInvocation`) now require a `Closure $closureToCall` parameter (non-nullable). Generated proxy code always passes a first-class callable: `$this->__aop__method(...)` for own instance methods, `self::__aop__method(...)` for own static methods, `parent::method(...)` for inherited methods, and `\functionName(...)` for functions. * [Feature] **Private method interception** — both dynamic (`private function foo()`) and static (`private static function bar()`) private methods can now be intercepted by aspects. This was impossible with the old extend-based engine because PHP does not allow overriding private methods in subclasses. diff --git a/README.md b/README.md index 0506bcc2..4297b5d6 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ if ($fieldAccess->getField()->isInitialized($this)) { - **Opcode cache friendly** — First-class support for **OPcache**. Transformed files and classes are stored as plain PHP files, fully optimized by your opcode cache just like regular code. - - **Smart caching** — Lazy loading of advice and aspects — only what's needed gets loaded. Joinpoints are resolved at compile-time and cached, eliminating runtime reflection costs. + - **Smart caching** — Advices are woven as **first-class callables** pointing straight at your aspect methods, joinpoints are resolved at compile-time and cached in the generated code — eliminating runtime reflection costs and lazy advisor indirection. - **No runtime overhead** — Zero runtime annotation parsing, no slow `__call` methods, no proxy objects wrapping your instances. Method interception happens through direct, inlined PHP code — as fast as handwritten cross-cutting code. **Zero** overhead for non-intercepted methods. @@ -231,6 +231,7 @@ $applicationAspectKernel->init([ ### 4. Create an aspect Aspect is the key element of AOP philosophy. Go! AOP framework just uses simple PHP classes for declaring aspects, which makes it possible to use all features of OOP for aspect classes. +Advices are declared as **public methods** of the aspect — the framework weaves them into your code as [first-class callables](https://www.php.net/manual/en/functions.first_class_callable_syntax.php), so every advice must be callable on the aspect instance from the outside (a `protected` or `private` advice method is rejected during aspect loading). As an example, let's intercept all the methods and display their names: ```php @@ -270,6 +271,36 @@ all dynamic public methods in the class Example. This is done with the help of a `#[Before("execution(public Example->*(*))")]` Hooks can be of any types, you will see them later. +#### Advices are first-class callables + +There is no magic behind applying an aspect. For every intercepted method the framework +generates a plain, debuggable interceptor chain in which each advice is referenced as a +**closure created with first-class callable syntax** right on the aspect instance: + +```php +static $__joinPoint = InterceptorInjector::forMethod( + self::class, + 'doSomething', + [ + Interceptor::before(The::aspect(MonitorAspect::class)->beforeMethodExecution(...)), + ], + $this->__aop__doSomething(...), +); +``` + +`The::aspect()` fetches the aspect instance from the aspect container, and +`->beforeMethodExecution(...)` is the very advice method you wrote above — you can +Ctrl-click it in your IDE, set a breakpoint inside it, and step through the woven code as if +it were handwritten. This direct wiring is the main way advices are applied. + +Advices that are registered in the container as plain closures (rather than aspect methods) +are woven through the lazy `The::advice()` accessor instead, which resolves the advisor by +its identifier and unwraps it down to the raw advice closure: + +```php +Interceptor::around(The::advice('advisor.Demo\Aspect\DynamicMethodsAspect->aroundMagicMethods')), +``` + ### 5. Register the aspect in the aspect kernel To register the aspect just add an instance of it in the `configureAop()` method of the kernel: diff --git a/demos/Demo/Aspect/CachingAspect.php b/demos/Demo/Aspect/CachingAspect.php index f6ba4816..d3114be4 100644 --- a/demos/Demo/Aspect/CachingAspect.php +++ b/demos/Demo/Aspect/CachingAspect.php @@ -31,7 +31,7 @@ class CachingAspect implements Aspect * Real-life examples will use APC or Memcache to store value in the cache */ #[Around('@execution(Demo\Attribute\Cacheable)')] - protected function aroundCacheable(MethodInvocation $invocation): mixed + public function aroundCacheable(MethodInvocation $invocation): mixed { static $memoryCache = []; diff --git a/demos/Demo/Aspect/FluentInterfaceAspect.php b/demos/Demo/Aspect/FluentInterfaceAspect.php index f5b5dd10..2d28c3a8 100644 --- a/demos/Demo/Aspect/FluentInterfaceAspect.php +++ b/demos/Demo/Aspect/FluentInterfaceAspect.php @@ -31,7 +31,7 @@ class FluentInterfaceAspect implements Aspect * Fluent interface advice */ #[Around('within(Demo\Aspect\FluentInterface+) && execution(public **->set*(*))')] - protected function aroundMethodExecution(MethodInvocation $invocation): mixed + public function aroundMethodExecution(MethodInvocation $invocation): mixed { $result = $invocation->proceed(); diff --git a/demos/Demo/Aspect/HealthyLiveAspect.php b/demos/Demo/Aspect/HealthyLiveAspect.php index 45fd268e..be697aa4 100644 --- a/demos/Demo/Aspect/HealthyLiveAspect.php +++ b/demos/Demo/Aspect/HealthyLiveAspect.php @@ -38,7 +38,7 @@ protected function humanEat(): void * @param DynamicMethodInvocation $invocation */ #[Before('$this->humanEat')] - protected function washUpBeforeEat(DynamicMethodInvocation $invocation): void + public function washUpBeforeEat(DynamicMethodInvocation $invocation): void { $person = $invocation->getThis(); $person->washUp(); @@ -50,7 +50,7 @@ protected function washUpBeforeEat(DynamicMethodInvocation $invocation): void * @param DynamicMethodInvocation $invocation */ #[After('$this->humanEat')] - protected function cleanTeethAfterEat(DynamicMethodInvocation $invocation): void + public function cleanTeethAfterEat(DynamicMethodInvocation $invocation): void { $person = $invocation->getThis(); $person->cleanTeeth(); @@ -62,7 +62,7 @@ protected function cleanTeethAfterEat(DynamicMethodInvocation $invocation): void * @param DynamicMethodInvocation $invocation */ #[Before('execution(public Demo\Example\HumanDemo->sleep(*))')] - protected function cleanTeethBeforeSleep(DynamicMethodInvocation $invocation): void + public function cleanTeethBeforeSleep(DynamicMethodInvocation $invocation): void { $person = $invocation->getThis(); $person->cleanTeeth(); diff --git a/src/Aop/AGENTS.md b/src/Aop/AGENTS.md index 14909dc2..6c020aac 100644 --- a/src/Aop/AGENTS.md +++ b/src/Aop/AGENTS.md @@ -35,6 +35,13 @@ Proxy generators use TypeGenerator::renderTypeForPhpDoc() to emit V as 2nd gener | ClassFieldAccess | FieldAccess | Property interception via native get/set hooks on proxied properties | | StaticInitializationJoinpoint | ClassJoinpoint | Fired once after proxy class loaded via injectJoinPoints() | +## Advice wiring (src/Aop/Framework/) +- The — proxy-code accessor: aspect(X::class) fetches aspect from container; advice('advisorId') resolves container-backed closure advice (unwraps Advisor/AbstractInterceptor to raw Closure) +- Interceptor — factory facade for generated code: before()/after()/around()/afterThrowing(Closure, int $order=0) +- GeneratedInterceptor — internal descriptor built by AbstractJoinpoint::flatAndSortAdvices() via fromAdvice(); usesContainerAdvice=true when advice closure isn't scoped to an Aspect class +- AdviceTypeEnum — Advice::getType() kind + sorting priority (before → after/afterThrowing → around → introduction); replaced AdviceBefore/AdviceAfter/AdviceAround marker interfaces +- Advice methods MUST be public (FCC calls them on the aspect instance from generated code) + ## Pointcuts (src/Aop/Pointcut/) - LALR grammar: PointcutGrammar, PointcutParser, PointcutLexer, PointcutParseTable - Combinators: AndPointcut, OrPointcut, NotPointcut, NamePointcut, AttributePointcut, ClassInheritancePointcut, MatchInheritedPointcut, ModifierPointcut, ReturnTypePointcut, TruePointcut diff --git a/src/Aop/Advice.php b/src/Aop/Advice.php index e62f10a4..735005d0 100644 --- a/src/Aop/Advice.php +++ b/src/Aop/Advice.php @@ -19,4 +19,11 @@ */ interface Advice { + + /** + * Returns the Advice type + * + * @api + */ + public function getType(): AdviceTypeEnum; } diff --git a/src/Aop/AdviceAfter.php b/src/Aop/AdviceAfter.php deleted file mode 100644 index 10026a17..00000000 --- a/src/Aop/AdviceAfter.php +++ /dev/null @@ -1,22 +0,0 @@ - - * - * This source file is subject to the license that is bundled - * with this source code in the file LICENSE. - */ - -namespace Go\Aop; - -/** - * Tag class for all after advices either for field access or method calling - * - * @api - */ -interface AdviceAfter extends Advice -{ -} diff --git a/src/Aop/AdviceAround.php b/src/Aop/AdviceAround.php deleted file mode 100644 index dda6fb10..00000000 --- a/src/Aop/AdviceAround.php +++ /dev/null @@ -1,22 +0,0 @@ - - * - * This source file is subject to the license that is bundled - * with this source code in the file LICENSE. - */ - -namespace Go\Aop; - -/** - * Tag class for all around advices either for field access or method calling - * - * @api - */ -interface AdviceAround extends Advice -{ -} diff --git a/src/Aop/AdviceBefore.php b/src/Aop/AdviceBefore.php deleted file mode 100644 index 7b8ec586..00000000 --- a/src/Aop/AdviceBefore.php +++ /dev/null @@ -1,22 +0,0 @@ - - * - * This source file is subject to the license that is bundled - * with this source code in the file LICENSE. - */ - -namespace Go\Aop; - -/** - * Tag class for all before advices either for field access or method calling - * - * @api - */ -interface AdviceBefore extends Advice -{ -} diff --git a/src/Aop/AdviceTypeEnum.php b/src/Aop/AdviceTypeEnum.php new file mode 100644 index 00000000..388182b6 --- /dev/null +++ b/src/Aop/AdviceTypeEnum.php @@ -0,0 +1,50 @@ + + * + * This source file is subject to the license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Go\Aop; + +/** + * Advice type enumeration + * + * @api + */ +enum AdviceTypeEnum: string +{ + case After = 'after'; + case AfterThrowing = 'afterThrowing'; + case Around = 'around'; + case Before = 'before'; + case Introduction = 'introduction'; + + /** + * Compares the relative invocation priority against another advice type. + * + * Advices execute in the order before -> after (and after-throwing) -> around, matching the + * classic AOP interceptor chain where "around" wraps everything else. + * + * @api + */ + public function compareTo(self $other): int + { + return $this->sortWeight() <=> $other->sortWeight(); + } + + private function sortWeight(): int + { + return match ($this) { + self::Before => 0, + self::After, self::AfterThrowing => 1, + self::Around => 2, + self::Introduction => 3, + }; + } +} diff --git a/src/Aop/Framework/AbstractInterceptor.php b/src/Aop/Framework/AbstractInterceptor.php index aaa1e5c7..bc51b1e3 100644 --- a/src/Aop/Framework/AbstractInterceptor.php +++ b/src/Aop/Framework/AbstractInterceptor.php @@ -102,7 +102,7 @@ final public function __serialize(): array final public function __unserialize(array $state): void { $state['adviceMethod'] = static::unserializeAdvice($state['adviceMethod']); - foreach ($state as $key => $value) { + foreach ($state + ['adviceOrder' => 0, 'pointcutExpression' => ''] as $key => $value) { $this->$key = $value; } } diff --git a/src/Aop/Framework/AbstractJoinpoint.php b/src/Aop/Framework/AbstractJoinpoint.php index 10fb7c05..44b51add 100644 --- a/src/Aop/Framework/AbstractJoinpoint.php +++ b/src/Aop/Framework/AbstractJoinpoint.php @@ -13,9 +13,8 @@ namespace Go\Aop\Framework; use Go\Aop\Advice; -use Go\Aop\AdviceAfter; -use Go\Aop\AdviceAround; -use Go\Aop\AdviceBefore; +use Go\Aop\AspectException; +use Go\Aop\IntroductionInfo; use Go\Aop\Intercept\Interceptor; use Go\Aop\Intercept\Joinpoint; use Go\Aop\OrderedAdvice; @@ -52,21 +51,26 @@ public function __construct(protected readonly array $advices = []) {} /** * Sorts advices by priority * - * @param array $advices + * @param array $advices * - * @return array Sorted list of advices + * @return array Sorted list of advices */ public static function sortAdvices(array $advices): array { $sortedAdvices = $advices; uasort( $sortedAdvices, - fn(Advice $first, Advice $second) => match (true) { - $first instanceof AdviceBefore && !($second instanceof AdviceBefore) => -1, - $first instanceof AdviceAround && !($second instanceof AdviceAround) => 1, - $first instanceof AdviceAfter && !($second instanceof AdviceAfter) => $second instanceof AdviceBefore ? 1 : -1, - $first instanceof OrderedAdvice && $second instanceof OrderedAdvice => $first->getAdviceOrder() - $second->getAdviceOrder(), - default => 0, + function (mixed $first, mixed $second): int { + if ($first instanceof Advice && $second instanceof Advice) { + $priority = $first->getType()->compareTo($second->getType()); + if ($priority !== 0) { + return $priority; + } + } + + return $first instanceof OrderedAdvice && $second instanceof OrderedAdvice + ? $first->getAdviceOrder() - $second->getAdviceOrder() + : 0; } ); @@ -74,18 +78,30 @@ public static function sortAdvices(array $advices): array } /** - * Replace concrete advices with list of ids + * Replace concrete advices with generated-code descriptors or introduction ids. * - * @param array>> $advices List of advices + * @param array>> $advices List of advices * - * @return array>> Sorted identifier of advices/interceptors + * @return array>> Sorted advices/interceptors */ public static function flatAndSortAdvices(array $advices): array { $flattenAdvices = []; foreach ($advices as $type => $typedAdvices) { foreach ($typedAdvices as $name => $concreteAdvices) { - $flattenAdvices[$type][$name] = array_keys(self::sortAdvices($concreteAdvices)); + foreach (self::sortAdvices($concreteAdvices) as $advisorId => $advice) { + if ($advice instanceof IntroductionInfo) { + $flattenAdvices[$type][$name][] = (string) $advisorId; + + continue; + } + if (!$advice instanceof Advice) { + throw new AspectException( + "Advisor {$advisorId} provides " . get_debug_type($advice) . ' instead of advice instance' + ); + } + $flattenAdvices[$type][$name][] = GeneratedInterceptor::fromAdvice((string) $advisorId, $advice); + } } } diff --git a/src/Aop/Framework/AfterInterceptor.php b/src/Aop/Framework/AfterInterceptor.php index 8780c2ec..f9348f25 100644 --- a/src/Aop/Framework/AfterInterceptor.php +++ b/src/Aop/Framework/AfterInterceptor.php @@ -12,7 +12,7 @@ namespace Go\Aop\Framework; -use Go\Aop\AdviceAfter; +use Go\Aop\AdviceTypeEnum; use Go\Aop\Intercept\Joinpoint; /** @@ -20,7 +20,7 @@ * * @api */ -final class AfterInterceptor extends AbstractInterceptor implements AdviceAfter +final class AfterInterceptor extends AbstractInterceptor { public function invoke(Joinpoint $joinpoint): mixed { @@ -30,4 +30,9 @@ public function invoke(Joinpoint $joinpoint): mixed ($this->adviceMethod)($joinpoint); } } + + public function getType(): AdviceTypeEnum + { + return AdviceTypeEnum::After; + } } diff --git a/src/Aop/Framework/AfterThrowingInterceptor.php b/src/Aop/Framework/AfterThrowingInterceptor.php index 33e61d47..069e2f1c 100644 --- a/src/Aop/Framework/AfterThrowingInterceptor.php +++ b/src/Aop/Framework/AfterThrowingInterceptor.php @@ -12,7 +12,7 @@ namespace Go\Aop\Framework; -use Go\Aop\AdviceAfter; +use Go\Aop\AdviceTypeEnum; use Go\Aop\Intercept\Joinpoint; use Throwable; @@ -21,7 +21,7 @@ * * @api */ -final class AfterThrowingInterceptor extends AbstractInterceptor implements AdviceAfter +final class AfterThrowingInterceptor extends AbstractInterceptor { /** * @inheritdoc @@ -37,4 +37,9 @@ public function invoke(Joinpoint $joinpoint): mixed throw $throwableInstance; } } + + public function getType(): AdviceTypeEnum + { + return AdviceTypeEnum::AfterThrowing; + } } diff --git a/src/Aop/Framework/AroundInterceptor.php b/src/Aop/Framework/AroundInterceptor.php index 89a65307..c595561c 100644 --- a/src/Aop/Framework/AroundInterceptor.php +++ b/src/Aop/Framework/AroundInterceptor.php @@ -12,7 +12,7 @@ namespace Go\Aop\Framework; -use Go\Aop\AdviceAround; +use Go\Aop\AdviceTypeEnum; use Go\Aop\Intercept\Joinpoint; /** @@ -20,10 +20,15 @@ * * @api */ -final class AroundInterceptor extends AbstractInterceptor implements AdviceAround +final class AroundInterceptor extends AbstractInterceptor { public function invoke(Joinpoint $joinpoint): mixed { return ($this->adviceMethod)($joinpoint); } + + public function getType(): AdviceTypeEnum + { + return AdviceTypeEnum::Around; + } } diff --git a/src/Aop/Framework/BeforeInterceptor.php b/src/Aop/Framework/BeforeInterceptor.php index 6da1cf78..61d85c0a 100644 --- a/src/Aop/Framework/BeforeInterceptor.php +++ b/src/Aop/Framework/BeforeInterceptor.php @@ -12,7 +12,7 @@ namespace Go\Aop\Framework; -use Go\Aop\AdviceBefore; +use Go\Aop\AdviceTypeEnum; use Go\Aop\Intercept\Joinpoint; /** @@ -20,7 +20,7 @@ * * @api */ -final class BeforeInterceptor extends AbstractInterceptor implements AdviceBefore +final class BeforeInterceptor extends AbstractInterceptor { public function invoke(Joinpoint $joinpoint): mixed { @@ -28,4 +28,9 @@ public function invoke(Joinpoint $joinpoint): mixed return $joinpoint->proceed(); } + + public function getType(): AdviceTypeEnum + { + return AdviceTypeEnum::Before; + } } diff --git a/src/Aop/Framework/GeneratedInterceptor.php b/src/Aop/Framework/GeneratedInterceptor.php new file mode 100644 index 00000000..b80f69e5 --- /dev/null +++ b/src/Aop/Framework/GeneratedInterceptor.php @@ -0,0 +1,55 @@ + + * + * This source file is subject to the license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Go\Aop\Framework; + +use Go\Aop\Advice; +use Go\Aop\Aspect; +use Go\Aop\AspectException; +use ReflectionFunction; + +/** + * Internal descriptor used by proxy generators to render first-class advice callables. + * + * @internal + */ +final readonly class GeneratedInterceptor +{ + private function __construct( + public string $factoryMethod, + public ?string $aspectClass, + public ?string $adviceMethod, + public int $order, + public string $advisorId, + public bool $usesContainerAdvice = false + ) {} + + public static function fromAdvice(string $advisorId, Advice $advice): self + { + if (!$advice instanceof AbstractInterceptor) { + throw new AspectException("Advisor {$advisorId} uses unsupported advice " . get_debug_type($advice) . '; only framework aspect-method interceptors can be generated'); + } + + $reflectionAdvice = new ReflectionFunction($advice->getRawAdvice()); + $scopeClass = $reflectionAdvice->getClosureScopeClass(); + $usesContainerAdvice = $scopeClass === null || !is_subclass_of($scopeClass->name, Aspect::class); + + return new self( + $advice->getType()->value, + $usesContainerAdvice ? null : $scopeClass->name, + $usesContainerAdvice ? null : $reflectionAdvice->name, + $advice->getAdviceOrder(), + $advisorId, + $usesContainerAdvice + ); + } +} diff --git a/src/Aop/Framework/Interceptor.php b/src/Aop/Framework/Interceptor.php new file mode 100644 index 00000000..1a65ed4e --- /dev/null +++ b/src/Aop/Framework/Interceptor.php @@ -0,0 +1,41 @@ + + * + * This source file is subject to the license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Go\Aop\Framework; + +use Closure; + +/** + * Factory facade for generated proxy interceptor declarations. + */ +final class Interceptor +{ + public static function before(Closure $advice, int $order = 0): BeforeInterceptor + { + return new BeforeInterceptor($advice, $order); + } + + public static function after(Closure $advice, int $order = 0): AfterInterceptor + { + return new AfterInterceptor($advice, $order); + } + + public static function around(Closure $advice, int $order = 0): AroundInterceptor + { + return new AroundInterceptor($advice, $order); + } + + public static function afterThrowing(Closure $advice, int $order = 0): AfterThrowingInterceptor + { + return new AfterThrowingInterceptor($advice, $order); + } +} diff --git a/src/Aop/Framework/InterceptorInjector.php b/src/Aop/Framework/InterceptorInjector.php index b780041b..d6079f1d 100644 --- a/src/Aop/Framework/InterceptorInjector.php +++ b/src/Aop/Framework/InterceptorInjector.php @@ -20,30 +20,26 @@ use Go\Aop\Intercept\FunctionInvocation; use Go\Aop\Intercept\Interceptor; use Go\Aop\Intercept\StaticMethodInvocation; -use Go\Core\AspectKernel; -use Go\Core\LazyAdvisorAccessor; /** * Central factory for creating concrete joinpoint implementations. */ final class InterceptorInjector { - private static ?LazyAdvisorAccessor $accessor = null; - /** * @template T of object * @param class-string $className * @param non-empty-string $methodName - * @param non-empty-list $advisorNames + * @param non-empty-list $interceptors * @param Closure $closureToCall First-class callable to the original method body, * e.g. `$this->__aop__method(...)` for trait-aliased methods or * `parent::method(...)` for inherited methods. * @return DynamicMethodInvocation */ - public static function forMethod(string $className, string $methodName, array $advisorNames, Closure $closureToCall): DynamicMethodInvocation + public static function forMethod(string $className, string $methodName, array $interceptors, Closure $closureToCall): DynamicMethodInvocation { return new DynamicTraitAliasMethodInvocation( - self::fillInterceptors($advisorNames), + $interceptors, $className, $methodName, $closureToCall @@ -54,16 +50,16 @@ public static function forMethod(string $className, string $methodName, array $a * @template T of object * @param class-string $className * @param non-empty-string $methodName - * @param non-empty-list $advisorNames + * @param non-empty-list $interceptors * @param Closure $closureToCall First-class callable to the original static method body, * e.g. `self::__aop__method(...)` for trait-aliased methods or * `parent::method(...)` for inherited methods. * @return StaticMethodInvocation */ - public static function forStaticMethod(string $className, string $methodName, array $advisorNames, Closure $closureToCall): StaticMethodInvocation + public static function forStaticMethod(string $className, string $methodName, array $interceptors, Closure $closureToCall): StaticMethodInvocation { return new StaticTraitAliasMethodInvocation( - self::fillInterceptors($advisorNames), + $interceptors, $className, $methodName, $closureToCall @@ -74,13 +70,13 @@ public static function forStaticMethod(string $className, string $methodName, ar * @template T of object * @param class-string $className * @param non-empty-string $propertyName - * @param non-empty-list $advisorNames + * @param non-empty-list $interceptors * @return FieldAccess */ - public static function forProperty(string $className, string $propertyName, array $advisorNames): FieldAccess + public static function forProperty(string $className, string $propertyName, array $interceptors): FieldAccess { return new ClassFieldAccess( - self::fillInterceptors($advisorNames), + $interceptors, $className, $propertyName ); @@ -88,14 +84,14 @@ public static function forProperty(string $className, string $propertyName, arra /** * @param non-empty-string $functionName - * @param non-empty-list $advisorNames + * @param non-empty-list $interceptors * @param Closure $closureToCall First-class callable to the original global function * (e.g. `\file_get_contents(...)`). */ - public static function forFunction(string $functionName, array $advisorNames, Closure $closureToCall): FunctionInvocation + public static function forFunction(string $functionName, array $interceptors, Closure $closureToCall): FunctionInvocation { return new ReflectionFunctionInvocation( - self::fillInterceptors($advisorNames), + $interceptors, $functionName, $closureToCall ); @@ -104,13 +100,13 @@ public static function forFunction(string $functionName, array $advisorNames, Cl /** * @template T of object * @param class-string $className - * @param non-empty-list $advisorNames + * @param non-empty-list $interceptors * @return ClassJoinpoint */ - public static function forStaticInitialization(string $className, array $advisorNames): ClassJoinpoint + public static function forStaticInitialization(string $className, array $interceptors): ClassJoinpoint { return new StaticInitializationJoinpoint( - self::fillInterceptors($advisorNames), + $interceptors, $className ); } @@ -118,32 +114,14 @@ public static function forStaticInitialization(string $className, array $advisor /** * @template T of object * @param class-string $className - * @param non-empty-list $advisorNames + * @param non-empty-list $interceptors * @return ConstructorInvocation */ - public static function forInitialization(string $className, array $advisorNames): ConstructorInvocation + public static function forInitialization(string $className, array $interceptors): ConstructorInvocation { return new ReflectionConstructorInvocation( - self::fillInterceptors($advisorNames), + $interceptors, $className ); } - - /** - * @param non-empty-list $advisorNames - * @return non-empty-list - */ - private static function fillInterceptors(array $advisorNames): array - { - if (self::$accessor === null) { - self::$accessor = AspectKernel::getInstance()->getContainer()->getService(LazyAdvisorAccessor::class); - } - - $filledAdvices = []; - foreach ($advisorNames as $advisorName) { - $filledAdvices[] = self::$accessor->getInterceptor($advisorName); - } - - return $filledAdvices; - } } diff --git a/src/Aop/Framework/The.php b/src/Aop/Framework/The.php new file mode 100644 index 00000000..0d162400 --- /dev/null +++ b/src/Aop/Framework/The.php @@ -0,0 +1,58 @@ + + * + * This source file is subject to the license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Go\Aop\Framework; + +use Closure; +use Go\Aop\Advisor; +use Go\Aop\Aspect; +use Go\Aop\AspectException; +use Go\Core\AspectContainer; +use Go\Core\AspectKernel; + +/** + * Accessor for aspect instances from generated proxy code. + */ +final class The +{ + /** + * @template T of Aspect + * @param class-string $aspectClass + * @return T + */ + public static function aspect(string $aspectClass): Aspect + { + return self::getContainer()->getService($aspectClass); + } + + public static function advice(string $advisorId): Closure + { + $value = self::getContainer()->getValue($advisorId); + + if ($value instanceof Advisor) { + $value = $value->getAdvice(); + } + if ($value instanceof AbstractInterceptor) { + return $value->getRawAdvice(); + } + if ($value instanceof Closure) { + return $value; + } + + throw new AspectException("Advisor {$advisorId} does not expose a closure advice"); + } + + private static function getContainer(): AspectContainer + { + return AspectKernel::getInstance()->getContainer(); + } +} diff --git a/src/Aop/Framework/TraitIntroductionInfo.php b/src/Aop/Framework/TraitIntroductionInfo.php index 6fd018fd..0e10cdc4 100644 --- a/src/Aop/Framework/TraitIntroductionInfo.php +++ b/src/Aop/Framework/TraitIntroductionInfo.php @@ -12,6 +12,7 @@ namespace Go\Aop\Framework; +use Go\Aop\AdviceTypeEnum; use Go\Aop\IntroductionInfo; /** @@ -39,4 +40,9 @@ public function getTrait(): string { return $this->introducedTrait; } + + public function getType(): AdviceTypeEnum + { + return AdviceTypeEnum::Introduction; + } } diff --git a/src/Aop/Intercept/Interceptor.php b/src/Aop/Intercept/Interceptor.php index 4dae2670..e4bdfc5c 100644 --- a/src/Aop/Intercept/Interceptor.php +++ b/src/Aop/Intercept/Interceptor.php @@ -13,6 +13,7 @@ namespace Go\Aop\Intercept; use Go\Aop\Advice; +use Go\Aop\AdviceTypeEnum; /** * This interface represents a generic interceptor. diff --git a/src/Core/AGENTS.md b/src/Core/AGENTS.md index dd6ffd2b..46bc286d 100644 --- a/src/Core/AGENTS.md +++ b/src/Core/AGENTS.md @@ -6,7 +6,7 @@ ## Aspect loading - AspectLoader / CachedAspectLoader — scan aspect classes for pointcut/advice attributes → Advisor[] -- AttributeAspectLoaderExtension — handles PHP 8 attribute-based aspect definitions +- AttributeAspectLoaderExtension — handles PHP 8 attribute-based aspect definitions; throws AspectException for non-public advice methods (first-class callable advices require public visibility; #[Pointcut]-only methods exempt) - AdviceMatcher — given class reflector, returns applicable advisors keyed by join point - Scans IS_PUBLIC|IS_PROTECTED|IS_PRIVATE methods - Private methods from parent classes excluded diff --git a/src/Core/AttributeAspectLoaderExtension.php b/src/Core/AttributeAspectLoaderExtension.php index 538272a5..b909d278 100644 --- a/src/Core/AttributeAspectLoaderExtension.php +++ b/src/Core/AttributeAspectLoaderExtension.php @@ -14,6 +14,7 @@ use Closure; use Go\Aop\Aspect; +use Go\Aop\AspectException; use Go\Aop\Framework\AfterInterceptor; use Go\Aop\Framework\AfterThrowingInterceptor; use Go\Aop\Framework\AroundInterceptor; @@ -64,12 +65,17 @@ public function load(Aspect $aspect, ReflectionClass $reflectionAspect): array * Returns an advice (interceptor) instance by meta-type attribute and closure * * @throws UnexpectedValueException For unsupported annotations + * @throws AspectException If the advice method is not public */ protected function getAdvice( AbstractInterceptor $interceptorAttribute, Aspect $aspect, ReflectionMethod $aspectMethod ): Interceptor { + if (!$aspectMethod->isPublic()) { + throw new AspectException("Advice method {$aspectMethod->class}::{$aspectMethod->name}() must be public; first-class advice callables require all advice methods to be public"); + } + $adviceCallback = $aspectMethod->getClosure($aspect); $adviceOrder = $interceptorAttribute->order; $pointcutExpression = $interceptorAttribute->expression; diff --git a/src/Core/Container.php b/src/Core/Container.php index a489e22b..05bb57a3 100644 --- a/src/Core/Container.php +++ b/src/Core/Container.php @@ -100,11 +100,6 @@ public function __construct(array $resources = []) return new CachedAspectLoader($container, AspectLoader::class, $options); }); - $this->addLazyService(LazyAdvisorAccessor::class, fn(AspectContainer $container): LazyAdvisorAccessor => new LazyAdvisorAccessor( - $container, - $container->getService(CachedAspectLoader::class) - )); - $this->addLazyService(CachePathManager::class, fn(AspectContainer $container): CachePathManager => new CachePathManager( $container->getService(AspectKernel::class) )); diff --git a/src/Core/LazyAdvisorAccessor.php b/src/Core/LazyAdvisorAccessor.php deleted file mode 100644 index a2e5ae35..00000000 --- a/src/Core/LazyAdvisorAccessor.php +++ /dev/null @@ -1,75 +0,0 @@ - - * - * This source file is subject to the license that is bundled - * with this source code in the file LICENSE. - */ - -namespace Go\Core; - -use Go\Aop\Advisor; -use Go\Aop\Aspect; -use Go\Aop\AspectException; -use Go\Aop\Intercept\Interceptor; -use InvalidArgumentException; - -/** - * Provides an interface for loading of advisors from the container - */ -final class LazyAdvisorAccessor -{ - /** - * @var array Resolved interceptors, keyed by advisor identifier - */ - private array $interceptors = []; - - /** - * Accessor constructor - */ - public function __construct( - private readonly AspectContainer $container, - private readonly AspectLoader $loader - ) {} - - /** - * Returns the Interceptor for the given advisor name, loading and caching it on first access - * - * @throws InvalidArgumentException if referenced value is not an advisor or its advice is not an Interceptor - */ - public function getInterceptor(string $name): Interceptor - { - return $this->interceptors[$name] ??= $this->loadInterceptor($name); - } - - /** - * Resolves an interceptor from the container, loading the owning aspect on demand - * - * @throws InvalidArgumentException if referenced value is not an advisor or its advice is not an Interceptor - */ - private function loadInterceptor(string $name): Interceptor - { - if (!$this->container->has($name)) { - [$aspectName] = explode('->', $name, 2); - if (!is_subclass_of($aspectName, Aspect::class)) { - throw new AspectException("{$aspectName} is not a valid aspect class"); - } - $aspectInstance = $this->container->getService($aspectName); - $this->loader->loadAndRegister($aspectInstance); - } - $advisor = $this->container->getValue($name); - if (!$advisor instanceof Advisor) { - throw new InvalidArgumentException("Reference {$name} is not an advisor"); - } - $advice = $advisor->getAdvice(); - if (!$advice instanceof Interceptor) { - throw new InvalidArgumentException("Advice {$name} is not an Interceptor"); - } - - return $advice; - } -} diff --git a/src/Instrument/AGENTS.md b/src/Instrument/AGENTS.md index 918a334d..15b1b2ab 100644 --- a/src/Instrument/AGENTS.md +++ b/src/Instrument/AGENTS.md @@ -33,12 +33,21 @@ class Foo extends OriginalParent implements OriginalInterfaces, \Go\Aop\Proxy public function interceptedMethod(ArgType $arg): ReturnType { /** @var \Go\Aop\Intercept\DynamicMethodInvocation $__joinPoint */ static $__joinPoint = \Go\Aop\Framework\InterceptorInjector::forMethod( - self::class, 'interceptedMethod', [...], $this->__aop__interceptedMethod(...) + self::class, + 'interceptedMethod', + [ + Interceptor::before(The::aspect(SomeAspect::class)->adviceMethod(...)), + ], + $this->__aop__interceptedMethod(...), ); return $__joinPoint->__invoke($this, [$arg]); } } ``` +Interceptor list entries are first-class advice callables on the aspect instance +(`The::aspect(X::class)->m(...)`); container-backed closure advices use +`The::advice('advisorId')` instead. Emitted by InterceptorListGenerator from +GeneratedInterceptor descriptors (string advisor ids are rejected). ### Key invariants - Proxy re-inherits parent+interfaces via reflection (not from woven source) diff --git a/src/Instrument/Transformer/WeavingTransformer.php b/src/Instrument/Transformer/WeavingTransformer.php index eefd79bf..e991311b 100644 --- a/src/Instrument/Transformer/WeavingTransformer.php +++ b/src/Instrument/Transformer/WeavingTransformer.php @@ -250,7 +250,7 @@ private function getPositionAfterAttributeGroups(ClassLike $classNode): ?int * - Renames the class to $newClassName (__AopProxied suffix) * - Removes the 'extends X' and 'implements Y, Z' clauses (moved to the proxy class) * - * @param array>> $advices List of class advices (sorted advice IDs) + * @param array>> $advices List of class advices */ private function convertClassToTrait( ReflectionClass $class, @@ -449,7 +449,7 @@ private function removeAdjacentAttributeComma(int $start, int $end, StreamMetaDa * - Removes the backed type (': string' / ': int') and any 'implements ...' clause * - Removes all enum case declarations from the body (cases live in the proxy enum instead) * - * @param array>> $advices List of class advices (sorted advice IDs) + * @param array>> $advices List of class advices */ private function convertEnumToTrait( ReflectionClass $class, @@ -538,7 +538,7 @@ private function convertEnumToTrait( * parent method → fatal error. We strip the attribute only from methods that will be aliased * (those with dynamic or static method advices). * - * @param array>> $advices + * @param array>> $advices */ private function stripOverrideAttributeFromInterceptedMethods( ReflectionClass $class, @@ -676,7 +676,7 @@ private function stripOverrideAttributeFromInterceptedMethods( * The proxy class re-declares these properties with native PHP 8.4 hooks. Tokens are neutralised * (not deleted) to preserve original line numbers for debugger mapping. * - * @param array>> $advices + * @param array>> $advices */ private function commentOutInterceptedPropertiesInTraitBody( ReflectionClass $class, diff --git a/src/Proxy/ClassProxyGenerator.php b/src/Proxy/ClassProxyGenerator.php index 10b01f5f..138995af 100644 --- a/src/Proxy/ClassProxyGenerator.php +++ b/src/Proxy/ClassProxyGenerator.php @@ -13,7 +13,17 @@ namespace Go\Proxy; use Go\Aop\Framework\AbstractMethodInvocation; +use Go\Aop\Framework\GeneratedInterceptor; +use Go\Aop\Framework\Interceptor; +use Go\Aop\Framework\InterceptorInjector; +use Go\Aop\Framework\The; use Go\Aop\InitializationAware; +use Go\Aop\Intercept\ClassJoinpoint; +use Go\Aop\Intercept\ConstructorInvocation; +use Go\Aop\Intercept\DynamicMethodInvocation; +use Go\Aop\Intercept\FieldAccess; +use Go\Aop\Intercept\FieldAccessType; +use Go\Aop\Intercept\StaticMethodInvocation; use Go\Aop\Proxy; use Go\Aop\StaticInitializationAware; use Go\Core\AspectContainer; @@ -21,6 +31,7 @@ use Go\Proxy\Generator\ClassGenerator; use Go\Proxy\Generator\DocBlockGenerator; use Go\Proxy\Generator\GeneratorInterface; +use Go\Proxy\Generator\InterceptorListGenerator; use Go\Proxy\Generator\MethodGenerator; use Go\Proxy\Generator\ParameterGenerator; use Go\Proxy\Generator\TypeGenerator; @@ -42,7 +53,7 @@ class ClassProxyGenerator /** * List of advices that are used for generation of child * - * @var string[][][] + * @var array>> */ protected array $adviceNames = []; @@ -61,7 +72,7 @@ class ClassProxyGenerator * * @param ReflectionClass $originalClass Original class reflection (before transformation) * @param string $traitName FQCN of the generated trait (e.g. Ns\Foo__AopProxied) - * @param string[][][] $classAdviceNames List of advices for class + * @param array>> $classAdviceNames List of advices for class */ public function __construct( ReflectionClass $originalClass, @@ -75,11 +86,17 @@ public function __construct( $propertyAdvices = $classAdviceNames[AspectContainer::PROPERTY_PREFIX] ?? []; $interceptedMethods = array_keys($dynamicMethodAdvices + $staticMethodAdvices); $interceptedProperties = array_keys($propertyAdvices); - $introducedInterfaces = $classAdviceNames[AspectContainer::INTRODUCTION_INTERFACE_PREFIX]['root'] ?? []; - $introducedTraits = $classAdviceNames[AspectContainer::INTRODUCTION_TRAIT_PREFIX]['root'] ?? []; - - $staticInitializationAdvices = array_values($classAdviceNames[AspectContainer::STATIC_INIT_PREFIX]['root'] ?? []); - $initializationAdvices = array_values($classAdviceNames[AspectContainer::INIT_PREFIX]['root'] ?? []); + $introducedInterfaces = array_values(array_filter( + $classAdviceNames[AspectContainer::INTRODUCTION_INTERFACE_PREFIX]['root'] ?? [], + is_string(...) + )); + $introducedTraits = array_values(array_filter( + $classAdviceNames[AspectContainer::INTRODUCTION_TRAIT_PREFIX]['root'] ?? [], + is_string(...) + )); + + $staticInitializationAdvices = $classAdviceNames[AspectContainer::STATIC_INIT_PREFIX]['root'] ?? []; + $initializationAdvices = $classAdviceNames[AspectContainer::INIT_PREFIX]['root'] ?? []; $generatedProperties = []; $generatedMethods = $this->interceptMethods($originalClass, $interceptedMethods); @@ -169,28 +186,35 @@ public function __construct( $classGenerator->addTraitAlias($effectiveTraitName, $methodName, AbstractMethodInvocation::TRAIT_ALIAS_PREFIX . $methodName, ReflectionMethod::IS_PRIVATE); } // Add any AOP-introduced traits - $classGenerator->addTraits(array_values($introducedTraits)); + $classGenerator->addTraits($introducedTraits); // Register use-imports for AOP classes referenced in generated method bodies. // Determine needed invocation types from actual method signatures, not advice // category keys, because callers may place static-method advices under METHOD_PREFIX. - $classGenerator->addUse('Go\Aop\Framework\InterceptorInjector'); + $classGenerator->addUse(InterceptorInjector::class); + $classGenerator->addUse(Interceptor::class); + $classGenerator->addUse(The::class); + foreach ($this->collectAspectClasses($classAdviceNames) as $aspectClass) { + if (str_contains($aspectClass, '\\')) { + $classGenerator->addUse($aspectClass); + } + } foreach ($interceptedMethods as $methodName) { if ($originalClass->hasMethod($methodName) && $originalClass->getMethod($methodName)->isStatic()) { - $classGenerator->addUse('Go\Aop\Intercept\StaticMethodInvocation'); + $classGenerator->addUse(StaticMethodInvocation::class); } else { - $classGenerator->addUse('Go\Aop\Intercept\DynamicMethodInvocation'); + $classGenerator->addUse(DynamicMethodInvocation::class); } } if ($staticInitializationAdvices !== []) { - $classGenerator->addUse('Go\Aop\Intercept\ClassJoinpoint'); + $classGenerator->addUse(ClassJoinpoint::class); } if ($initializationAdvices !== []) { - $classGenerator->addUse('Go\Aop\Intercept\ConstructorInvocation'); + $classGenerator->addUse(ConstructorInvocation::class); } if (!empty($propertyAdvices)) { - $classGenerator->addUse('Go\Aop\Intercept\FieldAccess'); - $classGenerator->addUse('Go\Aop\Intercept\FieldAccessType'); + $classGenerator->addUse(FieldAccess::class); + $classGenerator->addUse(FieldAccessType::class); } $this->generator = $classGenerator; @@ -212,7 +236,7 @@ public function addUse(string $use, ?string $useAlias = null): void public function generate(): string { $classCode = $this->generator->generate(); - $staticInitializationAdvices = array_values($this->adviceNames[AspectContainer::STATIC_INIT_PREFIX]['root'] ?? []); + $staticInitializationAdvices = $this->adviceNames[AspectContainer::STATIC_INIT_PREFIX]['root'] ?? []; if ($staticInitializationAdvices !== []) { $classCode .= "\n" . $this->generator->getName() . '::__aop__staticInitialization();'; @@ -244,7 +268,7 @@ protected function interceptMethods(ReflectionClass $originalClass, array $metho /** * @param ReflectionClass $originalClass - * @param array> $propertyAdvices + * @param array> $propertyAdvices * @param string[] $propertyNames Intercepted property names from advice map * * @return InterceptedPropertyGenerator[] @@ -261,7 +285,7 @@ private function interceptProperties(ReflectionClass $originalClass, array $prop if (!isset($targetProperties[$property->getName()])) { continue; } - $adviceNames = array_values($propertyAdvices[$property->getName()] ?? []); + $adviceNames = $propertyAdvices[$property->getName()] ?? []; if ($adviceNames === []) { continue; } @@ -303,9 +327,7 @@ protected function getJoinpointInvocationBody(ReflectionMethod $method, ?Reflect $adviceNames = $this->adviceNames[$prefix][$method->name] ?? ($isStatic ? ($this->adviceNames[AspectContainer::METHOD_PREFIX][$method->name] ?? []) : []); - $advicesArrayValue = new ValueGenerator($adviceNames); - $advicesArrayValue->setArrayDepth(1); - $advicesCode = $advicesArrayValue->generate(); + $advicesCode = (new InterceptorListGenerator($adviceNames))->generate(); $returnTypeString = $method->hasReturnType() ? ', ' . TypeGenerator::renderTypeForPhpDoc($method->getReturnType()) : ''; // On PHP 8.5+, ReflectionNamedType::getName() resolves 'self'/'parent' to the actual FQCN. // Use the raw AST return-type node when available (goaop/parser-reflection) to preserve keywords. @@ -333,19 +355,24 @@ protected function getJoinpointInvocationBody(ReflectionMethod $method, ?Reflect $hasTraitAlias = $originalClass !== null && ($method->class === $originalClass->name); if ($hasTraitAlias) { $callableExpression = $isStatic - ? ', self::' . AbstractMethodInvocation::TRAIT_ALIAS_PREFIX . $method->name . '(...)' - : ', $this->' . AbstractMethodInvocation::TRAIT_ALIAS_PREFIX . $method->name . '(...)'; + ? 'self::' . AbstractMethodInvocation::TRAIT_ALIAS_PREFIX . $method->name . '(...)' + : '$this->' . AbstractMethodInvocation::TRAIT_ALIAS_PREFIX . $method->name . '(...)'; } else { // Inherited method (no trait alias): use parent:: first-class callable for both static and dynamic. // DynamicTraitAliasMethodInvocation uses ReflectionMethod internally, so the callable is stored // but not used for the actual dispatch. StaticTraitAliasMethodInvocation wraps it in a // forward_static_call shim to preserve late-static-binding. - $callableExpression = ', parent::' . $method->name . '(...)'; + $callableExpression = 'parent::' . $method->name . '(...)'; } $body = <<name}', {$advicesCode}{$callableExpression}); + static \$__joinPoint = InterceptorInjector::{$injectorMethod}( + self::class, + '{$method->name}', + {$advicesCode}, + {$callableExpression}, + ); {$return}\$__joinPoint->__invoke($invocationArguments); BODY; @@ -353,20 +380,21 @@ protected function getJoinpointInvocationBody(ReflectionMethod $method, ?Reflect } /** - * @param non-empty-list $advisorNames + * @param non-empty-list $advisorNames */ private function createStaticInitializationMethod(array $advisorNames): MethodGenerator { - $advicesValue = new ValueGenerator($advisorNames); - $advicesValue->setArrayDepth(1); - $advicesCode = $advicesValue->generate(); + $advicesCode = (new InterceptorListGenerator($advisorNames))->generate(); $method = new MethodGenerator('__aop__staticInitialization'); $method->setStatic(true); $method->setReturnType('void'); $method->setBody(<< \$__joinPoint */ - static \$__joinPoint = InterceptorInjector::forStaticInitialization(self::class, {$advicesCode}); + static \$__joinPoint = InterceptorInjector::forStaticInitialization( + self::class, + {$advicesCode}, + ); \$__joinPoint(static::class); BODY); @@ -374,13 +402,11 @@ private function createStaticInitializationMethod(array $advisorNames): MethodGe } /** - * @param non-empty-list $advisorNames + * @param non-empty-list $advisorNames */ private function createInitializationMethod(array $advisorNames): MethodGenerator { - $advicesValue = new ValueGenerator($advisorNames); - $advicesValue->setArrayDepth(1); - $advicesCode = $advicesValue->generate(); + $advicesCode = (new InterceptorListGenerator($advisorNames))->generate(); $method = new MethodGenerator('__aop__initialization'); $method->setStatic(true); @@ -395,11 +421,34 @@ private function createInitializationMethod(array $advisorNames): MethodGenerato $method->addParameter($argumentsParameter); $method->setBody(<< \$__joinPoint */ - static \$__joinPoint = InterceptorInjector::forInitialization(self::class, {$advicesCode}); + static \$__joinPoint = InterceptorInjector::forInitialization( + self::class, + {$advicesCode}, + ); return \$__joinPoint->__invoke(\$arguments); BODY); return $method; } + /** + * @param array>> $adviceNames + * @return list + */ + protected function collectAspectClasses(array $adviceNames): array + { + $interceptors = []; + foreach ($adviceNames as $typedAdvices) { + foreach ($typedAdvices as $concreteAdvices) { + foreach ($concreteAdvices as $advice) { + if ($advice instanceof GeneratedInterceptor) { + $interceptors[] = $advice; + } + } + } + } + + return InterceptorListGenerator::aspectClasses($interceptors); + } + } diff --git a/src/Proxy/EnumProxyGenerator.php b/src/Proxy/EnumProxyGenerator.php index 0c8c7f31..771c3b90 100644 --- a/src/Proxy/EnumProxyGenerator.php +++ b/src/Proxy/EnumProxyGenerator.php @@ -13,11 +13,17 @@ namespace Go\Proxy; use Go\Aop\Framework\AbstractMethodInvocation; +use Go\Aop\Framework\GeneratedInterceptor; +use Go\Aop\Framework\Interceptor; +use Go\Aop\Framework\InterceptorInjector; +use Go\Aop\Framework\The; +use Go\Aop\Intercept\DynamicMethodInvocation; +use Go\Aop\Intercept\StaticMethodInvocation; use Go\Aop\Proxy; use Go\Core\AspectContainer; use Go\Proxy\Generator\EnumGenerator; +use Go\Proxy\Generator\InterceptorListGenerator; use Go\Proxy\Generator\TypeGenerator; -use Go\Proxy\Generator\ValueGenerator; use Go\Proxy\Part\FunctionCallArgumentListGenerator; use PhpParser\Node\Expr; use PhpParser\Node\Stmt\ClassMethod; @@ -74,7 +80,7 @@ class EnumProxyGenerator extends ClassProxyGenerator * * @param ReflectionClass $originalClass Original enum reflection (before transformation) * @param string $traitName FQCN of the generated trait (e.g. Ns\Foo__AopProxied) - * @param string[][][] $classAdviceNames List of advices for enum + * @param array>> $classAdviceNames List of advices for enum */ public function __construct( ReflectionClass $originalClass, @@ -160,12 +166,19 @@ public function __construct( // Register use-imports for AOP classes referenced in generated method bodies. // Determine needed invocation types from actual method signatures, not advice // category keys, because callers may place static-method advices under METHOD_PREFIX. - $enumGenerator->addUse('Go\Aop\Framework\InterceptorInjector'); + $enumGenerator->addUse(InterceptorInjector::class); + $enumGenerator->addUse(Interceptor::class); + $enumGenerator->addUse(The::class); + foreach ($this->collectAspectClasses($classAdviceNames) as $aspectClass) { + if (str_contains($aspectClass, '\\')) { + $enumGenerator->addUse($aspectClass); + } + } foreach ($interceptedMethods as $methodName) { if ($originalClass->hasMethod($methodName) && $originalClass->getMethod($methodName)->isStatic()) { - $enumGenerator->addUse('Go\Aop\Intercept\StaticMethodInvocation'); + $enumGenerator->addUse(StaticMethodInvocation::class); } else { - $enumGenerator->addUse('Go\Aop\Intercept\DynamicMethodInvocation'); + $enumGenerator->addUse(DynamicMethodInvocation::class); } } @@ -222,9 +235,7 @@ protected function getJoinpointInvocationBody(ReflectionMethod $method, ?Reflect $adviceNames = $this->adviceNames[$prefix][$method->name] ?? ($isStatic ? ($this->adviceNames[AspectContainer::METHOD_PREFIX][$method->name] ?? []) : []); - $advicesArrayValue = new ValueGenerator($adviceNames); - $advicesArrayValue->setArrayDepth(1); - $advicesCode = $advicesArrayValue->generate(); + $advicesCode = (new InterceptorListGenerator($adviceNames))->generate(); $returnTypeString = $method->hasReturnType() ? ', ' . TypeGenerator::renderTypeForPhpDoc($method->getReturnType()) : ''; // On PHP 8.5+, ReflectionNamedType::getName() resolves 'self'/'parent' to the actual FQCN. // Use the raw AST return-type node when available (goaop/parser-reflection) to preserve keywords. @@ -246,7 +257,12 @@ protected function getJoinpointInvocationBody(ReflectionMethod $method, ?Reflect return <<name}', {$advicesCode}, {$callableExpression}); + static \$__joinPoint = InterceptorInjector::{$injectorMethod}( + self::class, + '{$method->name}', + {$advicesCode}, + {$callableExpression}, + ); {$return}\$__joinPoint->__invoke($argumentCode); BODY; } diff --git a/src/Proxy/FunctionProxyGenerator.php b/src/Proxy/FunctionProxyGenerator.php index 34cf2bd6..8625539f 100644 --- a/src/Proxy/FunctionProxyGenerator.php +++ b/src/Proxy/FunctionProxyGenerator.php @@ -12,12 +12,17 @@ namespace Go\Proxy; +use Go\Aop\Framework\GeneratedInterceptor; +use Go\Aop\Framework\Interceptor; +use Go\Aop\Framework\InterceptorInjector; +use Go\Aop\Framework\The; +use Go\Aop\Intercept\FunctionInvocation; use Go\Core\AspectContainer; use Go\ParserReflection\ReflectionFileNamespace; use Go\Proxy\Generator\FileGenerator; use Go\Proxy\Generator\FunctionGenerator; +use Go\Proxy\Generator\InterceptorListGenerator; use Go\Proxy\Generator\TypeGenerator; -use Go\Proxy\Generator\ValueGenerator; use Go\Proxy\Part\FunctionCallArgumentListGenerator; use ReflectionException; use ReflectionFunction; @@ -31,7 +36,7 @@ class FunctionProxyGenerator /** * List of advices that are used for generation of child * - * @var string[][][] + * @var array>> */ protected array $adviceNames = []; @@ -44,7 +49,7 @@ class FunctionProxyGenerator * Constructs functions stub class from namespace Reflection * * @param ReflectionFileNamespace $namespace Reflection of namespace - * @param string[][][] $adviceNames List of function advices + * @param array>> $adviceNames List of function advices * * @throws ReflectionException If there is an advice for unknown function */ @@ -55,8 +60,15 @@ public function __construct( $this->adviceNames = $adviceNames; $this->fileGenerator = new FileGenerator(); $this->fileGenerator->setNamespace($namespace->getName()); - $this->fileGenerator->addUse('Go\Aop\Framework\InterceptorInjector'); - $this->fileGenerator->addUse('Go\Aop\Intercept\FunctionInvocation'); + $this->fileGenerator->addUse(InterceptorInjector::class); + $this->fileGenerator->addUse(Interceptor::class); + $this->fileGenerator->addUse(The::class); + $this->fileGenerator->addUse(FunctionInvocation::class); + foreach ($this->collectAspectClasses($adviceNames) as $aspectClass) { + if (str_contains($aspectClass, '\\')) { + $this->fileGenerator->addUse($aspectClass); + } + } $functionsContent = []; $functionAdvices = $adviceNames[AspectContainer::FUNCTION_PREFIX] ?? []; @@ -101,9 +113,7 @@ protected function getJoinpointInvocationBody(ReflectionFunction $function): str } $functionAdvices = $this->adviceNames[AspectContainer::FUNCTION_PREFIX][$function->name]; - $advicesArray = new ValueGenerator($functionAdvices); - $advicesArray->setArrayDepth(1); - $advicesCode = $advicesArray->generate(); + $advicesCode = (new InterceptorListGenerator(array_values($functionAdvices)))->generate(); $returnTypeString = $function->hasReturnType() ? '<' . TypeGenerator::renderTypeForPhpDoc($function->getReturnType()) . '>' : ''; // Use a fully-qualified (global) callable so proceed() calls the original built-in @@ -112,8 +122,32 @@ protected function getJoinpointInvocationBody(ReflectionFunction $function): str return <<name}', {$advicesCode}, {$callableExpression}); + static \$__joinPoint = InterceptorInjector::forFunction( + '{$function->name}', + {$advicesCode}, + {$callableExpression}, + ); {$return}\$__joinPoint->__invoke($argumentCode); BODY; } + + /** + * @param array>> $adviceNames + * @return list + */ + private function collectAspectClasses(array $adviceNames): array + { + $interceptors = []; + foreach ($adviceNames as $typedAdvices) { + foreach ($typedAdvices as $concreteAdvices) { + foreach ($concreteAdvices as $advice) { + if ($advice instanceof GeneratedInterceptor) { + $interceptors[] = $advice; + } + } + } + } + + return InterceptorListGenerator::aspectClasses($interceptors); + } } diff --git a/src/Proxy/Generator/ClassGenerator.php b/src/Proxy/Generator/ClassGenerator.php index 61702992..c7935c7e 100644 --- a/src/Proxy/Generator/ClassGenerator.php +++ b/src/Proxy/Generator/ClassGenerator.php @@ -274,7 +274,7 @@ private function mapVisibility(int $visibility): int private static function getPrinter(): Standard { if (self::$printer === null) { - self::$printer = new Standard(['shortArraySyntax' => true]); + self::$printer = new GeneratedCodePrinter(['shortArraySyntax' => true]); } return self::$printer; } diff --git a/src/Proxy/Generator/EnumGenerator.php b/src/Proxy/Generator/EnumGenerator.php index 000960d1..7be0abbc 100644 --- a/src/Proxy/Generator/EnumGenerator.php +++ b/src/Proxy/Generator/EnumGenerator.php @@ -249,7 +249,7 @@ private function mapVisibility(int $visibility): int private static function getPrinter(): Standard { if (self::$printer === null) { - self::$printer = new Standard(['shortArraySyntax' => true]); + self::$printer = new GeneratedCodePrinter(['shortArraySyntax' => true]); } return self::$printer; } diff --git a/src/Proxy/Generator/FunctionGenerator.php b/src/Proxy/Generator/FunctionGenerator.php index 1873dcf5..d3caf867 100644 --- a/src/Proxy/Generator/FunctionGenerator.php +++ b/src/Proxy/Generator/FunctionGenerator.php @@ -215,7 +215,7 @@ public function generate(): string private static function getPrinter(): Standard { if (self::$printer === null) { - self::$printer = new Standard(['shortArraySyntax' => true]); + self::$printer = new GeneratedCodePrinter(['shortArraySyntax' => true]); } return self::$printer; } diff --git a/src/Proxy/Generator/GeneratedCodePrinter.php b/src/Proxy/Generator/GeneratedCodePrinter.php new file mode 100644 index 00000000..e3e5b912 --- /dev/null +++ b/src/Proxy/Generator/GeneratedCodePrinter.php @@ -0,0 +1,71 @@ + + * + * This source file is subject to the license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Go\Proxy\Generator; + +use PhpParser\Node\Expr; +use PhpParser\Node\Identifier; +use PhpParser\Node\Name; +use PhpParser\PrettyPrinter\Standard; + +/** + * Pretty-printer for generated proxy code. + * + * Keeps joinpoint initialization readable even when method bodies are parsed + * through AST nodes before class generation. + */ +final class GeneratedCodePrinter extends Standard +{ + protected function pExpr_Array(Expr\Array_ $node): string + { + if (empty($node->items)) { + return $node->getAttribute('kind') === Expr\Array_::KIND_SHORT ? '[]' : 'array()'; + } + if (!$this->isInterceptorArray($node)) { + return parent::pExpr_Array($node); + } + + $isShort = $node->getAttribute('kind') === Expr\Array_::KIND_SHORT; + + return ($isShort ? '[' : 'array(') + . $this->pCommaSeparatedMultiline($node->items, true) + . $this->nl + . ($isShort ? ']' : ')'); + } + + protected function pExpr_StaticCall(Expr\StaticCall $node): string + { + if ($node->class instanceof Name && str_ends_with($node->class->toString(), 'InterceptorInjector')) { + $name = $node->name instanceof Identifier ? $node->name->toString() : $this->p($node->name); + + return $this->pStaticDereferenceLhs($node->class) . '::' . $name + . '(' . $this->pCommaSeparatedMultiline($node->args, true) . $this->nl . ')'; + } + + return parent::pExpr_StaticCall($node); + } + + private function isInterceptorArray(Expr\Array_ $node): bool + { + foreach ($node->items as $item) { + if (!$item->value instanceof Expr\StaticCall) { + return false; + } + $call = $item->value; + if (!$call->class instanceof Name || !str_ends_with($call->class->toString(), 'Interceptor')) { + return false; + } + } + + return true; + } +} diff --git a/src/Proxy/Generator/InterceptorListGenerator.php b/src/Proxy/Generator/InterceptorListGenerator.php new file mode 100644 index 00000000..a90a3c2b --- /dev/null +++ b/src/Proxy/Generator/InterceptorListGenerator.php @@ -0,0 +1,149 @@ + + * + * This source file is subject to the license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Go\Proxy\Generator; + +use Go\Aop\AspectException; +use Go\Aop\Framework\GeneratedInterceptor; +use PhpParser\Node\Arg; +use PhpParser\Node\ArrayItem; +use PhpParser\Node\Expr; +use PhpParser\Node\Expr\Array_; +use PhpParser\Node\Expr\ClassConstFetch; +use PhpParser\Node\Expr\MethodCall; +use PhpParser\Node\Expr\StaticCall; +use PhpParser\Node\Identifier; +use PhpParser\Node\Name; +use PhpParser\Node\Scalar\Int_; +use PhpParser\Node\Scalar\String_; +use PhpParser\Node\VariadicPlaceholder; + +/** + * Renders generated interceptor descriptors as Interceptor::* factory calls. + * + * @internal + */ +final class InterceptorListGenerator +{ + /** + * Default continuation-line indentation for the rendered interceptor list. + * + * The list is embedded as an argument of the `InterceptorInjector::for*()` call inside a + * generated proxy method body, so every line after the opening bracket sits three levels + * deep: method body (2 levels) plus the injector call arguments (1 level), 4 spaces each. + */ + private const string JOINPOINT_ARGUMENT_INDENT = ' '; + + /** + * @var list + */ + private readonly array $interceptors; + + /** + * @param array $interceptors Only generated interceptor descriptors are + * accepted, string entries are rejected loudly + */ + public function __construct(array $interceptors) + { + $descriptors = []; + foreach ($interceptors as $interceptor) { + if (!$interceptor instanceof GeneratedInterceptor) { + throw new AspectException( + 'Interceptor list expects generated interceptor descriptors, got ' . get_debug_type($interceptor) + ); + } + $descriptors[] = $interceptor; + } + $this->interceptors = $descriptors; + } + + /** + * @param list $interceptors + * @return list + */ + public static function aspectClasses(array $interceptors): array + { + $classes = []; + foreach ($interceptors as $interceptor) { + if ($interceptor->aspectClass === null) { + continue; + } + $classes[$interceptor->aspectClass] = $interceptor->aspectClass; + } + + return array_values($classes); + } + + public function generate(string $indent = self::JOINPOINT_ARGUMENT_INDENT): string + { + if ($this->interceptors === []) { + return '[]'; + } + + $printed = (new GeneratedCodePrinter(['shortArraySyntax' => true]))->prettyPrintExpr($this->getNode()); + + return str_replace("\n", "\n" . $indent, $printed); + } + + public function getNode(): Array_ + { + return new Array_(array_map( + static fn(GeneratedInterceptor $interceptor): ArrayItem => new ArrayItem(self::createCallNode($interceptor)), + $this->interceptors + ), ['kind' => Array_::KIND_SHORT]); + } + + private static function createCallNode(GeneratedInterceptor $interceptor): StaticCall + { + $args = [ + new Arg(self::createAdviceAccessorNode($interceptor)), + ]; + + if ($interceptor->order !== 0) { + $args[] = new Arg(new Int_($interceptor->order), name: new Identifier('order')); + } + + return new StaticCall(new Name('Interceptor'), $interceptor->factoryMethod, $args); + } + + private static function createAdviceAccessorNode(GeneratedInterceptor $interceptor): Expr + { + if ($interceptor->usesContainerAdvice) { + return new StaticCall( + new Name('The'), + 'advice', + [ + new Arg(new String_($interceptor->advisorId)), + ] + ); + } + + if ($interceptor->aspectClass === null || $interceptor->adviceMethod === null) { + throw new \LogicException('Aspect-backed interceptor descriptor is incomplete'); + } + + return new MethodCall( + new StaticCall(new Name('The'), 'aspect', [ + new Arg(new ClassConstFetch(new Name(self::shortClassName($interceptor->aspectClass)), 'class')), + ]), + $interceptor->adviceMethod, + [new VariadicPlaceholder()] + ); + } + + private static function shortClassName(string $className): string + { + $lastSeparator = strrpos($className, '\\'); + + return $lastSeparator === false ? $className : substr($className, $lastSeparator + 1); + } +} diff --git a/src/Proxy/Generator/MethodGenerator.php b/src/Proxy/Generator/MethodGenerator.php index 6e4248cc..d5dbc1d5 100644 --- a/src/Proxy/Generator/MethodGenerator.php +++ b/src/Proxy/Generator/MethodGenerator.php @@ -307,7 +307,7 @@ public function generate(): string private static function getPrinter(): Standard { if (self::$printer === null) { - self::$printer = new Standard(['shortArraySyntax' => true]); + self::$printer = new GeneratedCodePrinter(['shortArraySyntax' => true]); } return self::$printer; } diff --git a/src/Proxy/Generator/TraitGenerator.php b/src/Proxy/Generator/TraitGenerator.php index bb8a6dea..36499467 100644 --- a/src/Proxy/Generator/TraitGenerator.php +++ b/src/Proxy/Generator/TraitGenerator.php @@ -193,7 +193,7 @@ private function mapVisibility(int $visibility): int private static function getPrinter(): Standard { if (self::$printer === null) { - self::$printer = new Standard(['shortArraySyntax' => true]); + self::$printer = new GeneratedCodePrinter(['shortArraySyntax' => true]); } return self::$printer; } diff --git a/src/Proxy/Part/InterceptedPropertyGenerator.php b/src/Proxy/Part/InterceptedPropertyGenerator.php index 1fc9dcbf..6bcc9385 100644 --- a/src/Proxy/Part/InterceptedPropertyGenerator.php +++ b/src/Proxy/Part/InterceptedPropertyGenerator.php @@ -12,13 +12,12 @@ namespace Go\Proxy\Part; -use Go\Aop\Framework\InterceptorInjector; +use Go\Aop\Framework\GeneratedInterceptor; use Go\Aop\Intercept\FieldAccessType; +use Go\Proxy\Generator\InterceptorListGenerator; use Go\Proxy\Generator\PropertyNodeProvider; use PhpParser\Node\Arg; -use PhpParser\Node\Expr\Array_; use PhpParser\Node\Expr\Assign; -use PhpParser\Node\ArrayItem; use PhpParser\Node\Expr\ClassConstFetch; use PhpParser\Node\Expr\MethodCall; use PhpParser\Node\Expr\PropertyFetch; @@ -81,7 +80,7 @@ final class InterceptedPropertyGenerator extends AbstractInterceptedPropertyGenerator implements PropertyNodeProvider { /** - * @param list $adviceNames + * @param list $adviceNames */ public function __construct( ReflectionProperty $property, @@ -280,10 +279,7 @@ private function createFieldAccessInitializationExpression(string $propertyName) [ new Arg(new ClassConstFetch(new Name('self'), 'class')), new Arg(new String_($propertyName)), - new Arg(new Array_(array_map( - static fn (string $adviceName): ArrayItem => new ArrayItem(new String_($adviceName)), - $this->adviceNames - ))), + new Arg((new InterceptorListGenerator($this->adviceNames))->getNode()), ] ); } diff --git a/src/Proxy/Part/TraitInterceptedPropertyGenerator.php b/src/Proxy/Part/TraitInterceptedPropertyGenerator.php index 06d35b66..e2feef23 100644 --- a/src/Proxy/Part/TraitInterceptedPropertyGenerator.php +++ b/src/Proxy/Part/TraitInterceptedPropertyGenerator.php @@ -12,13 +12,12 @@ namespace Go\Proxy\Part; -use Go\Aop\Framework\InterceptorInjector; +use Go\Aop\Framework\GeneratedInterceptor; use Go\Aop\Intercept\FieldAccessType; +use Go\Proxy\Generator\InterceptorListGenerator; use Go\Proxy\Generator\PropertyNodeProvider; use PhpParser\Node; use PhpParser\Node\Arg; -use PhpParser\Node\Expr\Array_; -use PhpParser\Node\ArrayItem; use PhpParser\Node\Expr\Assign; use PhpParser\Node\Expr\ClassConstFetch; use PhpParser\Node\Expr\MethodCall; @@ -46,7 +45,7 @@ final class TraitInterceptedPropertyGenerator extends AbstractInterceptedPropertyGenerator implements PropertyNodeProvider { /** - * @param list $adviceNames + * @param list $adviceNames */ public function __construct( ReflectionProperty $property, @@ -160,10 +159,7 @@ private function getFieldAccessInitializationStatements(): array [ new Arg(new ClassConstFetch(new Name('self'), 'class')), new Arg(new String_($propertyName)), - new Arg(new Array_(array_map( - static fn (string $adviceName): ArrayItem => new ArrayItem(new String_($adviceName)), - $this->adviceNames - ))), + new Arg((new InterceptorListGenerator($this->adviceNames))->getNode()), ] ); diff --git a/src/Proxy/TraitProxyGenerator.php b/src/Proxy/TraitProxyGenerator.php index cb4c5f5e..003c3e0a 100644 --- a/src/Proxy/TraitProxyGenerator.php +++ b/src/Proxy/TraitProxyGenerator.php @@ -13,11 +13,19 @@ namespace Go\Proxy; use Go\Aop\Framework\AbstractMethodInvocation; +use Go\Aop\Framework\GeneratedInterceptor; +use Go\Aop\Framework\Interceptor; +use Go\Aop\Framework\InterceptorInjector; +use Go\Aop\Framework\The; +use Go\Aop\Intercept\DynamicMethodInvocation; +use Go\Aop\Intercept\FieldAccess; +use Go\Aop\Intercept\FieldAccessType; +use Go\Aop\Intercept\StaticMethodInvocation; use Go\Core\AspectContainer; use Go\Proxy\Generator\DocBlockGenerator; +use Go\Proxy\Generator\InterceptorListGenerator; use Go\Proxy\Generator\TraitGenerator; use Go\Proxy\Generator\TypeGenerator; -use Go\Proxy\Generator\ValueGenerator; use Go\Proxy\Part\FunctionCallArgumentListGenerator; use Go\Proxy\Part\TraitInterceptedPropertyGenerator; use PhpParser\Node\Stmt\ClassMethod; @@ -35,7 +43,7 @@ class TraitProxyGenerator extends ClassProxyGenerator * * @param ReflectionClass $originalTrait Original class reflection * @param string $parentTraitName Parent trait name to use - * @param string[][][] $traitAdviceNames List of advices for class + * @param array>> $traitAdviceNames List of advices for class */ public function __construct( ReflectionClass $originalTrait, @@ -51,8 +59,7 @@ public function __construct( $generatedProperties = []; foreach ($traitAdviceNames[AspectContainer::PROPERTY_PREFIX] ?? [] as $propertyName => $adviceNames) { $property = $originalTrait->getProperty($propertyName); - $normalizedAdviceNames = array_is_list($adviceNames) ? $adviceNames : array_keys($adviceNames); - $generatedProperties[] = (new TraitInterceptedPropertyGenerator($property, $normalizedAdviceNames))->getNode(); + $generatedProperties[] = (new TraitInterceptedPropertyGenerator($property, $adviceNames))->getNode(); } $docComment = $originalTrait->getDocComment(); @@ -86,18 +93,25 @@ public function __construct( // Register use-imports for AOP classes referenced in generated method bodies. // Determine needed invocation types from actual method signatures, not advice // category keys, because callers may place static-method advices under METHOD_PREFIX. - $traitGenerator->addUse('Go\Aop\Framework\InterceptorInjector'); + $traitGenerator->addUse(InterceptorInjector::class); + $traitGenerator->addUse(Interceptor::class); + $traitGenerator->addUse(The::class); + foreach ($this->collectAspectClasses($traitAdviceNames) as $aspectClass) { + if (str_contains($aspectClass, '\\')) { + $traitGenerator->addUse($aspectClass); + } + } foreach ($interceptedMethods as $methodName) { if ($originalTrait->hasMethod($methodName) && $originalTrait->getMethod($methodName)->isStatic()) { - $traitGenerator->addUse('Go\Aop\Intercept\StaticMethodInvocation'); + $traitGenerator->addUse(StaticMethodInvocation::class); } else { - $traitGenerator->addUse('Go\Aop\Intercept\DynamicMethodInvocation'); + $traitGenerator->addUse(DynamicMethodInvocation::class); } } $propertyAdvices = $traitAdviceNames[AspectContainer::PROPERTY_PREFIX] ?? []; if (!empty($propertyAdvices)) { - $traitGenerator->addUse('Go\Aop\Intercept\FieldAccess'); - $traitGenerator->addUse('Go\Aop\Intercept\FieldAccessType'); + $traitGenerator->addUse(FieldAccess::class); + $traitGenerator->addUse(FieldAccessType::class); } // Store generator instance for compatibility with parent generate() call @@ -132,9 +146,7 @@ protected function getJoinpointInvocationBody(ReflectionMethod $method, ?Reflect $adviceNames = $this->adviceNames[$prefix][$method->name] ?? ($isStatic ? ($this->adviceNames[AspectContainer::METHOD_PREFIX][$method->name] ?? []) : []); - $advicesArrayValue = new ValueGenerator($adviceNames); - $advicesArrayValue->setArrayDepth(1); - $advicesCode = $advicesArrayValue->generate(); + $advicesCode = (new InterceptorListGenerator($adviceNames))->generate(); $returnTypeString = $method->hasReturnType() ? ', ' . TypeGenerator::renderTypeForPhpDoc($method->getReturnType()) : ''; // On PHP 8.5+, ReflectionNamedType::getName() resolves 'self'/'parent' to the actual FQCN. // Use the raw AST return-type node when available (goaop/parser-reflection) to preserve keywords. @@ -156,7 +168,12 @@ protected function getJoinpointInvocationBody(ReflectionMethod $method, ?Reflect return <<name}', {$advicesCode}, {$callableExpression}); + static \$__joinPoint = InterceptorInjector::{$injectorMethod}( + self::class, + '{$method->name}', + {$advicesCode}, + {$callableExpression}, + ); {$return}\$__joinPoint->__invoke($argumentCode); BODY; } diff --git a/tests/Aop/Framework/AbstractJoinpointTest.php b/tests/Aop/Framework/AbstractJoinpointTest.php index 0454bfd6..e20e93f3 100644 --- a/tests/Aop/Framework/AbstractJoinpointTest.php +++ b/tests/Aop/Framework/AbstractJoinpointTest.php @@ -4,9 +4,9 @@ namespace Go\Aop\Framework; -use Go\Aop\AdviceAfter; -use Go\Aop\AdviceAround; -use Go\Aop\AdviceBefore; +use Go\Aop\Advice; +use Go\Aop\AdviceTypeEnum; +use Go\Aop\AspectException; use Go\Aop\OrderedAdvice; use PHPUnit\Framework\TestCase; @@ -24,15 +24,20 @@ public function testSortingLogic(array $advices, array $order = []): void $advices = AbstractJoinpoint::sortAdvices($advices); foreach ($advices as $advice) { $expected = array_shift($order); - $this->assertInstanceOf($expected, $advice); + if ($expected instanceof AdviceTypeEnum) { + $this->assertInstanceOf(Advice::class, $advice); + $this->assertSame($expected, $advice->getType()); + } else { + $this->assertInstanceOf($expected, $advice); + } } } public static function sortingTestSource(): array { - $after = new class implements AdviceAfter {}; - $before = new class implements AdviceBefore {}; - $around = new class implements AdviceAround {}; + $after = self::makeAdvice(AdviceTypeEnum::After); + $before = self::makeAdvice(AdviceTypeEnum::Before); + $around = self::makeAdvice(AdviceTypeEnum::Around); $forth = self::makeOrderedAdvice(4); $first = self::makeOrderedAdvice(1); @@ -41,37 +46,37 @@ public static function sortingTestSource(): array // #0 [ [clone $after, clone $before], - [AdviceBefore::class, AdviceAfter::class] + [AdviceTypeEnum::Before, AdviceTypeEnum::After] ], // #1 [ [clone $after, clone $around], - [AdviceAfter::class, AdviceAround::class] + [AdviceTypeEnum::After, AdviceTypeEnum::Around] ], // #2 [ [clone $before, clone $after], - [AdviceBefore::class, AdviceAfter::class] + [AdviceTypeEnum::Before, AdviceTypeEnum::After] ], // #3 [ [clone $before, clone $around], - [AdviceBefore::class, AdviceAround::class] + [AdviceTypeEnum::Before, AdviceTypeEnum::Around] ], // #4 [ [clone $around, clone $after], - [AdviceAfter::class, AdviceAround::class] + [AdviceTypeEnum::After, AdviceTypeEnum::Around] ], // #5 [ [clone $around, clone $before], - [AdviceBefore::class, AdviceAround::class] + [AdviceTypeEnum::Before, AdviceTypeEnum::Around] ], // #6 [ [clone $before, clone $around, clone $before, clone $after], - [AdviceBefore::class, AdviceBefore::class, AdviceAfter::class, AdviceAround::class] + [AdviceTypeEnum::Before, AdviceTypeEnum::Before, AdviceTypeEnum::After, AdviceTypeEnum::Around] ], // #7 [ @@ -81,6 +86,75 @@ public static function sortingTestSource(): array ]; } + public function testFlatAndSortAdvicesGeneratesDescriptorsForEveryAdviceType(): void + { + $noop = static fn(): mixed => null; + $advices = [ + 'method' => [ + 'execute' => [ + 'advisor.around' => new AroundInterceptor($noop), + 'advisor.afterThrowing' => new AfterThrowingInterceptor($noop), + 'advisor.after' => new AfterInterceptor($noop), + 'advisor.before' => new BeforeInterceptor($noop), + ], + ], + ]; + + $flattened = AbstractJoinpoint::flatAndSortAdvices($advices); + + $descriptors = $flattened['method']['execute']; + $this->assertContainsOnlyInstancesOf(GeneratedInterceptor::class, $descriptors); + $this->assertSame( + ['before', 'afterThrowing', 'after', 'around'], + array_map(static fn(GeneratedInterceptor $descriptor): string => $descriptor->factoryMethod, $descriptors) + ); + $this->assertSame( + ['advisor.before', 'advisor.afterThrowing', 'advisor.after', 'advisor.around'], + array_map(static fn(GeneratedInterceptor $descriptor): string => $descriptor->advisorId, $descriptors) + ); + } + + public function testFlatAndSortAdvicesKeepsIntroductionAdvisorIds(): void + { + $advices = [ + 'introduction' => [ + 'root' => [ + '\Some\Interface' => new TraitIntroductionInfo('\Some\Trait', '\Some\Interface'), + ], + ], + ]; + + $flattened = AbstractJoinpoint::flatAndSortAdvices($advices); + + $this->assertSame(['\Some\Interface'], $flattened['introduction']['root']); + } + + public function testFlatAndSortAdvicesRejectsNonAdviceValues(): void + { + $this->expectException(AspectException::class); + $this->expectExceptionMessage('instead of advice instance'); + + AbstractJoinpoint::flatAndSortAdvices([ + 'method' => [ + 'execute' => [ + 'advisor.broken' => true, + ], + ], + ]); + } + + private static function makeAdvice(AdviceTypeEnum $type): Advice + { + return new class($type) implements Advice { + public function __construct(private readonly AdviceTypeEnum $type) {} + + public function getType(): AdviceTypeEnum + { + return $this->type; + } + }; + } + private static function makeOrderedAdvice(int $order): OrderedAdvice { return new class($order) implements OrderedAdvice { @@ -90,6 +164,11 @@ public function getAdviceOrder(): int { return $this->order; } + + public function getType(): AdviceTypeEnum + { + return AdviceTypeEnum::Introduction; + } }; } } diff --git a/tests/Aop/Framework/BaseInterceptorTest.php b/tests/Aop/Framework/BaseInterceptorTest.php index d5e3440e..10828e7f 100644 --- a/tests/Aop/Framework/BaseInterceptorTest.php +++ b/tests/Aop/Framework/BaseInterceptorTest.php @@ -30,7 +30,7 @@ public function testReturnsRawAdvice() $interceptor = $this->getMockBuilder(AbstractInterceptor::class) ->setConstructorArgs([$advice]) - ->onlyMethods(['invoke']) + ->onlyMethods(['invoke', 'getType']) ->getMock(); $this->assertEquals($advice, $interceptor->getRawAdvice()); } @@ -63,5 +63,6 @@ public function testCanUnserializeInterceptor() $serialized = 'O:' . $mockNameLength .':"' . $mockClass . '":1:{s:12:"adviceMethod";a:3:{s:5:"scope";s:6:"aspect";s:6:"method";s:26:"Go\Aop\Framework\{closure}";s:6:"aspect";s:36:"Go\Aop\Framework\BaseInterceptorTest";}}'; $result = unserialize($serialized); $this->assertEquals($advice, $result->getRawAdvice()); + $this->assertSame(0, $result->getAdviceOrder()); } } diff --git a/tests/Aop/Framework/GeneratedInterceptorTest.php b/tests/Aop/Framework/GeneratedInterceptorTest.php new file mode 100644 index 00000000..5b9cf06a --- /dev/null +++ b/tests/Aop/Framework/GeneratedInterceptorTest.php @@ -0,0 +1,92 @@ +assertSame('before', $interceptor->factoryMethod); + $this->assertNull($interceptor->aspectClass); + $this->assertNull($interceptor->adviceMethod); + $this->assertSame(10, $interceptor->order); + $this->assertSame('manual-advisor', $interceptor->advisorId); + $this->assertTrue($interceptor->usesContainerAdvice); + } + + /** + * @param class-string $interceptorClass + */ + #[DataProvider('adviceTypeSource')] + public function testResolvesFactoryMethodFromAdviceType(string $interceptorClass, string $expectedFactoryMethod): void + { + $interceptor = GeneratedInterceptor::fromAdvice( + 'manual-advisor', + new $interceptorClass(static fn(): mixed => null) + ); + + $this->assertSame($expectedFactoryMethod, $interceptor->factoryMethod); + $this->assertTrue($interceptor->usesContainerAdvice); + $this->assertSame('manual-advisor', $interceptor->advisorId); + } + + /** + * @return array, string}> + */ + public static function adviceTypeSource(): array + { + return [ + 'before' => [BeforeInterceptor::class, AdviceTypeEnum::Before->value], + 'after' => [AfterInterceptor::class, AdviceTypeEnum::After->value], + 'around' => [AroundInterceptor::class, AdviceTypeEnum::Around->value], + 'afterThrowing' => [AfterThrowingInterceptor::class, AdviceTypeEnum::AfterThrowing->value], + ]; + } + + public function testCreatesAspectBackedDescriptorForAspectScopedAdvice(): void + { + $aspect = new AttributeAspectLoaderExtensionTestPublicAspect(); + $advice = new ReflectionMethod($aspect, 'publicAdvice')->getClosure($aspect); + + $interceptor = GeneratedInterceptor::fromAdvice( + 'advisor.' . $aspect::class . '->publicAdvice', + new BeforeInterceptor($advice) + ); + + $this->assertSame('before', $interceptor->factoryMethod); + $this->assertSame($aspect::class, $interceptor->aspectClass); + $this->assertSame('publicAdvice', $interceptor->adviceMethod); + $this->assertFalse($interceptor->usesContainerAdvice); + } + + public function testRejectsNonInterceptorAdvice(): void + { + $advice = new class implements Advice { + public function getType(): AdviceTypeEnum + { + return AdviceTypeEnum::Before; + } + }; + + $this->expectException(AspectException::class); + $this->expectExceptionMessage('unsupported advice'); + + GeneratedInterceptor::fromAdvice('manual-advisor', $advice); + } +} diff --git a/tests/Aop/Framework/InterceptorTest.php b/tests/Aop/Framework/InterceptorTest.php new file mode 100644 index 00000000..3d0bbf90 --- /dev/null +++ b/tests/Aop/Framework/InterceptorTest.php @@ -0,0 +1,41 @@ +assertInstanceOf(BeforeInterceptor::class, $interceptor); + $this->assertSame(10, $interceptor->getAdviceOrder()); + } + + public function testCreatesAfterInterceptor(): void + { + $interceptor = Interceptor::after(static function (Joinpoint $joinpoint): void {}); + + $this->assertInstanceOf(AfterInterceptor::class, $interceptor); + $this->assertSame(0, $interceptor->getAdviceOrder()); + } + + public function testCreatesAroundInterceptor(): void + { + $interceptor = Interceptor::around(static fn(Joinpoint $joinpoint): mixed => $joinpoint->proceed()); + + $this->assertInstanceOf(AroundInterceptor::class, $interceptor); + } + + public function testCreatesAfterThrowingInterceptor(): void + { + $interceptor = Interceptor::afterThrowing(static function (Joinpoint $joinpoint, \Throwable $throwable): void {}); + + $this->assertInstanceOf(AfterThrowingInterceptor::class, $interceptor); + } +} diff --git a/tests/Aop/Framework/TheTest.php b/tests/Aop/Framework/TheTest.php new file mode 100644 index 00000000..47c515d9 --- /dev/null +++ b/tests/Aop/Framework/TheTest.php @@ -0,0 +1,147 @@ +setValue(null, null); + } + + public function testReturnsRegisteredAspectInstance(): void + { + $this->initKernelWithContainerValues([]); + + $this->assertInstanceOf(TheTestAspect::class, The::aspect(TheTestAspect::class)); + } + + public function testReturnsAdviceClosureFromAdvisor(): void + { + $this->initKernelWithContainerValues([ + 'manual-advisor' => new class implements Advisor { + public Closure $advice; + + public function __construct() + { + $this->advice = static function (): void {}; + } + + public function getAdvice(): Advice + { + return new AroundInterceptor($this->advice); + } + }, + ]); + + $this->assertInstanceOf(Closure::class, The::advice('manual-advisor')); + } + + public function testReturnsAdviceClosureFromDirectInterceptor(): void + { + $advice = static function (): void {}; + $this->initKernelWithContainerValues([ + 'manual-interceptor' => new BeforeInterceptor($advice), + ]); + + $this->assertSame($advice, The::advice('manual-interceptor')); + } + + public function testReturnsDirectClosureAdvisor(): void + { + $advice = static function (): void {}; + $this->initKernelWithContainerValues([ + 'manual-closure' => $advice, + ]); + + $this->assertSame($advice, The::advice('manual-closure')); + } + + public function testFailsForMissingAdvisor(): void + { + $this->initKernelWithContainerValues([]); + + $this->expectException(OutOfBoundsException::class); + + The::advice('missing'); + } + + public function testFailsForUnsupportedAdvisorValue(): void + { + $this->initKernelWithContainerValues([ + 'manual-value' => new stdClass(), + ]); + + $this->expectException(AspectException::class); + $this->expectExceptionMessage('does not expose a closure advice'); + + The::advice('manual-value'); + } + + public function testFailsForUnsupportedAdvisorAdvice(): void + { + $this->initKernelWithContainerValues([ + 'manual-advisor' => new class implements Advisor { + public function getAdvice(): Advice + { + return new class implements Advice { + public function getType(): AdviceTypeEnum + { + return AdviceTypeEnum::Before; + } + }; + } + }, + ]); + + $this->expectException(AspectException::class); + $this->expectExceptionMessage('does not expose a closure advice'); + + The::advice('manual-advisor'); + } + + /** + * @param array $values + */ + private function initKernelWithContainerValues(array $values): void + { + $kernel = TheTestAspectKernel::getInstance(); + $container = new Container(); + $container->registerAspect(new TheTestAspect()); + foreach ($values as $id => $value) { + $container->add($id, $value); + } + + $containerProperty = new ReflectionProperty(AspectKernel::class, 'container'); + $containerProperty->setValue($kernel, $container); + } +} + +final class TheTestAspectKernel extends AspectKernel +{ + protected function configureAop(AspectContainer $container): void + { + $container->registerAspect(new TheTestAspect()); + } +} + +final class TheTestAspect implements Aspect +{ +} diff --git a/tests/Core/AttributeAspectLoaderExtensionTest.php b/tests/Core/AttributeAspectLoaderExtensionTest.php new file mode 100644 index 00000000..3537b126 --- /dev/null +++ b/tests/Core/AttributeAspectLoaderExtensionTest.php @@ -0,0 +1,43 @@ +createStub(AspectContainer::class); + $this->extension = new AttributeAspectLoaderExtension(new PointcutLexer(), new PointcutParser(new PointcutGrammar($container))); + } + + public function testLoadsAdvisorForPublicAdviceMethod(): void + { + $aspect = new AttributeAspectLoaderExtensionTestPublicAspect(); + $loadedItems = $this->extension->load($aspect, new ReflectionClass($aspect)); + + $this->assertArrayHasKey($aspect::class . '->publicAdvice', $loadedItems); + } + + public function testRejectsNonPublicAdviceMethod(): void + { + $aspect = new AttributeAspectLoaderExtensionTestPrivateAspect(); + + $this->expectException(AspectException::class); + $this->expectExceptionMessage('first-class advice callables require all advice methods to be public'); + + $this->extension->load($aspect, new ReflectionClass($aspect)); + } +} diff --git a/tests/Core/ContainerTest.php b/tests/Core/ContainerTest.php index 18b1ee81..5b94c66a 100644 --- a/tests/Core/ContainerTest.php +++ b/tests/Core/ContainerTest.php @@ -72,7 +72,6 @@ public static function lazyInternalServices(): array AdviceMatcher::class => [AdviceMatcher::class], AspectLoader::class => [AspectLoader::class], CachedAspectLoader::class => [CachedAspectLoader::class], - LazyAdvisorAccessor::class => [LazyAdvisorAccessor::class], // [CachePathManager::class], // Need to politely switch to options instead of whole kernel ]; } diff --git a/tests/Instrument/Transformer/Php85AuditScratchTest.php b/tests/Instrument/Transformer/Php85AuditScratchTest.php index 41c6125b..d8bbcb87 100644 --- a/tests/Instrument/Transformer/Php85AuditScratchTest.php +++ b/tests/Instrument/Transformer/Php85AuditScratchTest.php @@ -11,6 +11,7 @@ namespace Go\Instrument\Transformer; use Go\Aop\Advisor; +use Go\Aop\Framework\BeforeInterceptor; use Go\Core\AdviceMatcherInterface; use Go\Core\AspectContainer; use Go\Core\AspectKernel; @@ -210,7 +211,7 @@ private function getInterceptEverythingMatcher(): AdviceMatcherInterface continue; } $advisorId = "advisor.{$refClass->name}->{$method->name}"; - $advices[AspectContainer::METHOD_PREFIX][$method->name][$advisorId] = true; + $advices[AspectContainer::METHOD_PREFIX][$method->name][$advisorId] = new BeforeInterceptor(static function (): void {}); } foreach ($refClass->getProperties() as $property) { if ($property->getDeclaringClass()->name !== $refClass->name) { @@ -221,7 +222,7 @@ private function getInterceptEverythingMatcher(): AdviceMatcherInterface continue; } $advisorId = "advisor.{$refClass->name}->{$property->name}"; - $advices[AspectContainer::PROPERTY_PREFIX][$property->name][$advisorId] = true; + $advices[AspectContainer::PROPERTY_PREFIX][$property->name][$advisorId] = new BeforeInterceptor(static function (): void {}); } return $advices; }); diff --git a/tests/Instrument/Transformer/WeavingTransformerTest.php b/tests/Instrument/Transformer/WeavingTransformerTest.php index cba7537f..31813c14 100644 --- a/tests/Instrument/Transformer/WeavingTransformerTest.php +++ b/tests/Instrument/Transformer/WeavingTransformerTest.php @@ -13,6 +13,7 @@ namespace Go\Instrument\Transformer; use Go\Aop\Advisor; +use Go\Aop\Framework\BeforeInterceptor; use Go\Core\AdviceMatcherInterface; use Go\Core\AspectContainer; use Go\Core\AspectKernel; @@ -402,7 +403,7 @@ public function testWeaverCopiesNonScalarAttributeArgumentsFromAst(): void $advices = []; foreach ($refClass->getMethods() as $method) { $advisorId = "advisor.{$refClass->name}->{$method->name}"; - $advices[AspectContainer::METHOD_PREFIX][$method->name][$advisorId] = true; + $advices[AspectContainer::METHOD_PREFIX][$method->name][$advisorId] = new BeforeInterceptor(static function (): void {}); } return $advices; }); @@ -477,7 +478,7 @@ public function testWeaverForPhp80To82Syntax(): void $advices = []; foreach ($refClass->getMethods() as $method) { $advisorId = "advisor.{$refClass->name}->{$method->name}"; - $advices[AspectContainer::METHOD_PREFIX][$method->name][$advisorId] = true; + $advices[AspectContainer::METHOD_PREFIX][$method->name][$advisorId] = new BeforeInterceptor(static function (): void {}); } return $advices; }); @@ -548,8 +549,12 @@ public function testWeaverMovesInterceptedPropertiesToProxyHooks(): void ->method('getAdvicesForClass') ->willReturn([ AspectContainer::PROPERTY_PREFIX => [ - 'value' => ['advisor.Go\Tests\TestProject\Application\Php84PropertyHooksClass->value' => true], - 'limited' => ['advisor.Go\Tests\TestProject\Application\Php84PropertyHooksClass->limited' => true], + 'value' => [ + 'advisor.Go\Tests\TestProject\Application\Php84PropertyHooksClass->value' => new BeforeInterceptor(static function (): void {}), + ], + 'limited' => [ + 'advisor.Go\Tests\TestProject\Application\Php84PropertyHooksClass->limited' => new BeforeInterceptor(static function (): void {}), + ], ], ]); $adviceMatcher @@ -587,8 +592,8 @@ public function testWeaverMovesInterceptedPropertiesToProxyHooks(): void $this->assertStringContainsString("public string \$value = 'test' {", $proxyContent); $this->assertStringContainsString("public protected(set) string \$limited = 'limited' {", $proxyContent); - $this->assertStringContainsString("InterceptorInjector::forProperty(self::class, 'value'", $proxyContent); - $this->assertStringContainsString("InterceptorInjector::forProperty(self::class, 'limited'", $proxyContent); + $this->assertStringContainsString("InterceptorInjector::forProperty(", $proxyContent); + $this->assertStringContainsString("InterceptorInjector::forProperty(", $proxyContent); } /** @@ -601,12 +606,12 @@ public function testWeaverDemotesInterceptedPromotedProperties(): void { $transformer = $this->createTransformerWithAdvices([ AspectContainer::PROPERTY_PREFIX => [ - 'name' => ['advisor.Go\Tests\TestProject\Application\PromotedPropertyClass->name' => true], - 'counter' => ['advisor.Go\Tests\TestProject\Application\PromotedPropertyClass->counter' => true], + 'name' => ['advisor.Go\Tests\TestProject\Application\PromotedPropertyClass->name' => new BeforeInterceptor(static function (): void {})], + 'counter' => ['advisor.Go\Tests\TestProject\Application\PromotedPropertyClass->counter' => new BeforeInterceptor(static function (): void {})], ], AspectContainer::METHOD_PREFIX => [ - '__construct' => ['advisor.Go\Tests\TestProject\Application\PromotedPropertyClass->__construct' => true], - 'getName' => ['advisor.Go\Tests\TestProject\Application\PromotedPropertyClass->getName' => true], + '__construct' => ['advisor.Go\Tests\TestProject\Application\PromotedPropertyClass->__construct' => new BeforeInterceptor(static function (): void {})], + 'getName' => ['advisor.Go\Tests\TestProject\Application\PromotedPropertyClass->getName' => new BeforeInterceptor(static function (): void {})], ], ]); @@ -641,11 +646,11 @@ public function testWeaverSkipsNewInInitializerDefaultOnProxyHookProperty(): voi { $transformer = $this->createTransformerWithAdvices([ AspectContainer::PROPERTY_PREFIX => [ - 'bag' => ['advisor.Go\Tests\TestProject\Application\NewInInitializerClass->bag' => true], + 'bag' => ['advisor.Go\Tests\TestProject\Application\NewInInitializerClass->bag' => new BeforeInterceptor(static function (): void {})], ], AspectContainer::METHOD_PREFIX => [ - '__construct' => ['advisor.Go\Tests\TestProject\Application\NewInInitializerClass->__construct' => true], - 'getBagItems' => ['advisor.Go\Tests\TestProject\Application\NewInInitializerClass->getBagItems' => true], + '__construct' => ['advisor.Go\Tests\TestProject\Application\NewInInitializerClass->__construct' => new BeforeInterceptor(static function (): void {})], + 'getBagItems' => ['advisor.Go\Tests\TestProject\Application\NewInInitializerClass->getBagItems' => new BeforeInterceptor(static function (): void {})], ], ]); @@ -684,10 +689,10 @@ public function testWeaverDemotesPromotedPropertyInSingleLineConstructor(): void { $transformer = $this->createTransformerWithAdvices([ AspectContainer::PROPERTY_PREFIX => [ - 'tag' => ['advisor.Go\Tests\TestProject\Application\SingleLinePromotedClass->tag' => true], + 'tag' => ['advisor.Go\Tests\TestProject\Application\SingleLinePromotedClass->tag' => new BeforeInterceptor(static function (): void {})], ], AspectContainer::METHOD_PREFIX => [ - '__construct' => ['advisor.Go\Tests\TestProject\Application\SingleLinePromotedClass->__construct' => true], + '__construct' => ['advisor.Go\Tests\TestProject\Application\SingleLinePromotedClass->__construct' => new BeforeInterceptor(static function (): void {})], ], ]); @@ -715,10 +720,10 @@ public function testWeaverDemotesFinalPromotedProperty(): void { $transformer = $this->createTransformerWithAdvices([ AspectContainer::PROPERTY_PREFIX => [ - 'token' => ['advisor.Go\Instrument\Transformer\Stubs\FinalPromotedClass85->token' => true], + 'token' => ['advisor.Go\Instrument\Transformer\Stubs\FinalPromotedClass85->token' => new BeforeInterceptor(static function (): void {})], ], AspectContainer::METHOD_PREFIX => [ - '__construct' => ['advisor.Go\Instrument\Transformer\Stubs\FinalPromotedClass85->__construct' => true], + '__construct' => ['advisor.Go\Instrument\Transformer\Stubs\FinalPromotedClass85->__construct' => new BeforeInterceptor(static function (): void {})], ], ]); @@ -827,7 +832,7 @@ protected function getAdviceMatcherMock(): AdviceMatcherInterface $advices = []; foreach ($refClass->getMethods() as $method) { $advisorId = "advisor.{$refClass->name}->{$method->name}"; - $advices[AspectContainer::METHOD_PREFIX][$method->name][$advisorId] = true; + $advices[AspectContainer::METHOD_PREFIX][$method->name][$advisorId] = new BeforeInterceptor(static function (): void {}); } return $advices; }); diff --git a/tests/Instrument/Transformer/_files/class-proxy.php b/tests/Instrument/Transformer/_files/class-proxy.php index 375369da..8f4bdb19 100644 --- a/tests/Instrument/Transformer/_files/class-proxy.php +++ b/tests/Instrument/Transformer/_files/class-proxy.php @@ -2,6 +2,8 @@ declare(strict_types=1); namespace Test\ns1; use Go\Aop\Framework\InterceptorInjector; +use Go\Aop\Framework\Interceptor; +use Go\Aop\Framework\The; use Go\Aop\Intercept\DynamicMethodInvocation; use Go\Aop\Intercept\StaticMethodInvocation; class TestClass implements \Go\Aop\Proxy @@ -18,43 +20,92 @@ class TestClass implements \Go\Aop\Proxy public function publicMethod() { /** @var DynamicMethodInvocation $__joinPoint */ - static $__joinPoint = InterceptorInjector::forMethod(self::class, 'publicMethod', ['advisor.Test\ns1\TestClass->publicMethod'], $this->__aop__publicMethod(...)); + static $__joinPoint = InterceptorInjector::forMethod( + self::class, + 'publicMethod', + [ + Interceptor::before(The::advice('advisor.Test\ns1\TestClass->publicMethod')), + ], + $this->__aop__publicMethod(...), + ); return $__joinPoint->__invoke($this); } protected function protectedMethod() { /** @var DynamicMethodInvocation $__joinPoint */ - static $__joinPoint = InterceptorInjector::forMethod(self::class, 'protectedMethod', ['advisor.Test\ns1\TestClass->protectedMethod'], $this->__aop__protectedMethod(...)); + static $__joinPoint = InterceptorInjector::forMethod( + self::class, + 'protectedMethod', + [ + Interceptor::before(The::advice('advisor.Test\ns1\TestClass->protectedMethod')), + ], + $this->__aop__protectedMethod(...), + ); return $__joinPoint->__invoke($this); } public static function publicStaticMethod() { /** @var StaticMethodInvocation $__joinPoint */ - static $__joinPoint = InterceptorInjector::forStaticMethod(self::class, 'publicStaticMethod', ['advisor.Test\ns1\TestClass->publicStaticMethod'], self::__aop__publicStaticMethod(...)); + static $__joinPoint = InterceptorInjector::forStaticMethod( + self::class, + 'publicStaticMethod', + [ + Interceptor::before(The::advice('advisor.Test\ns1\TestClass->publicStaticMethod')), + ], + self::__aop__publicStaticMethod(...), + ); return $__joinPoint->__invoke(static::class); } protected static function protectedStaticMethod() { /** @var StaticMethodInvocation $__joinPoint */ - static $__joinPoint = InterceptorInjector::forStaticMethod(self::class, 'protectedStaticMethod', ['advisor.Test\ns1\TestClass->protectedStaticMethod'], self::__aop__protectedStaticMethod(...)); + static $__joinPoint = InterceptorInjector::forStaticMethod( + self::class, + 'protectedStaticMethod', + [ + Interceptor::before(The::advice('advisor.Test\ns1\TestClass->protectedStaticMethod')), + ], + self::__aop__protectedStaticMethod(...), + ); return $__joinPoint->__invoke(static::class); } public function publicMethodDynamicArguments($a, &$b) { /** @var DynamicMethodInvocation $__joinPoint */ - static $__joinPoint = InterceptorInjector::forMethod(self::class, 'publicMethodDynamicArguments', ['advisor.Test\ns1\TestClass->publicMethodDynamicArguments'], $this->__aop__publicMethodDynamicArguments(...)); + static $__joinPoint = InterceptorInjector::forMethod( + self::class, + 'publicMethodDynamicArguments', + [ + Interceptor::before(The::advice('advisor.Test\ns1\TestClass->publicMethodDynamicArguments')), + ], + $this->__aop__publicMethodDynamicArguments(...), + ); return $__joinPoint->__invoke($this, [$a, &$b]); } public function publicMethodFixedArguments($a, $b, $c = null) { /** @var DynamicMethodInvocation $__joinPoint */ - static $__joinPoint = InterceptorInjector::forMethod(self::class, 'publicMethodFixedArguments', ['advisor.Test\ns1\TestClass->publicMethodFixedArguments'], $this->__aop__publicMethodFixedArguments(...)); + static $__joinPoint = InterceptorInjector::forMethod( + self::class, + 'publicMethodFixedArguments', + [ + Interceptor::before(The::advice('advisor.Test\ns1\TestClass->publicMethodFixedArguments')), + ], + $this->__aop__publicMethodFixedArguments(...), + ); return $__joinPoint->__invoke($this, \array_slice([$a, $b, $c], 0, \func_num_args())); } public function methodWithSpecialTypeArguments(self $instance) { /** @var DynamicMethodInvocation $__joinPoint */ - static $__joinPoint = InterceptorInjector::forMethod(self::class, 'methodWithSpecialTypeArguments', ['advisor.Test\ns1\TestClass->methodWithSpecialTypeArguments'], $this->__aop__methodWithSpecialTypeArguments(...)); + static $__joinPoint = InterceptorInjector::forMethod( + self::class, + 'methodWithSpecialTypeArguments', + [ + Interceptor::before(The::advice('advisor.Test\ns1\TestClass->methodWithSpecialTypeArguments')), + ], + $this->__aop__methodWithSpecialTypeArguments(...), + ); return $__joinPoint->__invoke($this, [$instance]); } } diff --git a/tests/Instrument/Transformer/_files/final-readonly-class-proxy.php b/tests/Instrument/Transformer/_files/final-readonly-class-proxy.php index 4156d57c..94467ba2 100644 --- a/tests/Instrument/Transformer/_files/final-readonly-class-proxy.php +++ b/tests/Instrument/Transformer/_files/final-readonly-class-proxy.php @@ -2,6 +2,8 @@ declare(strict_types=1); namespace Test\ns1; use Go\Aop\Framework\InterceptorInjector; +use Go\Aop\Framework\Interceptor; +use Go\Aop\Framework\The; use Go\Aop\Intercept\DynamicMethodInvocation; use Go\Aop\Intercept\StaticMethodInvocation; final readonly class TestReadonlyClass implements \Go\Aop\Proxy @@ -14,19 +16,40 @@ public function publicMethod(): string { /** @var DynamicMethodInvocation $__joinPoint */ - static $__joinPoint = InterceptorInjector::forMethod(self::class, 'publicMethod', ['advisor.Test\ns1\TestReadonlyClass->publicMethod'], $this->__aop__publicMethod(...)); + static $__joinPoint = InterceptorInjector::forMethod( + self::class, + 'publicMethod', + [ + Interceptor::before(The::advice('advisor.Test\ns1\TestReadonlyClass->publicMethod')), + ], + $this->__aop__publicMethod(...), + ); return $__joinPoint->__invoke($this); } public function anotherMethod(int $x): int { /** @var DynamicMethodInvocation $__joinPoint */ - static $__joinPoint = InterceptorInjector::forMethod(self::class, 'anotherMethod', ['advisor.Test\ns1\TestReadonlyClass->anotherMethod'], $this->__aop__anotherMethod(...)); + static $__joinPoint = InterceptorInjector::forMethod( + self::class, + 'anotherMethod', + [ + Interceptor::before(The::advice('advisor.Test\ns1\TestReadonlyClass->anotherMethod')), + ], + $this->__aop__anotherMethod(...), + ); return $__joinPoint->__invoke($this, [$x]); } public static function staticMethod(): string { /** @var StaticMethodInvocation $__joinPoint */ - static $__joinPoint = InterceptorInjector::forStaticMethod(self::class, 'staticMethod', ['advisor.Test\ns1\TestReadonlyClass->staticMethod'], self::__aop__staticMethod(...)); + static $__joinPoint = InterceptorInjector::forStaticMethod( + self::class, + 'staticMethod', + [ + Interceptor::before(The::advice('advisor.Test\ns1\TestReadonlyClass->staticMethod')), + ], + self::__aop__staticMethod(...), + ); return $__joinPoint->__invoke(static::class); } } diff --git a/tests/Instrument/Transformer/_files/php7-class-proxy.php b/tests/Instrument/Transformer/_files/php7-class-proxy.php index ec77845a..2c8ca622 100644 --- a/tests/Instrument/Transformer/_files/php7-class-proxy.php +++ b/tests/Instrument/Transformer/_files/php7-class-proxy.php @@ -2,6 +2,8 @@ declare(strict_types=1); namespace Test\ns1; use Go\Aop\Framework\InterceptorInjector; +use Go\Aop\Framework\Interceptor; +use Go\Aop\Framework\The; use Go\Aop\Intercept\DynamicMethodInvocation; class TestPhp7Class implements \Go\Aop\Proxy { @@ -27,103 +29,222 @@ class TestPhp7Class implements \Go\Aop\Proxy public function stringSth(string $arg) { /** @var DynamicMethodInvocation $__joinPoint */ - static $__joinPoint = InterceptorInjector::forMethod(self::class, 'stringSth', ['advisor.Test\ns1\TestPhp7Class->stringSth'], $this->__aop__stringSth(...)); + static $__joinPoint = InterceptorInjector::forMethod( + self::class, + 'stringSth', + [ + Interceptor::before(The::advice('advisor.Test\ns1\TestPhp7Class->stringSth')), + ], + $this->__aop__stringSth(...), + ); return $__joinPoint->__invoke($this, [$arg]); } public function floatSth(float $arg) { /** @var DynamicMethodInvocation $__joinPoint */ - static $__joinPoint = InterceptorInjector::forMethod(self::class, 'floatSth', ['advisor.Test\ns1\TestPhp7Class->floatSth'], $this->__aop__floatSth(...)); + static $__joinPoint = InterceptorInjector::forMethod( + self::class, + 'floatSth', + [ + Interceptor::before(The::advice('advisor.Test\ns1\TestPhp7Class->floatSth')), + ], + $this->__aop__floatSth(...), + ); return $__joinPoint->__invoke($this, [$arg]); } public function boolSth(bool $arg) { /** @var DynamicMethodInvocation $__joinPoint */ - static $__joinPoint = InterceptorInjector::forMethod(self::class, 'boolSth', ['advisor.Test\ns1\TestPhp7Class->boolSth'], $this->__aop__boolSth(...)); + static $__joinPoint = InterceptorInjector::forMethod( + self::class, + 'boolSth', + [ + Interceptor::before(The::advice('advisor.Test\ns1\TestPhp7Class->boolSth')), + ], + $this->__aop__boolSth(...), + ); return $__joinPoint->__invoke($this, [$arg]); } public function intSth(int $arg) { /** @var DynamicMethodInvocation $__joinPoint */ - static $__joinPoint = InterceptorInjector::forMethod(self::class, 'intSth', ['advisor.Test\ns1\TestPhp7Class->intSth'], $this->__aop__intSth(...)); + static $__joinPoint = InterceptorInjector::forMethod( + self::class, + 'intSth', + [ + Interceptor::before(The::advice('advisor.Test\ns1\TestPhp7Class->intSth')), + ], + $this->__aop__intSth(...), + ); return $__joinPoint->__invoke($this, [$arg]); } public function callableSth(callable $arg) { /** @var DynamicMethodInvocation $__joinPoint */ - static $__joinPoint = InterceptorInjector::forMethod(self::class, 'callableSth', ['advisor.Test\ns1\TestPhp7Class->callableSth'], $this->__aop__callableSth(...)); + static $__joinPoint = InterceptorInjector::forMethod( + self::class, + 'callableSth', + [ + Interceptor::before(The::advice('advisor.Test\ns1\TestPhp7Class->callableSth')), + ], + $this->__aop__callableSth(...), + ); return $__joinPoint->__invoke($this, [$arg]); } public function arraySth(array $arg) { /** @var DynamicMethodInvocation $__joinPoint */ - static $__joinPoint = InterceptorInjector::forMethod(self::class, 'arraySth', ['advisor.Test\ns1\TestPhp7Class->arraySth'], $this->__aop__arraySth(...)); + static $__joinPoint = InterceptorInjector::forMethod( + self::class, + 'arraySth', + [ + Interceptor::before(The::advice('advisor.Test\ns1\TestPhp7Class->arraySth')), + ], + $this->__aop__arraySth(...), + ); return $__joinPoint->__invoke($this, [$arg]); } public function variadicStringSthByRef(string &...$args) { /** @var DynamicMethodInvocation $__joinPoint */ - static $__joinPoint = InterceptorInjector::forMethod(self::class, 'variadicStringSthByRef', ['advisor.Test\ns1\TestPhp7Class->variadicStringSthByRef'], $this->__aop__variadicStringSthByRef(...)); + static $__joinPoint = InterceptorInjector::forMethod( + self::class, + 'variadicStringSthByRef', + [ + Interceptor::before(The::advice('advisor.Test\ns1\TestPhp7Class->variadicStringSthByRef')), + ], + $this->__aop__variadicStringSthByRef(...), + ); return $__joinPoint->__invoke($this, $args); } public function exceptionArg(\Exception $exception, \Test\ns1\Exception $localException) { /** @var DynamicMethodInvocation $__joinPoint */ - static $__joinPoint = InterceptorInjector::forMethod(self::class, 'exceptionArg', ['advisor.Test\ns1\TestPhp7Class->exceptionArg'], $this->__aop__exceptionArg(...)); + static $__joinPoint = InterceptorInjector::forMethod( + self::class, + 'exceptionArg', + [ + Interceptor::before(The::advice('advisor.Test\ns1\TestPhp7Class->exceptionArg')), + ], + $this->__aop__exceptionArg(...), + ); return $__joinPoint->__invoke($this, [$exception, $localException]); } public function stringRth(string $arg): string { /** @var DynamicMethodInvocation $__joinPoint */ - static $__joinPoint = InterceptorInjector::forMethod(self::class, 'stringRth', ['advisor.Test\ns1\TestPhp7Class->stringRth'], $this->__aop__stringRth(...)); + static $__joinPoint = InterceptorInjector::forMethod( + self::class, + 'stringRth', + [ + Interceptor::before(The::advice('advisor.Test\ns1\TestPhp7Class->stringRth')), + ], + $this->__aop__stringRth(...), + ); return $__joinPoint->__invoke($this, [$arg]); } public function floatRth(float $arg): float { /** @var DynamicMethodInvocation $__joinPoint */ - static $__joinPoint = InterceptorInjector::forMethod(self::class, 'floatRth', ['advisor.Test\ns1\TestPhp7Class->floatRth'], $this->__aop__floatRth(...)); + static $__joinPoint = InterceptorInjector::forMethod( + self::class, + 'floatRth', + [ + Interceptor::before(The::advice('advisor.Test\ns1\TestPhp7Class->floatRth')), + ], + $this->__aop__floatRth(...), + ); return $__joinPoint->__invoke($this, [$arg]); } public function boolRth(bool $arg): bool { /** @var DynamicMethodInvocation $__joinPoint */ - static $__joinPoint = InterceptorInjector::forMethod(self::class, 'boolRth', ['advisor.Test\ns1\TestPhp7Class->boolRth'], $this->__aop__boolRth(...)); + static $__joinPoint = InterceptorInjector::forMethod( + self::class, + 'boolRth', + [ + Interceptor::before(The::advice('advisor.Test\ns1\TestPhp7Class->boolRth')), + ], + $this->__aop__boolRth(...), + ); return $__joinPoint->__invoke($this, [$arg]); } public function intRth(int $arg): int { /** @var DynamicMethodInvocation $__joinPoint */ - static $__joinPoint = InterceptorInjector::forMethod(self::class, 'intRth', ['advisor.Test\ns1\TestPhp7Class->intRth'], $this->__aop__intRth(...)); + static $__joinPoint = InterceptorInjector::forMethod( + self::class, + 'intRth', + [ + Interceptor::before(The::advice('advisor.Test\ns1\TestPhp7Class->intRth')), + ], + $this->__aop__intRth(...), + ); return $__joinPoint->__invoke($this, [$arg]); } public function callableRth(callable $arg): callable { /** @var DynamicMethodInvocation $__joinPoint */ - static $__joinPoint = InterceptorInjector::forMethod(self::class, 'callableRth', ['advisor.Test\ns1\TestPhp7Class->callableRth'], $this->__aop__callableRth(...)); + static $__joinPoint = InterceptorInjector::forMethod( + self::class, + 'callableRth', + [ + Interceptor::before(The::advice('advisor.Test\ns1\TestPhp7Class->callableRth')), + ], + $this->__aop__callableRth(...), + ); return $__joinPoint->__invoke($this, [$arg]); } public function arrayRth(array $arg): array { /** @var DynamicMethodInvocation $__joinPoint */ - static $__joinPoint = InterceptorInjector::forMethod(self::class, 'arrayRth', ['advisor.Test\ns1\TestPhp7Class->arrayRth'], $this->__aop__arrayRth(...)); + static $__joinPoint = InterceptorInjector::forMethod( + self::class, + 'arrayRth', + [ + Interceptor::before(The::advice('advisor.Test\ns1\TestPhp7Class->arrayRth')), + ], + $this->__aop__arrayRth(...), + ); return $__joinPoint->__invoke($this, [$arg]); } public function exceptionRth(\Exception $exception): \Exception { /** @var DynamicMethodInvocation $__joinPoint */ - static $__joinPoint = InterceptorInjector::forMethod(self::class, 'exceptionRth', ['advisor.Test\ns1\TestPhp7Class->exceptionRth'], $this->__aop__exceptionRth(...)); + static $__joinPoint = InterceptorInjector::forMethod( + self::class, + 'exceptionRth', + [ + Interceptor::before(The::advice('advisor.Test\ns1\TestPhp7Class->exceptionRth')), + ], + $this->__aop__exceptionRth(...), + ); return $__joinPoint->__invoke($this, [$exception]); } public function noRth(\Test\ns1\LocalException $exception) { /** @var DynamicMethodInvocation $__joinPoint */ - static $__joinPoint = InterceptorInjector::forMethod(self::class, 'noRth', ['advisor.Test\ns1\TestPhp7Class->noRth'], $this->__aop__noRth(...)); + static $__joinPoint = InterceptorInjector::forMethod( + self::class, + 'noRth', + [ + Interceptor::before(The::advice('advisor.Test\ns1\TestPhp7Class->noRth')), + ], + $this->__aop__noRth(...), + ); return $__joinPoint->__invoke($this, [$exception]); } public function returnSelf(): self { /** @var DynamicMethodInvocation $__joinPoint */ - static $__joinPoint = InterceptorInjector::forMethod(self::class, 'returnSelf', ['advisor.Test\ns1\TestPhp7Class->returnSelf'], $this->__aop__returnSelf(...)); + static $__joinPoint = InterceptorInjector::forMethod( + self::class, + 'returnSelf', + [ + Interceptor::before(The::advice('advisor.Test\ns1\TestPhp7Class->returnSelf')), + ], + $this->__aop__returnSelf(...), + ); return $__joinPoint->__invoke($this); } } diff --git a/tests/Instrument/Transformer/_files/php80-82-syntax-proxy.php b/tests/Instrument/Transformer/_files/php80-82-syntax-proxy.php index 43e5cb58..c2f04918 100644 --- a/tests/Instrument/Transformer/_files/php80-82-syntax-proxy.php +++ b/tests/Instrument/Transformer/_files/php80-82-syntax-proxy.php @@ -3,6 +3,8 @@ namespace Test\ns1; use Go\Aop\Framework\InterceptorInjector; +use Go\Aop\Framework\Interceptor; +use Go\Aop\Framework\The; use Go\Aop\Intercept\DynamicMethodInvocation; /** * Compact class covering general PHP 8.0-8.3 syntax through the weaver: @@ -19,13 +21,27 @@ class TestPhp80To82SyntaxClass implements \Go\Aop\Proxy public function __construct(string $label = 'default', \ArrayObject $items = new \ArrayObject([1, 2, 3])) { /** @var DynamicMethodInvocation $__joinPoint */ - static $__joinPoint = InterceptorInjector::forMethod(self::class, '__construct', ['advisor.Test\ns1\TestPhp80To82SyntaxClass->__construct'], $this->__aop____construct(...)); + static $__joinPoint = InterceptorInjector::forMethod( + self::class, + '__construct', + [ + Interceptor::before(The::advice('advisor.Test\ns1\TestPhp80To82SyntaxClass->__construct')), + ], + $this->__aop____construct(...), + ); return $__joinPoint->__invoke($this, \array_slice([$label, $items], 0, \func_num_args())); } public function describe(?\ArrayObject $extra = null): string { /** @var DynamicMethodInvocation $__joinPoint */ - static $__joinPoint = InterceptorInjector::forMethod(self::class, 'describe', ['advisor.Test\ns1\TestPhp80To82SyntaxClass->describe'], $this->__aop__describe(...)); + static $__joinPoint = InterceptorInjector::forMethod( + self::class, + 'describe', + [ + Interceptor::before(The::advice('advisor.Test\ns1\TestPhp80To82SyntaxClass->describe')), + ], + $this->__aop__describe(...), + ); return $__joinPoint->__invoke($this, \array_slice([$extra], 0, \func_num_args())); } } \ No newline at end of file diff --git a/tests/Instrument/Transformer/_files/php80-promoted-property-proxy.php b/tests/Instrument/Transformer/_files/php80-promoted-property-proxy.php index 28de8941..fc3b4128 100644 --- a/tests/Instrument/Transformer/_files/php80-promoted-property-proxy.php +++ b/tests/Instrument/Transformer/_files/php80-promoted-property-proxy.php @@ -3,6 +3,8 @@ namespace Go\Tests\TestProject\Application; use Go\Aop\Framework\InterceptorInjector; +use Go\Aop\Framework\Interceptor; +use Go\Aop\Framework\The; use Go\Aop\Intercept\DynamicMethodInvocation; use Go\Aop\Intercept\FieldAccess; use Go\Aop\Intercept\FieldAccessType; @@ -19,37 +21,75 @@ class PromotedPropertyClass implements \Go\Aop\Proxy private string $name = 'initial' { get { /** @var FieldAccess $__joinPoint */ - static $__joinPoint = InterceptorInjector::forProperty(self::class, 'name', ['advisor.Go\Tests\TestProject\Application\PromotedPropertyClass->name']); + static $__joinPoint = InterceptorInjector::forProperty( + self::class, + 'name', + [ + Interceptor::before(The::advice('advisor.Go\Tests\TestProject\Application\PromotedPropertyClass->name')), + ], + ); return $__joinPoint->__invoke($this, FieldAccessType::READ, $this->name); } set { /** @var FieldAccess $__joinPoint */ - static $__joinPoint = InterceptorInjector::forProperty(self::class, 'name', ['advisor.Go\Tests\TestProject\Application\PromotedPropertyClass->name']); + static $__joinPoint = InterceptorInjector::forProperty( + self::class, + 'name', + [ + Interceptor::before(The::advice('advisor.Go\Tests\TestProject\Application\PromotedPropertyClass->name')), + ], + ); $this->name = $__joinPoint->__invoke($this, FieldAccessType::WRITE, $value, $this->name); } } final public private(set) int $counter = 1 { get { /** @var FieldAccess $__joinPoint */ - static $__joinPoint = InterceptorInjector::forProperty(self::class, 'counter', ['advisor.Go\Tests\TestProject\Application\PromotedPropertyClass->counter']); + static $__joinPoint = InterceptorInjector::forProperty( + self::class, + 'counter', + [ + Interceptor::before(The::advice('advisor.Go\Tests\TestProject\Application\PromotedPropertyClass->counter')), + ], + ); return $__joinPoint->__invoke($this, FieldAccessType::READ, $this->counter); } set { /** @var FieldAccess $__joinPoint */ - static $__joinPoint = InterceptorInjector::forProperty(self::class, 'counter', ['advisor.Go\Tests\TestProject\Application\PromotedPropertyClass->counter']); + static $__joinPoint = InterceptorInjector::forProperty( + self::class, + 'counter', + [ + Interceptor::before(The::advice('advisor.Go\Tests\TestProject\Application\PromotedPropertyClass->counter')), + ], + ); $this->counter = $__joinPoint->__invoke($this, FieldAccessType::WRITE, $value, $this->counter); } } public function __construct(string $name = 'initial', int $counter = 1, ?\ArrayObject $bag = null) { /** @var DynamicMethodInvocation $__joinPoint */ - static $__joinPoint = InterceptorInjector::forMethod(self::class, '__construct', ['advisor.Go\Tests\TestProject\Application\PromotedPropertyClass->__construct'], $this->__aop____construct(...)); + static $__joinPoint = InterceptorInjector::forMethod( + self::class, + '__construct', + [ + Interceptor::before(The::advice('advisor.Go\Tests\TestProject\Application\PromotedPropertyClass->__construct')), + ], + $this->__aop____construct(...), + ); return $__joinPoint->__invoke($this, \array_slice([$name, $counter, $bag], 0, \func_num_args())); } public function getName(): string { /** @var DynamicMethodInvocation $__joinPoint */ - static $__joinPoint = InterceptorInjector::forMethod(self::class, 'getName', ['advisor.Go\Tests\TestProject\Application\PromotedPropertyClass->getName'], $this->__aop__getName(...)); + static $__joinPoint = InterceptorInjector::forMethod( + self::class, + 'getName', + [ + Interceptor::before(The::advice('advisor.Go\Tests\TestProject\Application\PromotedPropertyClass->getName')), + ], + $this->__aop__getName(...), + ); return $__joinPoint->__invoke($this); } } \ No newline at end of file diff --git a/tests/Instrument/Transformer/_files/php80-promoted-property-single-line-proxy.php b/tests/Instrument/Transformer/_files/php80-promoted-property-single-line-proxy.php index 6da7cc51..abd9026d 100644 --- a/tests/Instrument/Transformer/_files/php80-promoted-property-single-line-proxy.php +++ b/tests/Instrument/Transformer/_files/php80-promoted-property-single-line-proxy.php @@ -3,6 +3,8 @@ namespace Go\Tests\TestProject\Application; use Go\Aop\Framework\InterceptorInjector; +use Go\Aop\Framework\Interceptor; +use Go\Aop\Framework\The; use Go\Aop\Intercept\DynamicMethodInvocation; use Go\Aop\Intercept\FieldAccess; use Go\Aop\Intercept\FieldAccessType; @@ -18,19 +20,38 @@ class SingleLinePromotedClass implements \Go\Aop\Proxy public string $tag = 'default' { get { /** @var FieldAccess $__joinPoint */ - static $__joinPoint = InterceptorInjector::forProperty(self::class, 'tag', ['advisor.Go\Tests\TestProject\Application\SingleLinePromotedClass->tag']); + static $__joinPoint = InterceptorInjector::forProperty( + self::class, + 'tag', + [ + Interceptor::before(The::advice('advisor.Go\Tests\TestProject\Application\SingleLinePromotedClass->tag')), + ], + ); return $__joinPoint->__invoke($this, FieldAccessType::READ, $this->tag); } set { /** @var FieldAccess $__joinPoint */ - static $__joinPoint = InterceptorInjector::forProperty(self::class, 'tag', ['advisor.Go\Tests\TestProject\Application\SingleLinePromotedClass->tag']); + static $__joinPoint = InterceptorInjector::forProperty( + self::class, + 'tag', + [ + Interceptor::before(The::advice('advisor.Go\Tests\TestProject\Application\SingleLinePromotedClass->tag')), + ], + ); $this->tag = $__joinPoint->__invoke($this, FieldAccessType::WRITE, $value, $this->tag); } } public function __construct(string $tag = 'default') { /** @var DynamicMethodInvocation $__joinPoint */ - static $__joinPoint = InterceptorInjector::forMethod(self::class, '__construct', ['advisor.Go\Tests\TestProject\Application\SingleLinePromotedClass->__construct'], $this->__aop____construct(...)); + static $__joinPoint = InterceptorInjector::forMethod( + self::class, + '__construct', + [ + Interceptor::before(The::advice('advisor.Go\Tests\TestProject\Application\SingleLinePromotedClass->__construct')), + ], + $this->__aop____construct(...), + ); return $__joinPoint->__invoke($this, \array_slice([$tag], 0, \func_num_args())); } } \ No newline at end of file diff --git a/tests/Instrument/Transformer/_files/php81-attr-args-proxy.php b/tests/Instrument/Transformer/_files/php81-attr-args-proxy.php index e53ede86..60f4be19 100644 --- a/tests/Instrument/Transformer/_files/php81-attr-args-proxy.php +++ b/tests/Instrument/Transformer/_files/php81-attr-args-proxy.php @@ -3,6 +3,8 @@ namespace Test\ns1; use Go\Aop\Framework\InterceptorInjector; +use Go\Aop\Framework\Interceptor; +use Go\Aop\Framework\The; use Go\Aop\Intercept\DynamicMethodInvocation; class TestAttributeArgsClass implements \Go\Aop\Proxy { @@ -17,14 +19,28 @@ public function tagged( ): int { /** @var DynamicMethodInvocation $__joinPoint */ - static $__joinPoint = InterceptorInjector::forMethod(self::class, 'tagged', ['advisor.Test\ns1\TestAttributeArgsClass->tagged'], $this->__aop__tagged(...)); + static $__joinPoint = InterceptorInjector::forMethod( + self::class, + 'tagged', + [ + Interceptor::before(The::advice('advisor.Test\ns1\TestAttributeArgsClass->tagged')), + ], + $this->__aop__tagged(...), + ); return $__joinPoint->__invoke($this, \array_slice([$x], 0, \func_num_args())); } #[\Test\ns1\RichValueAttr(\Test\ns1\AttrStatus::Active, new \ArrayObject([1, 2]))] public function collected(): array { /** @var DynamicMethodInvocation $__joinPoint */ - static $__joinPoint = InterceptorInjector::forMethod(self::class, 'collected', ['advisor.Test\ns1\TestAttributeArgsClass->collected'], $this->__aop__collected(...)); + static $__joinPoint = InterceptorInjector::forMethod( + self::class, + 'collected', + [ + Interceptor::before(The::advice('advisor.Test\ns1\TestAttributeArgsClass->collected')), + ], + $this->__aop__collected(...), + ); return $__joinPoint->__invoke($this); } } \ No newline at end of file diff --git a/tests/Instrument/Transformer/_files/php81-enum-const-expr-proxy.php b/tests/Instrument/Transformer/_files/php81-enum-const-expr-proxy.php index e992fb8a..11988376 100644 --- a/tests/Instrument/Transformer/_files/php81-enum-const-expr-proxy.php +++ b/tests/Instrument/Transformer/_files/php81-enum-const-expr-proxy.php @@ -3,6 +3,8 @@ namespace Test\ns1; use Go\Aop\Framework\InterceptorInjector; +use Go\Aop\Framework\Interceptor; +use Go\Aop\Framework\The; use Go\Aop\Intercept\DynamicMethodInvocation; enum ConstExprStatus : int implements \Go\Aop\Proxy { @@ -15,7 +17,14 @@ enum ConstExprStatus : int implements \Go\Aop\Proxy public function describe(): string { /** @var DynamicMethodInvocation $__joinPoint */ - static $__joinPoint = InterceptorInjector::forMethod(self::class, 'describe', ['advisor.Test\ns1\ConstExprStatus->describe'], $this->__aop__describe(...)); + static $__joinPoint = InterceptorInjector::forMethod( + self::class, + 'describe', + [ + Interceptor::before(The::advice('advisor.Test\ns1\ConstExprStatus->describe')), + ], + $this->__aop__describe(...), + ); return $__joinPoint->__invoke($this); } } \ No newline at end of file diff --git a/tests/Instrument/Transformer/_files/php81-enum-proxy.php b/tests/Instrument/Transformer/_files/php81-enum-proxy.php index 55acd06e..0396b4ed 100644 --- a/tests/Instrument/Transformer/_files/php81-enum-proxy.php +++ b/tests/Instrument/Transformer/_files/php81-enum-proxy.php @@ -2,6 +2,8 @@ declare(strict_types=1); namespace Test\ns1; use Go\Aop\Framework\InterceptorInjector; +use Go\Aop\Framework\Interceptor; +use Go\Aop\Framework\The; use Go\Aop\Intercept\DynamicMethodInvocation; enum TestStatus : string implements \Go\Aop\Proxy { @@ -13,7 +15,14 @@ enum TestStatus : string implements \Go\Aop\Proxy public function label(): string { /** @var DynamicMethodInvocation $__joinPoint */ - static $__joinPoint = InterceptorInjector::forMethod(self::class, 'label', ['advisor.Test\ns1\TestStatus->label'], $this->__aop__label(...)); + static $__joinPoint = InterceptorInjector::forMethod( + self::class, + 'label', + [ + Interceptor::before(The::advice('advisor.Test\ns1\TestStatus->label')), + ], + $this->__aop__label(...), + ); return $__joinPoint->__invoke($this); } } diff --git a/tests/Instrument/Transformer/_files/php83-override-proxy.php b/tests/Instrument/Transformer/_files/php83-override-proxy.php index dddec077..7c1e8678 100644 --- a/tests/Instrument/Transformer/_files/php83-override-proxy.php +++ b/tests/Instrument/Transformer/_files/php83-override-proxy.php @@ -2,6 +2,8 @@ declare(strict_types=1); namespace Test\ns1; use Go\Aop\Framework\InterceptorInjector; +use Go\Aop\Framework\Interceptor; +use Go\Aop\Framework\The; use Go\Aop\Intercept\DynamicMethodInvocation; /** * PHP 8.3 — class with #[\Override] on an intercepted method. @@ -18,13 +20,27 @@ class TestClassWithOverride implements \Go\Aop\Proxy public function overriddenMethod(): string { /** @var DynamicMethodInvocation $__joinPoint */ - static $__joinPoint = InterceptorInjector::forMethod(self::class, 'overriddenMethod', ['advisor.Test\ns1\TestClassWithOverride->overriddenMethod'], $this->__aop__overriddenMethod(...)); + static $__joinPoint = InterceptorInjector::forMethod( + self::class, + 'overriddenMethod', + [ + Interceptor::before(The::advice('advisor.Test\ns1\TestClassWithOverride->overriddenMethod')), + ], + $this->__aop__overriddenMethod(...), + ); return $__joinPoint->__invoke($this); } public function normalMethod(): int { /** @var DynamicMethodInvocation $__joinPoint */ - static $__joinPoint = InterceptorInjector::forMethod(self::class, 'normalMethod', ['advisor.Test\ns1\TestClassWithOverride->normalMethod'], $this->__aop__normalMethod(...)); + static $__joinPoint = InterceptorInjector::forMethod( + self::class, + 'normalMethod', + [ + Interceptor::before(The::advice('advisor.Test\ns1\TestClassWithOverride->normalMethod')), + ], + $this->__aop__normalMethod(...), + ); return $__joinPoint->__invoke($this); } } diff --git a/tests/PhpUnit/ProxyClassReflectionHelper.php b/tests/PhpUnit/ProxyClassReflectionHelper.php index 9d93cc29..5cf26077 100644 --- a/tests/PhpUnit/ProxyClassReflectionHelper.php +++ b/tests/PhpUnit/ProxyClassReflectionHelper.php @@ -19,10 +19,13 @@ use PhpParser\ConstExprEvaluator; use PhpParser\Node\Arg; use PhpParser\Node\Expr\Array_; +use PhpParser\Node\Expr\ClassConstFetch; +use PhpParser\Node\Expr\MethodCall; use PhpParser\Node\Expr\StaticCall; use PhpParser\Node\Identifier; use PhpParser\Node\Name; use PhpParser\Node\Scalar\String_; +use PhpParser\Node\Stmt\Use_; use PhpParser\NodeFinder; /** @@ -84,7 +87,7 @@ public static function extractAdvicesFromProxyFile(string $className, array $con }); if (!empty($injectorCalls)) { - return self::extractAdvicesFromInjectorCalls($injectorCalls); + return self::extractAdvicesFromInjectorCalls($injectorCalls, self::extractUseAliases($ast)); } // Legacy enum proxies use per-method static joinpoints via EnumProxyGenerator::getJoinPoint(). @@ -141,9 +144,10 @@ public static function extractAdvicesFromProxyFile(string $className, array $con /** * @param StaticCall[] $injectorCalls + * @param array $useAliases * @return array>> */ - private static function extractAdvicesFromInjectorCalls(array $injectorCalls): array + private static function extractAdvicesFromInjectorCalls(array $injectorCalls, array $useAliases): array { $evaluator = new ConstExprEvaluator(); $result = []; @@ -172,8 +176,12 @@ private static function extractAdvicesFromInjectorCalls(array $injectorCalls): a continue; } - $adviceNames = $evaluator->evaluateSilently($call->args[$advicesIndex]->value); - if (!is_array($adviceNames)) { + $advicesNode = $call->args[$advicesIndex]->value; + $adviceNames = self::extractAdviceNamesFromGeneratedFactories($advicesNode, $useAliases); + if ($adviceNames === []) { + $adviceNames = $evaluator->evaluateSilently($advicesNode); + } + if (!is_array($adviceNames) || $adviceNames === []) { continue; } @@ -198,6 +206,79 @@ private static function extractAdvicesFromInjectorCalls(array $injectorCalls): a return $result; } + /** + * @param array $ast + * @return array + */ + private static function extractUseAliases(array $ast): array + { + $uses = []; + /** @var Use_[] $useNodes */ + $useNodes = (new NodeFinder())->findInstanceOf($ast, Use_::class); + foreach ($useNodes as $useNode) { + foreach ($useNode->uses as $useUse) { + $fqcn = $useUse->name->toString(); + $alias = $useUse->alias?->toString() ?? substr($fqcn, (int) strrpos($fqcn, '\\') + 1); + $uses[$alias] = $fqcn; + } + } + + return $uses; + } + + /** + * @param array $useAliases + * @return list + */ + private static function extractAdviceNamesFromGeneratedFactories(mixed $advicesNode, array $useAliases): array + { + if (!$advicesNode instanceof Array_) { + return []; + } + + $advisorNames = []; + foreach ($advicesNode->items as $item) { + $factoryCall = $item?->value; + if (!$factoryCall instanceof StaticCall || !$factoryCall->class instanceof Name || !$factoryCall->name instanceof Identifier) { + continue; + } + if (!str_ends_with($factoryCall->class->toString(), 'Interceptor') || !isset($factoryCall->args[0])) { + continue; + } + $adviceCall = $factoryCall->args[0]->value; + if (!$adviceCall instanceof MethodCall || !$adviceCall->name instanceof Identifier) { + continue; + } + $aspectCall = $adviceCall->var; + if (!$aspectCall instanceof StaticCall || !$aspectCall->class instanceof Name || !$aspectCall->name instanceof Identifier || !str_ends_with($aspectCall->class->toString(), 'The') || !isset($aspectCall->args[0])) { + continue; + } + + if ($aspectCall->name->toString() === 'advice') { + $advisorId = $aspectCall->args[0]->value; + if ($advisorId instanceof String_) { + $advisorNames[] = $advisorId->value; + } + + continue; + } + + if ($aspectCall->name->toString() !== 'aspect') { + continue; + } + + $aspectClassConst = $aspectCall->args[0]->value; + if (!$aspectClassConst instanceof ClassConstFetch || !$aspectClassConst->class instanceof Name) { + continue; + } + + $aspectName = $aspectClassConst->class->toString(); + $advisorNames[] = ($useAliases[$aspectName] ?? $aspectName) . '->' . $adviceCall->name->toString(); + } + + return $advisorNames; + } + /** * Creates \Go\ParserReflection\ReflectionClass instance that introspects class without loading class into memory. * diff --git a/tests/Proxy/ClassProxyGeneratorTest.php b/tests/Proxy/ClassProxyGeneratorTest.php index 5db88cc4..5838e1d9 100644 --- a/tests/Proxy/ClassProxyGeneratorTest.php +++ b/tests/Proxy/ClassProxyGeneratorTest.php @@ -12,6 +12,8 @@ namespace Go\Proxy; +use Go\Aop\Framework\BeforeInterceptor; +use Go\Aop\Framework\GeneratedInterceptor; use Go\Stubs\ClassWithMixedSources; use Go\Stubs\First; use Go\Stubs\FirstStatic; @@ -40,7 +42,7 @@ public function testGenerateProxyMethod(string $className, string $methodName): $reflectionClass = new ReflectionClass($className); $classAdvices = [ 'method' => [ - $methodName => ['test'] + $methodName => [self::testAdvice()] ] ]; @@ -61,7 +63,7 @@ public function testGenerateProxyMethod(string $className, string $methodName): // Proxy intercepted method delegates to the join-point invocation chain $this->assertStringContainsString( - "InterceptorInjector::forMethod(self::class, '{$methodName}'", + "InterceptorInjector::forMethod(", $proxyFileContent, 'Proxy method body must delegate to the join-point invocation chain' ); @@ -75,8 +77,8 @@ public function testGenerateWithPropertyInterception(): void $reflectionClass = new ReflectionClass(First::class); $classAdvices = [ 'prop' => [ - 'public' => ['test'], - 'protected' => ['test'], + 'public' => [self::testAdvice()], + 'protected' => [self::testAdvice()], ] ]; @@ -88,14 +90,14 @@ public function testGenerateWithPropertyInterception(): void $proxyFileContent, 'Proxy with property advices must re-declare intercepted properties with native hooks' ); - $this->assertStringContainsString("InterceptorInjector::forProperty(self::class, 'public'", $proxyFileContent); + $this->assertStringContainsString("InterceptorInjector::forProperty(", $proxyFileContent); $this->assertStringContainsString( "/** @var FieldAccess \$__joinPoint */", $proxyFileContent, 'Proxy with property advices must route writes through join points in property hooks' ); $this->assertStringContainsString( - "set {\n /** @var FieldAccess \$__joinPoint */\n static \$__joinPoint = InterceptorInjector::forProperty(self::class", + "set {\n /** @var FieldAccess \$__joinPoint */\n static \$__joinPoint = InterceptorInjector::forProperty(", $proxyFileContent ); } @@ -111,7 +113,7 @@ public function testGenerateWithPropertyInterceptionPreservesAsymmetricVisibilit $reflectionClass = new ReflectionClass($target); $classAdvices = [ 'prop' => [ - 'name' => ['test'], + 'name' => [self::testAdvice()], ] ]; @@ -136,7 +138,7 @@ public function testGenerateWithClassTypedPropertyUsesFullyQualifiedTypeInFieldA $reflectionClass = new ReflectionClass($target); $classAdvices = [ 'prop' => [ - 'privateProperty' => ['test'], + 'privateProperty' => [self::testAdvice()], ], ]; @@ -174,9 +176,9 @@ public function __construct() $reflectionClass = new ReflectionClass($target); $classAdvices = [ 'prop' => [ - 'intercepted' => ['test'], - 'readonly' => ['test'], - 'alreadyHooked' => ['test'], + 'intercepted' => [self::testAdvice()], + 'readonly' => [self::testAdvice()], + 'alreadyHooked' => [self::testAdvice()], ] ]; @@ -195,7 +197,7 @@ public function testGenerateWithFinalPropertyDeclaredInCurrentClass(): void $reflectionClass = new ReflectionClass($target); $classAdvices = [ 'prop' => [ - 'final' => ['test'], + 'final' => [self::testAdvice()], ], ]; @@ -203,7 +205,7 @@ public function testGenerateWithFinalPropertyDeclaredInCurrentClass(): void $proxyFileContent = "generate(); $this->assertStringContainsString("final public string \$final = 'final' {", $proxyFileContent); - $this->assertStringContainsString("InterceptorInjector::forProperty(self::class, 'final'", $proxyFileContent); + $this->assertStringContainsString("InterceptorInjector::forProperty(", $proxyFileContent); } /** @@ -214,8 +216,8 @@ public function testGenerateWithParentPropertyInterceptionIncludesPublicAndProte $reflectionClass = new ReflectionClass(PropertyInheritanceChild::class); $classAdvices = [ 'prop' => [ - 'parentPublic' => ['test'], - 'parentProtected' => ['test'], + 'parentPublic' => [self::testAdvice()], + 'parentProtected' => [self::testAdvice()], ], ]; @@ -224,8 +226,8 @@ public function testGenerateWithParentPropertyInterceptionIncludesPublicAndProte $this->assertStringContainsString("public string \$parentPublic = 'parent-public' {", $proxyFileContent); $this->assertStringContainsString("protected string \$parentProtected = 'parent-protected' {", $proxyFileContent); - $this->assertStringContainsString("InterceptorInjector::forProperty(self::class, 'parentPublic'", $proxyFileContent); - $this->assertStringContainsString("InterceptorInjector::forProperty(self::class, 'parentProtected'", $proxyFileContent); + $this->assertStringContainsString("InterceptorInjector::forProperty(", $proxyFileContent); + $this->assertStringContainsString("InterceptorInjector::forProperty(", $proxyFileContent); } /** @@ -239,7 +241,7 @@ public function testGenerateWithUninitializedTypedPropertyInterceptionAddsInitia $reflectionClass = new ReflectionClass($target); $classAdvices = [ 'prop' => [ - 'uninitialized' => ['test'], + 'uninitialized' => [self::testAdvice()], ], ]; @@ -284,7 +286,7 @@ public function appendValue(int $value): void $reflectionClass = new ReflectionClass($target); $classAdvices = [ 'prop' => [ - 'items' => ['test'], + 'items' => [self::testAdvice()], ], ]; @@ -311,10 +313,10 @@ public function testGenerateInterceptsPrivateMethods(): void $reflectionClass = new ReflectionClass(First::class); $classAdvices = [ 'method' => [ - 'privateMethod' => ['test'], // private function + 'privateMethod' => [self::testAdvice()], // private function ], 'static' => [ - 'staticSelfPrivate' => ['test'], // private static function + 'staticSelfPrivate' => [self::testAdvice()], // private static function ], ]; @@ -330,8 +332,8 @@ public function testGenerateInterceptsPrivateMethods(): void $this->assertStringContainsString('private static function staticSelfPrivate(', $proxyFileContent); // Method bodies must call the join-point chain - $this->assertStringContainsString("InterceptorInjector::forMethod(self::class, 'privateMethod'", $proxyFileContent); - $this->assertStringContainsString("InterceptorInjector::forStaticMethod(self::class, 'staticSelfPrivate'", $proxyFileContent); + $this->assertStringContainsString("InterceptorInjector::forMethod(", $proxyFileContent); + $this->assertStringContainsString("InterceptorInjector::forStaticMethod(", $proxyFileContent); } /** @@ -379,8 +381,8 @@ public function testGenerateProxyForClassUsingTraitMethods(): void // and also declares ownPublicMethod directly. $classAdvices = [ 'method' => [ - 'publicMethod' => ['test'], // defined in TraitAliasProxied - 'ownPublicMethod' => ['test'], // defined directly in ClassWithMixedSources + 'publicMethod' => [self::testAdvice()], // defined in TraitAliasProxied + 'ownPublicMethod' => [self::testAdvice()], // defined directly in ClassWithMixedSources ], ]; @@ -392,8 +394,8 @@ public function testGenerateProxyForClassUsingTraitMethods(): void $this->assertStringContainsString('__aop__ownPublicMethod', $proxyFileContent); // Both must delegate to the join-point chain - $this->assertStringContainsString("InterceptorInjector::forMethod(self::class, 'publicMethod'", $proxyFileContent); - $this->assertStringContainsString("InterceptorInjector::forMethod(self::class, 'ownPublicMethod'", $proxyFileContent); + $this->assertStringContainsString("InterceptorInjector::forMethod(", $proxyFileContent); + $this->assertStringContainsString("InterceptorInjector::forMethod(", $proxyFileContent); } /** @@ -410,7 +412,7 @@ public function testGenerateProxyForInheritedMethodDoesNotCreateTraitAlias(): vo $reflectionClass = new ReflectionClass(FirstStatic::class); $classAdvices = [ 'method' => [ - 'publicMethod' => ['test'], + 'publicMethod' => [self::testAdvice()], ], ]; @@ -422,7 +424,7 @@ public function testGenerateProxyForInheritedMethodDoesNotCreateTraitAlias(): vo $proxyFileContent ); $this->assertStringContainsString( - "InterceptorInjector::forMethod(self::class, 'publicMethod'", + "InterceptorInjector::forMethod(", $proxyFileContent ); // Inherited instance method must use parent:: first-class callable (no __aop__ alias available) @@ -446,7 +448,7 @@ public function testGenerateProxyForInheritedStaticMethodUsesParentCallable(): v $reflectionClass = new ReflectionClass(FirstStatic::class); $classAdvices = [ 'static' => [ - 'staticSelfPublic' => ['test'], + 'staticSelfPublic' => [self::testAdvice()], ], ]; @@ -462,7 +464,7 @@ public function testGenerateProxyForInheritedStaticMethodUsesParentCallable(): v // Must delegate to the join-point chain $this->assertStringContainsString( - "InterceptorInjector::forStaticMethod(self::class, 'staticSelfPublic'", + "InterceptorInjector::forStaticMethod(", $proxyFileContent ); @@ -491,8 +493,8 @@ public function normalMethod(): void {} $reflectionClass = new ReflectionClass($target); $classAdvices = [ 'method' => [ - 'oldMethod' => ['test'], - 'normalMethod' => ['test'], + 'oldMethod' => [self::testAdvice()], + 'normalMethod' => [self::testAdvice()], ], ]; @@ -531,7 +533,7 @@ public function testTraitAdoptionUsesShortNameWhenSameNamespace(): void $reflectionClass = new ReflectionClass(First::class); $classAdvices = [ 'method' => [ - 'publicMethod' => ['test'], + 'publicMethod' => [self::testAdvice()], ], ]; @@ -557,7 +559,7 @@ public function testTraitAdoptionUsesFqcnWhenDifferentNamespace(): void $reflectionClass = new ReflectionClass(First::class); $classAdvices = [ 'method' => [ - 'publicMethod' => ['test'], + 'publicMethod' => [self::testAdvice()], ], ]; @@ -572,6 +574,11 @@ public function testTraitAdoptionUsesFqcnWhenDifferentNamespace(): void $this->assertStringNotContainsString('use First__AopProxied {', $output); } + private static function testAdvice(): GeneratedInterceptor + { + return GeneratedInterceptor::fromAdvice('test', new BeforeInterceptor(static function (): void {})); + } + /** * Provides list of methods with expected attributes * diff --git a/tests/Proxy/EnumProxyGeneratorTest.php b/tests/Proxy/EnumProxyGeneratorTest.php index 05da91fa..8156d7aa 100644 --- a/tests/Proxy/EnumProxyGeneratorTest.php +++ b/tests/Proxy/EnumProxyGeneratorTest.php @@ -12,6 +12,8 @@ namespace Go\Proxy; +use Go\Aop\Framework\BeforeInterceptor; +use Go\Aop\Framework\GeneratedInterceptor; use Go\Stubs\StubBackedEnum; use Go\Stubs\StubConstExprBackedEnum; use PHPUnit\Framework\TestCase; @@ -41,7 +43,7 @@ public function testGenerateProxyEnumMethod(): void $traitName = 'Go\\Stubs\\StubBackedEnum__AopProxied'; $classAdvices = [ 'method' => [ - 'label' => ['advisor.StubBackedEnum->label'], + 'label' => [self::testAdvice('advisor.StubBackedEnum->label')], ], ]; @@ -80,7 +82,7 @@ public function testGenerateProxyEnumWithStaticMethod(): void $traitName = 'Go\\Stubs\\StubBackedEnum__AopProxied'; $classAdvices = [ 'static' => [ - 'fromLabel' => ['advisor.StubBackedEnum->fromLabel'], + 'fromLabel' => [self::testAdvice('advisor.StubBackedEnum->fromLabel')], ], ]; @@ -103,7 +105,7 @@ public function testGeneratePreservesEnumCases(): void { $reflectionClass = new ReflectionClass(StubBackedEnum::class); $classAdvices = [ - 'method' => ['label' => ['advisor']], + 'method' => ['label' => [self::testAdvice('advisor')]], ]; $generator = new EnumProxyGenerator($reflectionClass, 'Go\\Stubs\\StubBackedEnum__AopProxied', $classAdvices, false); @@ -125,7 +127,7 @@ public function testGeneratePreservesConstantExpressionCaseValues(): void { $reflectionClass = new ReflectionClass(StubConstExprBackedEnum::class); $classAdvices = [ - 'method' => ['describe' => ['advisor']], + 'method' => ['describe' => [self::testAdvice('advisor')]], ]; $generator = new EnumProxyGenerator( @@ -151,7 +153,7 @@ public function testGenerateBackedEnumPreservesType(): void { $reflectionClass = new ReflectionClass(StubBackedEnum::class); $classAdvices = [ - 'method' => ['label' => ['advisor']], + 'method' => ['label' => [self::testAdvice('advisor')]], ]; $generator = new EnumProxyGenerator($reflectionClass, 'Go\\Stubs\\StubBackedEnum__AopProxied', $classAdvices, false); @@ -169,7 +171,7 @@ public function testGenerateDoesNotEmitLegacyJoinPointMechanism(): void { $reflectionClass = new ReflectionClass(StubBackedEnum::class); $classAdvices = [ - 'method' => ['label' => ['advisor']], + 'method' => ['label' => [self::testAdvice('advisor')]], ]; $generator = new EnumProxyGenerator($reflectionClass, 'Go\\Stubs\\StubBackedEnum__AopProxied', $classAdvices, false); @@ -192,7 +194,7 @@ public function testGenerateDoesNotIncludeBuiltinEnumInterfaces(): void { $reflectionClass = new ReflectionClass(StubBackedEnum::class); $classAdvices = [ - 'method' => ['label' => ['advisor']], + 'method' => ['label' => [self::testAdvice('advisor')]], ]; $generator = new EnumProxyGenerator($reflectionClass, 'Go\\Stubs\\StubBackedEnum__AopProxied', $classAdvices, false); @@ -218,7 +220,7 @@ public function testTraitAdoptionUsesShortNameWhenSameNamespace(): void $reflectionClass = new ReflectionClass(StubBackedEnum::class); $classAdvices = [ 'method' => [ - 'label' => ['advisor'], + 'label' => [self::testAdvice('advisor')], ], ]; @@ -242,7 +244,7 @@ public function testTraitAdoptionUsesFqcnWhenDifferentNamespace(): void $reflectionClass = new ReflectionClass(StubBackedEnum::class); $classAdvices = [ 'method' => [ - 'label' => ['advisor'], + 'label' => [self::testAdvice('advisor')], ], ]; @@ -266,12 +268,12 @@ public function testGenerateFiltersOutBuiltinEnumMethods(): void $reflectionClass = new ReflectionClass(StubBackedEnum::class); $classAdvices = [ 'method' => [ - 'label' => ['advisor'], - 'cases' => ['advisor'], // built-in, must be ignored - 'from' => ['advisor'], // built-in, must be ignored + 'label' => [self::testAdvice('advisor')], + 'cases' => [self::testAdvice('advisor')], // built-in, must be ignored + 'from' => [self::testAdvice('advisor')], // built-in, must be ignored ], 'static' => [ - 'tryFrom' => ['advisor'], // built-in, must be ignored + 'tryFrom' => [self::testAdvice('advisor')], // built-in, must be ignored ], ]; @@ -284,4 +286,9 @@ public function testGenerateFiltersOutBuiltinEnumMethods(): void $this->assertStringNotContainsString('__aop__from', $output); $this->assertStringNotContainsString('__aop__tryFrom', $output); } + + private static function testAdvice(string $advisorId): GeneratedInterceptor + { + return GeneratedInterceptor::fromAdvice($advisorId, new BeforeInterceptor(static function (): void {})); + } } diff --git a/tests/Proxy/Generator/InterceptorListGeneratorTest.php b/tests/Proxy/Generator/InterceptorListGeneratorTest.php new file mode 100644 index 00000000..60b599fc --- /dev/null +++ b/tests/Proxy/Generator/InterceptorListGeneratorTest.php @@ -0,0 +1,69 @@ + null, 20) + ); + + $code = (new InterceptorListGenerator([$descriptor]))->generate(); + + $this->assertSame([], InterceptorListGenerator::aspectClasses([$descriptor])); + $this->assertSame( + <<<'PHP' +[ + Interceptor::around(The::advice('manual.around'), order: 20), + ] +PHP, + $code + ); + } + + public function testGeneratesMatchingFactoryCallForEveryAdviceType(): void + { + $noop = static fn(): mixed => null; + $descriptors = [ + GeneratedInterceptor::fromAdvice('manual.before', new BeforeInterceptor($noop)), + GeneratedInterceptor::fromAdvice('manual.after', new AfterInterceptor($noop)), + GeneratedInterceptor::fromAdvice('manual.around', new AroundInterceptor($noop)), + GeneratedInterceptor::fromAdvice('manual.afterThrowing', new AfterThrowingInterceptor($noop)), + ]; + + $code = (new InterceptorListGenerator($descriptors))->generate(); + + $this->assertSame( + <<<'PHP' +[ + Interceptor::before(The::advice('manual.before')), + Interceptor::after(The::advice('manual.after')), + Interceptor::around(The::advice('manual.around')), + Interceptor::afterThrowing(The::advice('manual.afterThrowing')), + ] +PHP, + $code + ); + } + + public function testRejectsPlainStringAdvisorIds(): void + { + $this->expectException(AspectException::class); + $this->expectExceptionMessage('expects generated interceptor descriptors'); + + new InterceptorListGenerator(['advisor.Some\Aspect->advice']); + } +} diff --git a/tests/Proxy/TraitProxyGeneratorTest.php b/tests/Proxy/TraitProxyGeneratorTest.php index bfb992e4..f29d5ce1 100644 --- a/tests/Proxy/TraitProxyGeneratorTest.php +++ b/tests/Proxy/TraitProxyGeneratorTest.php @@ -12,6 +12,8 @@ namespace Go\Proxy; +use Go\Aop\Framework\BeforeInterceptor; +use Go\Aop\Framework\GeneratedInterceptor; use Go\Stubs\TraitAliasProxied; use Go\Stubs\TraitWithClassTypedProperty; use PHPUnit\Framework\TestCase; @@ -42,7 +44,7 @@ public function testGenerateTraitWithInterceptedInstanceMethod(): void $reflectionTrait = new ReflectionClass(TraitAliasProxied::class); $traitAdvices = [ 'method' => [ - 'publicMethod' => ['advisor.TraitAliasProxied->publicMethod'], + 'publicMethod' => [self::testAdvice('advisor.TraitAliasProxied->publicMethod')], ], ]; @@ -82,7 +84,7 @@ public function testGenerateTraitWithInterceptedStaticMethod(): void $reflectionTrait = new ReflectionClass(TraitAliasProxied::class); $traitAdvices = [ 'static' => [ - 'staticPublicMethod' => ['advisor.TraitAliasProxied->staticPublicMethod'], + 'staticPublicMethod' => [self::testAdvice('advisor.TraitAliasProxied->staticPublicMethod')], ], ]; @@ -113,11 +115,11 @@ public function testGenerateTraitWithMultipleInterceptedMethods(): void $reflectionTrait = new ReflectionClass(TraitAliasProxied::class); $traitAdvices = [ 'method' => [ - 'publicMethod' => ['advisor1'], - 'protectedMethod' => ['advisor2'], + 'publicMethod' => [self::testAdvice('advisor1')], + 'protectedMethod' => [self::testAdvice('advisor2')], ], 'static' => [ - 'staticPublicMethod' => ['advisor3'], + 'staticPublicMethod' => [self::testAdvice('advisor3')], ], ]; @@ -137,9 +139,9 @@ public function testGenerateTraitWithMultipleInterceptedMethods(): void // Three separate injector calls (one per intercepted method) $this->assertSame(2, substr_count($output, 'InterceptorInjector::forMethod')); $this->assertSame(1, substr_count($output, 'InterceptorInjector::forStaticMethod')); - $this->assertStringContainsString("forMethod(self::class, 'publicMethod'", $output); - $this->assertStringContainsString("forMethod(self::class, 'protectedMethod'", $output); - $this->assertStringContainsString("forStaticMethod(self::class, 'staticPublicMethod'", $output); + $this->assertStringContainsString("'publicMethod'", $output); + $this->assertStringContainsString("'protectedMethod'", $output); + $this->assertStringContainsString("'staticPublicMethod'", $output); } /** @@ -150,7 +152,7 @@ public function testGenerateDoesNotEmitLegacyJoinPointMechanism(): void { $reflectionTrait = new ReflectionClass(TraitAliasProxied::class); $traitAdvices = [ - 'method' => ['publicMethod' => ['advisor']], + 'method' => ['publicMethod' => [self::testAdvice('advisor')]], ]; $generator = new TraitProxyGenerator( @@ -175,7 +177,7 @@ public function testMethodBodyUsesPerMethodStaticCaching(): void { $reflectionTrait = new ReflectionClass(TraitAliasProxied::class); $traitAdvices = [ - 'method' => ['publicMethod' => ['advisor']], + 'method' => ['publicMethod' => [self::testAdvice('advisor')]], ]; $generator = new TraitProxyGenerator( @@ -200,7 +202,7 @@ public function testGenerateTraitWithInterceptedProperty(): void $reflectionTrait = new ReflectionClass(TraitAliasProxied::class); $traitAdvices = [ 'prop' => [ - 'public' => ['advisor.TraitAliasProxied->public'], + 'public' => [self::testAdvice('advisor.TraitAliasProxied->public')], ], ]; @@ -215,7 +217,7 @@ public function testGenerateTraitWithInterceptedProperty(): void $this->assertStringContainsString('public int $public = 326 {', $output); $this->assertStringContainsString('static $__joinPoint = InterceptorInjector::forProperty', $output); - $this->assertStringContainsString("InterceptorInjector::forProperty(self::class, 'public'", $output); + $this->assertStringContainsString("InterceptorInjector::forProperty(", $output); $this->assertStringContainsString('FieldAccessType::READ', $output); $this->assertStringContainsString('FieldAccessType::WRITE', $output); $this->assertStringNotContainsString('$__joinPoints[', $output); @@ -226,7 +228,7 @@ public function testGenerateTraitWithClassTypedPropertyUsesFullyQualifiedTypeInF $reflectionTrait = new ReflectionClass(TraitWithClassTypedProperty::class); $traitAdvices = [ 'prop' => [ - 'privateProperty' => ['advisor.TraitWithClassTypedProperty->privateProperty'], + 'privateProperty' => [self::testAdvice('advisor.TraitWithClassTypedProperty->privateProperty')], ], ]; @@ -254,7 +256,7 @@ public function testTraitAdoptionUsesShortNameWhenSameNamespace(): void $reflectionTrait = new ReflectionClass(TraitAliasProxied::class); $traitAdvices = [ 'method' => [ - 'publicMethod' => ['advisor'], + 'publicMethod' => [self::testAdvice('advisor')], ], ]; @@ -278,7 +280,7 @@ public function testTraitAdoptionUsesFqcnWhenDifferentNamespace(): void $reflectionTrait = new ReflectionClass(TraitAliasProxied::class); $traitAdvices = [ 'method' => [ - 'publicMethod' => ['advisor'], + 'publicMethod' => [self::testAdvice('advisor')], ], ]; @@ -292,4 +294,9 @@ public function testTraitAdoptionUsesFqcnWhenDifferentNamespace(): void $this->assertStringContainsString('\\Other\\Namespace\\TraitAliasProxied__AopProxied::publicMethod as private __aop__publicMethod', $output); $this->assertStringNotContainsString('use TraitAliasProxied__AopProxied {', $output); } + + private static function testAdvice(string $advisorId): GeneratedInterceptor + { + return GeneratedInterceptor::fromAdvice($advisorId, new BeforeInterceptor(static function (): void {})); + } } diff --git a/tests/Stubs/AbstractInterceptorMock.php b/tests/Stubs/AbstractInterceptorMock.php index 75efb7eb..57dc097c 100644 --- a/tests/Stubs/AbstractInterceptorMock.php +++ b/tests/Stubs/AbstractInterceptorMock.php @@ -13,6 +13,7 @@ namespace Go\Stubs; use Closure; +use Go\Aop\AdviceTypeEnum; use Go\Aop\Framework\AbstractInterceptor; use Go\Aop\Intercept\Joinpoint; @@ -40,4 +41,9 @@ public function invoke(Joinpoint $joinpoint): Joinpoint { return $joinpoint; } + + public function getType(): AdviceTypeEnum + { + return AdviceTypeEnum::Before; + } } diff --git a/tests/Stubs/AttributeAspectLoaderExtensionTestPrivateAspect.php b/tests/Stubs/AttributeAspectLoaderExtensionTestPrivateAspect.php new file mode 100644 index 00000000..f5638667 --- /dev/null +++ b/tests/Stubs/AttributeAspectLoaderExtensionTestPrivateAspect.php @@ -0,0 +1,25 @@ + + * + * This source file is subject to the license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Go\Stubs; + +use Go\Aop\Aspect; +use Go\Aop\Intercept\MethodInvocation; +use Go\Lang\Attribute\Before; + +final class AttributeAspectLoaderExtensionTestPrivateAspect implements Aspect +{ + #[Before('execution(public NonExistent\**->*(*))')] + private function privateAdvice(MethodInvocation $invocation): void + { + } +} diff --git a/tests/Stubs/AttributeAspectLoaderExtensionTestPublicAspect.php b/tests/Stubs/AttributeAspectLoaderExtensionTestPublicAspect.php new file mode 100644 index 00000000..fd676587 --- /dev/null +++ b/tests/Stubs/AttributeAspectLoaderExtensionTestPublicAspect.php @@ -0,0 +1,25 @@ + + * + * This source file is subject to the license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Go\Stubs; + +use Go\Aop\Aspect; +use Go\Aop\Intercept\MethodInvocation; +use Go\Lang\Attribute\Before; + +final class AttributeAspectLoaderExtensionTestPublicAspect implements Aspect +{ + #[Before('execution(public NonExistent\**->*(*))')] + public function publicAdvice(MethodInvocation $invocation): void + { + } +}