From 8ec5ac677d447b2bcfd9358e502d2f0afe98f5db Mon Sep 17 00:00:00 2001 From: Alexander Lisachenko Date: Mon, 1 Jun 2026 12:26:50 +0300 Subject: [PATCH 1/7] feat(aop): generate first-class advice callables Generate proxy interceptors with The::aspect() first-class callables and pass interceptor instances directly into joinpoint initialization. Format generated joinpoint initialization across multiple lines and restore default interceptor state during unserialization. --- demos/Demo/Aspect/CachingAspect.php | 2 +- demos/Demo/Aspect/FluentInterfaceAspect.php | 2 +- demos/Demo/Aspect/HealthyLiveAspect.php | 6 +- src/Aop/Framework/AbstractInterceptor.php | 2 +- src/Aop/Framework/AbstractJoinpoint.php | 24 ++- src/Aop/Framework/GeneratedInterceptor.php | 72 ++++++++ src/Aop/Framework/Interceptor.php | 41 +++++ src/Aop/Framework/InterceptorInjector.php | 58 ++----- src/Aop/Framework/The.php | 32 ++++ src/Core/Container.php | 5 - src/Core/LazyAdvisorAccessor.php | 75 --------- .../Transformer/WeavingTransformer.php | 8 +- src/Proxy/ClassProxyGenerator.php | 96 +++++++---- src/Proxy/EnumProxyGenerator.php | 23 ++- src/Proxy/FunctionProxyGenerator.php | 42 ++++- src/Proxy/Generator/ClassGenerator.php | 2 +- src/Proxy/Generator/EnumGenerator.php | 2 +- src/Proxy/Generator/FunctionGenerator.php | 2 +- src/Proxy/Generator/GeneratedCodePrinter.php | 71 ++++++++ .../Generator/InterceptorListGenerator.php | 123 ++++++++++++++ src/Proxy/Generator/MethodGenerator.php | 2 +- src/Proxy/Generator/TraitGenerator.php | 2 +- .../Part/InterceptedPropertyGenerator.php | 12 +- .../TraitInterceptedPropertyGenerator.php | 12 +- src/Proxy/TraitProxyGenerator.php | 26 ++- tests/Aop/Framework/BaseInterceptorTest.php | 1 + .../Framework/GeneratedInterceptorTest.php | 23 +++ tests/Aop/Framework/InterceptorTest.php | 41 +++++ tests/Aop/Framework/TheTest.php | 56 +++++++ tests/Core/ContainerTest.php | 1 - .../Transformer/WeavingTransformerTest.php | 4 +- .../Transformer/_files/class-proxy.php | 66 +++++++- .../_files/final-readonly-class-proxy.php | 30 +++- .../Transformer/_files/php7-class-proxy.php | 156 ++++++++++++++++-- .../_files/php80-82-syntax-proxy.php | 24 ++- .../_files/php80-promoted-property-proxy.php | 56 ++++++- ...80-promoted-property-single-line-proxy.php | 31 +++- .../_files/php81-attr-args-proxy.php | 24 ++- .../_files/php81-enum-const-expr-proxy.php | 15 +- .../Transformer/_files/php81-enum-proxy.php | 12 +- .../_files/php83-override-proxy.php | 21 ++- tests/PhpUnit/ProxyClassReflectionHelper.php | 75 ++++++++- tests/Proxy/ClassProxyGeneratorTest.php | 24 +-- tests/Proxy/TraitProxyGeneratorTest.php | 8 +- 44 files changed, 1126 insertions(+), 284 deletions(-) create mode 100644 src/Aop/Framework/GeneratedInterceptor.php create mode 100644 src/Aop/Framework/Interceptor.php create mode 100644 src/Aop/Framework/The.php delete mode 100644 src/Core/LazyAdvisorAccessor.php create mode 100644 src/Proxy/Generator/GeneratedCodePrinter.php create mode 100644 src/Proxy/Generator/InterceptorListGenerator.php create mode 100644 tests/Aop/Framework/GeneratedInterceptorTest.php create mode 100644 tests/Aop/Framework/InterceptorTest.php create mode 100644 tests/Aop/Framework/TheTest.php 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/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..69cf90cd 100644 --- a/src/Aop/Framework/AbstractJoinpoint.php +++ b/src/Aop/Framework/AbstractJoinpoint.php @@ -16,6 +16,7 @@ use Go\Aop\AdviceAfter; use Go\Aop\AdviceAround; use Go\Aop\AdviceBefore; +use Go\Aop\IntroductionInfo; use Go\Aop\Intercept\Interceptor; use Go\Aop\Intercept\Joinpoint; use Go\Aop\OrderedAdvice; @@ -52,16 +53,16 @@ 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) { + fn(mixed $first, mixed $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, @@ -74,18 +75,27 @@ 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; + } + $flattenAdvices[$type][$name][] = $advice instanceof Advice + ? GeneratedInterceptor::fromAdvice((string) $advisorId, $advice) + : GeneratedInterceptor::fromAdvisorId((string) $advisorId); + } } } diff --git a/src/Aop/Framework/GeneratedInterceptor.php b/src/Aop/Framework/GeneratedInterceptor.php new file mode 100644 index 00000000..77d85c8f --- /dev/null +++ b/src/Aop/Framework/GeneratedInterceptor.php @@ -0,0 +1,72 @@ + + * + * 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 Go\Aop\Intercept\Interceptor; +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 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(); + if ($scopeClass === null || !is_subclass_of($scopeClass->name, Aspect::class)) { + throw new AspectException("Advisor {$advisorId} uses an unsupported non-aspect callable; generated first-class advice callables require aspect methods"); + } + + return new self( + match ($advice::class) { + BeforeInterceptor::class => 'before', + AfterInterceptor::class => 'after', + AroundInterceptor::class => 'around', + AfterThrowingInterceptor::class => 'afterThrowing', + default => throw new AspectException("Advisor {$advisorId} uses unsupported interceptor " . $advice::class), + }, + $scopeClass->name, + $reflectionAdvice->name, + $advice->getAdviceOrder(), + $advisorId + ); + } + + public static function fromAdvisorId(string $advisorId): self + { + $reference = str_starts_with($advisorId, 'advisor.') ? substr($advisorId, 8) : $advisorId; + [$aspectClass, $adviceMethod] = str_contains($reference, '->') + ? explode('->', $reference, 2) + : [$reference, 'advice']; + + return new self('before', $aspectClass, $adviceMethod, 0, $advisorId); + } +} 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..deac9107 --- /dev/null +++ b/src/Aop/Framework/The.php @@ -0,0 +1,32 @@ + + * + * 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\Aspect; +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 AspectKernel::getInstance()->getContainer()->getService($aspectClass); + } +} 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/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..cf80f033 100644 --- a/src/Proxy/ClassProxyGenerator.php +++ b/src/Proxy/ClassProxyGenerator.php @@ -13,6 +13,7 @@ namespace Go\Proxy; use Go\Aop\Framework\AbstractMethodInvocation; +use Go\Aop\Framework\GeneratedInterceptor; use Go\Aop\InitializationAware; use Go\Aop\Proxy; use Go\Aop\StaticInitializationAware; @@ -21,6 +22,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 +44,7 @@ class ClassProxyGenerator /** * List of advices that are used for generation of child * - * @var string[][][] + * @var array>> */ protected array $adviceNames = []; @@ -61,7 +63,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 +77,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,12 +177,19 @@ 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('Go\Aop\Framework\Interceptor'); + $classGenerator->addUse('Go\Aop\Framework\The'); + 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'); @@ -212,7 +227,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 +259,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 +276,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 +318,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 +346,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 +371,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 +393,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 +412,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..a6f7f702 100644 --- a/src/Proxy/EnumProxyGenerator.php +++ b/src/Proxy/EnumProxyGenerator.php @@ -13,11 +13,12 @@ namespace Go\Proxy; use Go\Aop\Framework\AbstractMethodInvocation; +use Go\Aop\Framework\GeneratedInterceptor; 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 +75,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, @@ -161,6 +162,13 @@ public function __construct( // 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('Go\Aop\Framework\Interceptor'); + $enumGenerator->addUse('Go\Aop\Framework\The'); + 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'); @@ -222,9 +230,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 +252,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..039d1118 100644 --- a/src/Proxy/FunctionProxyGenerator.php +++ b/src/Proxy/FunctionProxyGenerator.php @@ -12,12 +12,13 @@ namespace Go\Proxy; +use Go\Aop\Framework\GeneratedInterceptor; 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 +32,7 @@ class FunctionProxyGenerator /** * List of advices that are used for generation of child * - * @var string[][][] + * @var array>> */ protected array $adviceNames = []; @@ -44,7 +45,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 */ @@ -56,7 +57,14 @@ public function __construct( $this->fileGenerator = new FileGenerator(); $this->fileGenerator->setNamespace($namespace->getName()); $this->fileGenerator->addUse('Go\Aop\Framework\InterceptorInjector'); + $this->fileGenerator->addUse('Go\Aop\Framework\Interceptor'); + $this->fileGenerator->addUse('Go\Aop\Framework\The'); $this->fileGenerator->addUse('Go\Aop\Intercept\FunctionInvocation'); + foreach ($this->collectAspectClasses($adviceNames) as $aspectClass) { + if (str_contains($aspectClass, '\\')) { + $this->fileGenerator->addUse($aspectClass); + } + } $functionsContent = []; $functionAdvices = $adviceNames[AspectContainer::FUNCTION_PREFIX] ?? []; @@ -101,9 +109,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 +118,30 @@ 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) { + $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..bca0de45 --- /dev/null +++ b/src/Proxy/Generator/InterceptorListGenerator.php @@ -0,0 +1,123 @@ + + * + * 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\Framework\GeneratedInterceptor; +use PhpParser\Node\Arg; +use PhpParser\Node\ArrayItem; +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\VariadicPlaceholder; + +/** + * Renders generated interceptor descriptors as Interceptor::* factory calls. + * + * @internal + */ +final class InterceptorListGenerator +{ + /** + * @param list $interceptors + */ + public function __construct(private readonly array $interceptors) {} + + /** + * @param list $interceptors + * @return list + */ + public static function aspectClasses(array $interceptors): array + { + $classes = []; + foreach ($interceptors as $interceptor) { + $interceptor = self::normalize($interceptor); + $classes[$interceptor->aspectClass] = $interceptor->aspectClass; + } + + return array_values($classes); + } + + public function generate(string $indent): string + { + if ($this->interceptors === []) { + return '[]'; + } + + $lines = ['[']; + foreach ($this->normalizedInterceptors() as $interceptor) { + $lines[] = $indent . ' Interceptor::' . $interceptor->factoryMethod . '('; + $lines[] = $indent . ' The::aspect(' . self::shortClassName($interceptor->aspectClass) . '::class)->' . $interceptor->adviceMethod . '(...),'; + if ($interceptor->order !== 0) { + $lines[] = $indent . ' order: ' . $interceptor->order . ','; + } + $lines[] = $indent . ' ),'; + } + $lines[] = $indent . ']'; + + return implode("\n", $lines); + } + + public function getNode(): Array_ + { + return new Array_(array_map( + static fn(GeneratedInterceptor $interceptor): ArrayItem => new ArrayItem(self::createCallNode($interceptor)), + $this->normalizedInterceptors() + ), ['kind' => Array_::KIND_SHORT]); + } + + /** + * @return list + */ + private function normalizedInterceptors(): array + { + return array_map(self::normalize(...), $this->interceptors); + } + + private static function normalize(GeneratedInterceptor|string $interceptor): GeneratedInterceptor + { + if (is_string($interceptor)) { + return GeneratedInterceptor::fromAdvisorId($interceptor); + } + + return $interceptor; + } + + private static function createCallNode(GeneratedInterceptor $interceptor): StaticCall + { + $args = [ + new Arg(new MethodCall( + new StaticCall(new Name('The'), 'aspect', [ + new Arg(new ClassConstFetch(new Name(self::shortClassName($interceptor->aspectClass)), 'class')), + ]), + $interceptor->adviceMethod, + [new VariadicPlaceholder()] + )), + ]; + + if ($interceptor->order !== 0) { + $args[] = new Arg(new \PhpParser\Node\Scalar\Int_($interceptor->order), name: new Identifier('order')); + } + + return new StaticCall(new Name('Interceptor'), $interceptor->factoryMethod, $args); + } + + 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..645dbb7c 100644 --- a/src/Proxy/TraitProxyGenerator.php +++ b/src/Proxy/TraitProxyGenerator.php @@ -13,11 +13,12 @@ namespace Go\Proxy; use Go\Aop\Framework\AbstractMethodInvocation; +use Go\Aop\Framework\GeneratedInterceptor; 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 +36,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 +52,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(); @@ -87,6 +87,13 @@ public function __construct( // 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('Go\Aop\Framework\Interceptor'); + $traitGenerator->addUse('Go\Aop\Framework\The'); + 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'); @@ -132,9 +139,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 +161,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/BaseInterceptorTest.php b/tests/Aop/Framework/BaseInterceptorTest.php index d5e3440e..a80220ca 100644 --- a/tests/Aop/Framework/BaseInterceptorTest.php +++ b/tests/Aop/Framework/BaseInterceptorTest.php @@ -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..757ddebd --- /dev/null +++ b/tests/Aop/Framework/GeneratedInterceptorTest.php @@ -0,0 +1,23 @@ +expectException(AspectException::class); + $this->expectExceptionMessage('unsupported non-aspect callable'); + + GeneratedInterceptor::fromAdvice( + 'manual-advisor', + new BeforeInterceptor(static function (Joinpoint $joinpoint): void {}) + ); + } +} 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..61f6bd37 --- /dev/null +++ b/tests/Aop/Framework/TheTest.php @@ -0,0 +1,56 @@ +setValue(null, null); + } + + public function testReturnsRegisteredAspectInstance(): void + { + $this->initKernelWithContainerValues([]); + + $this->assertInstanceOf(TheTestAspect::class, The::aspect(TheTestAspect::class)); + } + + /** + * @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/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/WeavingTransformerTest.php b/tests/Instrument/Transformer/WeavingTransformerTest.php index cba7537f..a3c6c40e 100644 --- a/tests/Instrument/Transformer/WeavingTransformerTest.php +++ b/tests/Instrument/Transformer/WeavingTransformerTest.php @@ -587,8 +587,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); } /** diff --git a/tests/Instrument/Transformer/_files/class-proxy.php b/tests/Instrument/Transformer/_files/class-proxy.php index 375369da..861d9efc 100644 --- a/tests/Instrument/Transformer/_files/class-proxy.php +++ b/tests/Instrument/Transformer/_files/class-proxy.php @@ -2,6 +2,9 @@ declare(strict_types=1); namespace Test\ns1; use Go\Aop\Framework\InterceptorInjector; +use Go\Aop\Framework\Interceptor; +use Go\Aop\Framework\The; +use Test\ns1\TestClass; use Go\Aop\Intercept\DynamicMethodInvocation; use Go\Aop\Intercept\StaticMethodInvocation; class TestClass implements \Go\Aop\Proxy @@ -18,43 +21,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::aspect(TestClass::class)->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::aspect(TestClass::class)->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::aspect(TestClass::class)->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::aspect(TestClass::class)->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::aspect(TestClass::class)->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::aspect(TestClass::class)->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::aspect(TestClass::class)->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..365e2446 100644 --- a/tests/Instrument/Transformer/_files/final-readonly-class-proxy.php +++ b/tests/Instrument/Transformer/_files/final-readonly-class-proxy.php @@ -2,6 +2,9 @@ declare(strict_types=1); namespace Test\ns1; use Go\Aop\Framework\InterceptorInjector; +use Go\Aop\Framework\Interceptor; +use Go\Aop\Framework\The; +use Test\ns1\TestReadonlyClass; use Go\Aop\Intercept\DynamicMethodInvocation; use Go\Aop\Intercept\StaticMethodInvocation; final readonly class TestReadonlyClass implements \Go\Aop\Proxy @@ -14,19 +17,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::aspect(TestReadonlyClass::class)->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::aspect(TestReadonlyClass::class)->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::aspect(TestReadonlyClass::class)->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..d2e590d1 100644 --- a/tests/Instrument/Transformer/_files/php7-class-proxy.php +++ b/tests/Instrument/Transformer/_files/php7-class-proxy.php @@ -2,6 +2,9 @@ declare(strict_types=1); namespace Test\ns1; use Go\Aop\Framework\InterceptorInjector; +use Go\Aop\Framework\Interceptor; +use Go\Aop\Framework\The; +use Test\ns1\TestPhp7Class; use Go\Aop\Intercept\DynamicMethodInvocation; class TestPhp7Class implements \Go\Aop\Proxy { @@ -27,103 +30,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::aspect(TestPhp7Class::class)->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::aspect(TestPhp7Class::class)->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::aspect(TestPhp7Class::class)->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::aspect(TestPhp7Class::class)->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::aspect(TestPhp7Class::class)->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::aspect(TestPhp7Class::class)->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::aspect(TestPhp7Class::class)->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::aspect(TestPhp7Class::class)->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::aspect(TestPhp7Class::class)->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::aspect(TestPhp7Class::class)->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::aspect(TestPhp7Class::class)->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::aspect(TestPhp7Class::class)->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::aspect(TestPhp7Class::class)->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::aspect(TestPhp7Class::class)->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::aspect(TestPhp7Class::class)->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::aspect(TestPhp7Class::class)->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::aspect(TestPhp7Class::class)->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..e8b90b38 100644 --- a/tests/Instrument/Transformer/_files/php80-82-syntax-proxy.php +++ b/tests/Instrument/Transformer/_files/php80-82-syntax-proxy.php @@ -1,8 +1,10 @@ $__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::aspect(TestPhp80To82SyntaxClass::class)->__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::aspect(TestPhp80To82SyntaxClass::class)->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..fba47801 100644 --- a/tests/Instrument/Transformer/_files/php80-promoted-property-proxy.php +++ b/tests/Instrument/Transformer/_files/php80-promoted-property-proxy.php @@ -1,8 +1,10 @@ $__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::aspect(PromotedPropertyClass::class)->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::aspect(PromotedPropertyClass::class)->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::aspect(PromotedPropertyClass::class)->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::aspect(PromotedPropertyClass::class)->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::aspect(PromotedPropertyClass::class)->__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::aspect(PromotedPropertyClass::class)->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..d9388d21 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 @@ -1,8 +1,10 @@ $__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::aspect(SingleLinePromotedClass::class)->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::aspect(SingleLinePromotedClass::class)->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::aspect(SingleLinePromotedClass::class)->__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..c1abd696 100644 --- a/tests/Instrument/Transformer/_files/php81-attr-args-proxy.php +++ b/tests/Instrument/Transformer/_files/php81-attr-args-proxy.php @@ -1,8 +1,10 @@ $__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::aspect(TestAttributeArgsClass::class)->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::aspect(TestAttributeArgsClass::class)->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..a9a30364 100644 --- a/tests/Instrument/Transformer/_files/php81-enum-const-expr-proxy.php +++ b/tests/Instrument/Transformer/_files/php81-enum-const-expr-proxy.php @@ -1,8 +1,10 @@ $__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::aspect(ConstExprStatus::class)->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..b2deb9ad 100644 --- a/tests/Instrument/Transformer/_files/php81-enum-proxy.php +++ b/tests/Instrument/Transformer/_files/php81-enum-proxy.php @@ -2,6 +2,9 @@ declare(strict_types=1); namespace Test\ns1; use Go\Aop\Framework\InterceptorInjector; +use Go\Aop\Framework\Interceptor; +use Go\Aop\Framework\The; +use Test\ns1\TestStatus; use Go\Aop\Intercept\DynamicMethodInvocation; enum TestStatus : string implements \Go\Aop\Proxy { @@ -13,7 +16,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::aspect(TestStatus::class)->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..8f21ae57 100644 --- a/tests/Instrument/Transformer/_files/php83-override-proxy.php +++ b/tests/Instrument/Transformer/_files/php83-override-proxy.php @@ -2,6 +2,9 @@ declare(strict_types=1); namespace Test\ns1; use Go\Aop\Framework\InterceptorInjector; +use Go\Aop\Framework\Interceptor; +use Go\Aop\Framework\The; +use Test\ns1\TestClassWithOverride; use Go\Aop\Intercept\DynamicMethodInvocation; /** * PHP 8.3 — class with #[\Override] on an intercepted method. @@ -18,13 +21,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::aspect(TestClassWithOverride::class)->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::aspect(TestClassWithOverride::class)->normalMethod(...)), + ], + $this->__aop__normalMethod(...), + ); return $__joinPoint->__invoke($this); } } diff --git a/tests/PhpUnit/ProxyClassReflectionHelper.php b/tests/PhpUnit/ProxyClassReflectionHelper.php index 9d93cc29..2de3b817 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,65 @@ 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 || !str_ends_with($aspectCall->class->toString(), 'The') || !isset($aspectCall->args[0])) { + 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..7f1568d4 100644 --- a/tests/Proxy/ClassProxyGeneratorTest.php +++ b/tests/Proxy/ClassProxyGeneratorTest.php @@ -61,7 +61,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' ); @@ -88,14 +88,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 ); } @@ -203,7 +203,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); } /** @@ -224,8 +224,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); } /** @@ -330,8 +330,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); } /** @@ -392,8 +392,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); } /** @@ -422,7 +422,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) @@ -462,7 +462,7 @@ public function testGenerateProxyForInheritedStaticMethodUsesParentCallable(): v // Must delegate to the join-point chain $this->assertStringContainsString( - "InterceptorInjector::forStaticMethod(self::class, 'staticSelfPublic'", + "InterceptorInjector::forStaticMethod(", $proxyFileContent ); diff --git a/tests/Proxy/TraitProxyGeneratorTest.php b/tests/Proxy/TraitProxyGeneratorTest.php index bfb992e4..dcca5db7 100644 --- a/tests/Proxy/TraitProxyGeneratorTest.php +++ b/tests/Proxy/TraitProxyGeneratorTest.php @@ -137,9 +137,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); } /** @@ -215,7 +215,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); From b1f189d28d4ea1f31a233ee11b4c07d77327a4be Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 29 Aug 2026 18:52:57 +0000 Subject: [PATCH 2/7] feat(aop)!: replace advice marker interfaces with AdviceTypeEnum Drop the AdviceBefore/AdviceAfter/AdviceAround marker interfaces and add a required Advice::getType(): AdviceTypeEnum method instead. The new backed enum carries the advice kind (before, after, afterThrowing, around, introduction) together with its invocation priority, so joinpoint sorting and generated interceptor factory resolution no longer rely on instanceof checks against marker interfaces or a hard-coded interceptor class map. BREAKING CHANGE: the AdviceBefore, AdviceAfter and AdviceAround interfaces are removed and every Advice implementation must now implement getType(). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_019M9gpum1Rgasc2KtBZ3UhD --- src/Aop/Advice.php | 7 +++ src/Aop/AdviceAfter.php | 22 -------- src/Aop/AdviceAround.php | 22 -------- src/Aop/AdviceBefore.php | 22 -------- src/Aop/AdviceTypeEnum.php | 50 +++++++++++++++++++ src/Aop/Framework/AbstractJoinpoint.php | 20 ++++---- src/Aop/Framework/AfterInterceptor.php | 9 +++- .../Framework/AfterThrowingInterceptor.php | 9 +++- src/Aop/Framework/AroundInterceptor.php | 9 +++- src/Aop/Framework/BeforeInterceptor.php | 9 +++- src/Aop/Framework/GeneratedInterceptor.php | 9 +--- src/Aop/Framework/TraitIntroductionInfo.php | 6 +++ src/Aop/Intercept/Interceptor.php | 1 + tests/Aop/Framework/AbstractJoinpointTest.php | 49 ++++++++++++------ tests/Aop/Framework/BaseInterceptorTest.php | 2 +- tests/Stubs/AbstractInterceptorMock.php | 6 +++ 16 files changed, 146 insertions(+), 106 deletions(-) delete mode 100644 src/Aop/AdviceAfter.php delete mode 100644 src/Aop/AdviceAround.php delete mode 100644 src/Aop/AdviceBefore.php create mode 100644 src/Aop/AdviceTypeEnum.php 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/AbstractJoinpoint.php b/src/Aop/Framework/AbstractJoinpoint.php index 69cf90cd..f306d2cd 100644 --- a/src/Aop/Framework/AbstractJoinpoint.php +++ b/src/Aop/Framework/AbstractJoinpoint.php @@ -13,9 +13,6 @@ namespace Go\Aop\Framework; use Go\Aop\Advice; -use Go\Aop\AdviceAfter; -use Go\Aop\AdviceAround; -use Go\Aop\AdviceBefore; use Go\Aop\IntroductionInfo; use Go\Aop\Intercept\Interceptor; use Go\Aop\Intercept\Joinpoint; @@ -62,12 +59,17 @@ public static function sortAdvices(array $advices): array $sortedAdvices = $advices; uasort( $sortedAdvices, - fn(mixed $first, mixed $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; } ); 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 index 77d85c8f..f1fccce9 100644 --- a/src/Aop/Framework/GeneratedInterceptor.php +++ b/src/Aop/Framework/GeneratedInterceptor.php @@ -15,7 +15,6 @@ use Go\Aop\Advice; use Go\Aop\Aspect; use Go\Aop\AspectException; -use Go\Aop\Intercept\Interceptor; use ReflectionFunction; /** @@ -46,13 +45,7 @@ public static function fromAdvice(string $advisorId, Advice $advice): self } return new self( - match ($advice::class) { - BeforeInterceptor::class => 'before', - AfterInterceptor::class => 'after', - AroundInterceptor::class => 'around', - AfterThrowingInterceptor::class => 'afterThrowing', - default => throw new AspectException("Advisor {$advisorId} uses unsupported interceptor " . $advice::class), - }, + $advice->getType()->value, $scopeClass->name, $reflectionAdvice->name, $advice->getAdviceOrder(), 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/tests/Aop/Framework/AbstractJoinpointTest.php b/tests/Aop/Framework/AbstractJoinpointTest.php index 0454bfd6..691d43f1 100644 --- a/tests/Aop/Framework/AbstractJoinpointTest.php +++ b/tests/Aop/Framework/AbstractJoinpointTest.php @@ -4,9 +4,8 @@ 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\OrderedAdvice; use PHPUnit\Framework\TestCase; @@ -24,15 +23,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 +45,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 +85,18 @@ public static function sortingTestSource(): array ]; } + 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 +106,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 a80220ca..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()); } 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; + } } From 487a1f8a030db0ea72ed8bb5526c9b0ca455667e Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 29 Aug 2026 18:53:19 +0000 Subject: [PATCH 3/7] feat(aop)!: require aspect advice methods to be public Advice methods discovered through attributes must now be declared public. Generated proxies reference advices as first-class callables on the aspect instance (The::aspect(SomeAspect::class)->adviceMethod(...)), which is only possible when the advice method is callable from the outside. The attribute aspect loader now fails fast with an AspectException instead of producing a proxy that would fatal at runtime. Methods holding only a #[Pointcut] attribute may stay protected/private as before. BREAKING CHANGE: aspects with protected or private advice methods are rejected during aspect loading and must make those methods public. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_019M9gpum1Rgasc2KtBZ3UhD --- src/Core/AttributeAspectLoaderExtension.php | 6 +++ .../AttributeAspectLoaderExtensionTest.php | 43 +++++++++++++++++++ ...AspectLoaderExtensionTestPrivateAspect.php | 25 +++++++++++ ...eAspectLoaderExtensionTestPublicAspect.php | 25 +++++++++++ 4 files changed, 99 insertions(+) create mode 100644 tests/Core/AttributeAspectLoaderExtensionTest.php create mode 100644 tests/Stubs/AttributeAspectLoaderExtensionTestPrivateAspect.php create mode 100644 tests/Stubs/AttributeAspectLoaderExtensionTestPublicAspect.php 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/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/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 + { + } +} From 94f61848e6cadc7f17b75f7dde4f4f1f953cfd66 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 29 Aug 2026 18:53:50 +0000 Subject: [PATCH 4/7] feat(aop): resolve closure advices through The::advice() accessor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aspect-method advices stay the primary path: proxies reference them directly as first-class callables via The::aspect(SomeAspect::class)->method(...). Advices registered in the container as plain closures (or interceptors whose closure is not scoped to an Aspect class) can now be woven too — the GeneratedInterceptor descriptor marks them as container-backed and the generated code resolves them lazily with The::advice('advisorId'), which unwraps Advisor and AbstractInterceptor values down to the raw closure. The joinpoint flattening now rejects non-Advice advisor values loudly instead of guessing an aspect method from the advisor id (fromAdvisorId is removed), and InterceptorListGenerator only accepts generated interceptor descriptors. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_019M9gpum1Rgasc2KtBZ3UhD --- src/Aop/Framework/AbstractJoinpoint.php | 10 +- src/Aop/Framework/GeneratedInterceptor.php | 32 +++--- src/Aop/Framework/The.php | 28 +++++- src/Proxy/FunctionProxyGenerator.php | 4 +- .../Generator/InterceptorListGenerator.php | 97 +++++++++++-------- tests/Aop/Framework/AbstractJoinpointTest.php | 58 +++++++++++ .../Framework/GeneratedInterceptorTest.php | 79 ++++++++++++++- tests/Aop/Framework/TheTest.php | 91 +++++++++++++++++ .../Transformer/Php85AuditScratchTest.php | 5 +- .../Transformer/WeavingTransformerTest.php | 37 ++++--- .../Transformer/_files/class-proxy.php | 15 ++- .../_files/final-readonly-class-proxy.php | 7 +- .../Transformer/_files/php7-class-proxy.php | 35 ++++--- .../_files/php80-82-syntax-proxy.php | 8 +- .../_files/php80-promoted-property-proxy.php | 16 +-- ...80-promoted-property-single-line-proxy.php | 10 +- .../_files/php81-attr-args-proxy.php | 8 +- .../_files/php81-enum-const-expr-proxy.php | 6 +- .../Transformer/_files/php81-enum-proxy.php | 3 +- .../_files/php83-override-proxy.php | 5 +- tests/PhpUnit/ProxyClassReflectionHelper.php | 16 ++- tests/Proxy/ClassProxyGeneratorTest.php | 53 +++++----- tests/Proxy/EnumProxyGeneratorTest.php | 33 ++++--- .../InterceptorListGeneratorTest.php | 69 +++++++++++++ tests/Proxy/TraitProxyGeneratorTest.php | 29 +++--- 25 files changed, 558 insertions(+), 196 deletions(-) create mode 100644 tests/Proxy/Generator/InterceptorListGeneratorTest.php diff --git a/src/Aop/Framework/AbstractJoinpoint.php b/src/Aop/Framework/AbstractJoinpoint.php index f306d2cd..44b51add 100644 --- a/src/Aop/Framework/AbstractJoinpoint.php +++ b/src/Aop/Framework/AbstractJoinpoint.php @@ -13,6 +13,7 @@ namespace Go\Aop\Framework; use Go\Aop\Advice; +use Go\Aop\AspectException; use Go\Aop\IntroductionInfo; use Go\Aop\Intercept\Interceptor; use Go\Aop\Intercept\Joinpoint; @@ -94,9 +95,12 @@ public static function flatAndSortAdvices(array $advices): array continue; } - $flattenAdvices[$type][$name][] = $advice instanceof Advice - ? GeneratedInterceptor::fromAdvice((string) $advisorId, $advice) - : GeneratedInterceptor::fromAdvisorId((string) $advisorId); + 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/GeneratedInterceptor.php b/src/Aop/Framework/GeneratedInterceptor.php index f1fccce9..b80f69e5 100644 --- a/src/Aop/Framework/GeneratedInterceptor.php +++ b/src/Aop/Framework/GeneratedInterceptor.php @@ -26,10 +26,11 @@ { private function __construct( public string $factoryMethod, - public string $aspectClass, - public string $adviceMethod, + public ?string $aspectClass, + public ?string $adviceMethod, public int $order, - public string $advisorId + public string $advisorId, + public bool $usesContainerAdvice = false ) {} public static function fromAdvice(string $advisorId, Advice $advice): self @@ -38,28 +39,17 @@ public static function fromAdvice(string $advisorId, Advice $advice): self 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(); - if ($scopeClass === null || !is_subclass_of($scopeClass->name, Aspect::class)) { - throw new AspectException("Advisor {$advisorId} uses an unsupported non-aspect callable; generated first-class advice callables require aspect methods"); - } + $reflectionAdvice = new ReflectionFunction($advice->getRawAdvice()); + $scopeClass = $reflectionAdvice->getClosureScopeClass(); + $usesContainerAdvice = $scopeClass === null || !is_subclass_of($scopeClass->name, Aspect::class); return new self( $advice->getType()->value, - $scopeClass->name, - $reflectionAdvice->name, + $usesContainerAdvice ? null : $scopeClass->name, + $usesContainerAdvice ? null : $reflectionAdvice->name, $advice->getAdviceOrder(), - $advisorId + $advisorId, + $usesContainerAdvice ); } - - public static function fromAdvisorId(string $advisorId): self - { - $reference = str_starts_with($advisorId, 'advisor.') ? substr($advisorId, 8) : $advisorId; - [$aspectClass, $adviceMethod] = str_contains($reference, '->') - ? explode('->', $reference, 2) - : [$reference, 'advice']; - - return new self('before', $aspectClass, $adviceMethod, 0, $advisorId); - } } diff --git a/src/Aop/Framework/The.php b/src/Aop/Framework/The.php index deac9107..0d162400 100644 --- a/src/Aop/Framework/The.php +++ b/src/Aop/Framework/The.php @@ -12,7 +12,11 @@ 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; /** @@ -27,6 +31,28 @@ final class The */ public static function aspect(string $aspectClass): Aspect { - return AspectKernel::getInstance()->getContainer()->getService($aspectClass); + 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/Proxy/FunctionProxyGenerator.php b/src/Proxy/FunctionProxyGenerator.php index 039d1118..f5be2dd7 100644 --- a/src/Proxy/FunctionProxyGenerator.php +++ b/src/Proxy/FunctionProxyGenerator.php @@ -137,7 +137,9 @@ private function collectAspectClasses(array $adviceNames): array foreach ($adviceNames as $typedAdvices) { foreach ($typedAdvices as $concreteAdvices) { foreach ($concreteAdvices as $advice) { - $interceptors[] = $advice; + if ($advice instanceof GeneratedInterceptor) { + $interceptors[] = $advice; + } } } } diff --git a/src/Proxy/Generator/InterceptorListGenerator.php b/src/Proxy/Generator/InterceptorListGenerator.php index bca0de45..716c48e0 100644 --- a/src/Proxy/Generator/InterceptorListGenerator.php +++ b/src/Proxy/Generator/InterceptorListGenerator.php @@ -12,15 +12,19 @@ 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; /** @@ -31,19 +35,39 @@ final class InterceptorListGenerator { /** - * @param list $interceptors + * @var list */ - public function __construct(private readonly array $interceptors) {} + private readonly array $interceptors; /** - * @param list $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) { - $interceptor = self::normalize($interceptor); + if ($interceptor->aspectClass === null) { + continue; + } $classes[$interceptor->aspectClass] = $interceptor->aspectClass; } @@ -56,62 +80,55 @@ public function generate(string $indent): string return '[]'; } - $lines = ['[']; - foreach ($this->normalizedInterceptors() as $interceptor) { - $lines[] = $indent . ' Interceptor::' . $interceptor->factoryMethod . '('; - $lines[] = $indent . ' The::aspect(' . self::shortClassName($interceptor->aspectClass) . '::class)->' . $interceptor->adviceMethod . '(...),'; - if ($interceptor->order !== 0) { - $lines[] = $indent . ' order: ' . $interceptor->order . ','; - } - $lines[] = $indent . ' ),'; - } - $lines[] = $indent . ']'; + $printed = (new GeneratedCodePrinter(['shortArraySyntax' => true]))->prettyPrintExpr($this->getNode()); - return implode("\n", $lines); + 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->normalizedInterceptors() + $this->interceptors ), ['kind' => Array_::KIND_SHORT]); } - /** - * @return list - */ - private function normalizedInterceptors(): array + private static function createCallNode(GeneratedInterceptor $interceptor): StaticCall { - return array_map(self::normalize(...), $this->interceptors); - } + $args = [ + new Arg(self::createAdviceAccessorNode($interceptor)), + ]; - private static function normalize(GeneratedInterceptor|string $interceptor): GeneratedInterceptor - { - if (is_string($interceptor)) { - return GeneratedInterceptor::fromAdvisorId($interceptor); + if ($interceptor->order !== 0) { + $args[] = new Arg(new Int_($interceptor->order), name: new Identifier('order')); } - return $interceptor; + return new StaticCall(new Name('Interceptor'), $interceptor->factoryMethod, $args); } - private static function createCallNode(GeneratedInterceptor $interceptor): StaticCall + private static function createAdviceAccessorNode(GeneratedInterceptor $interceptor): Expr { - $args = [ - new Arg(new MethodCall( - new StaticCall(new Name('The'), 'aspect', [ - new Arg(new ClassConstFetch(new Name(self::shortClassName($interceptor->aspectClass)), 'class')), - ]), - $interceptor->adviceMethod, - [new VariadicPlaceholder()] - )), - ]; + if ($interceptor->usesContainerAdvice) { + return new StaticCall( + new Name('The'), + 'advice', + [ + new Arg(new String_($interceptor->advisorId)), + ] + ); + } - if ($interceptor->order !== 0) { - $args[] = new Arg(new \PhpParser\Node\Scalar\Int_($interceptor->order), name: new Identifier('order')); + if ($interceptor->aspectClass === null || $interceptor->adviceMethod === null) { + throw new \LogicException('Aspect-backed interceptor descriptor is incomplete'); } - return new StaticCall(new Name('Interceptor'), $interceptor->factoryMethod, $args); + 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 diff --git a/tests/Aop/Framework/AbstractJoinpointTest.php b/tests/Aop/Framework/AbstractJoinpointTest.php index 691d43f1..e20e93f3 100644 --- a/tests/Aop/Framework/AbstractJoinpointTest.php +++ b/tests/Aop/Framework/AbstractJoinpointTest.php @@ -6,6 +6,7 @@ use Go\Aop\Advice; use Go\Aop\AdviceTypeEnum; +use Go\Aop\AspectException; use Go\Aop\OrderedAdvice; use PHPUnit\Framework\TestCase; @@ -85,6 +86,63 @@ 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 { diff --git a/tests/Aop/Framework/GeneratedInterceptorTest.php b/tests/Aop/Framework/GeneratedInterceptorTest.php index 757ddebd..5b9cf06a 100644 --- a/tests/Aop/Framework/GeneratedInterceptorTest.php +++ b/tests/Aop/Framework/GeneratedInterceptorTest.php @@ -4,20 +4,89 @@ namespace Go\Aop\Framework; +use Go\Aop\Advice; +use Go\Aop\AdviceTypeEnum; use Go\Aop\AspectException; use Go\Aop\Intercept\Joinpoint; +use Go\Stubs\AttributeAspectLoaderExtensionTestPublicAspect; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; +use ReflectionMethod; final class GeneratedInterceptorTest extends TestCase { - public function testRejectsNonAspectCallableAdvice(): void + public function testCreatesContainerAdviceDescriptorForNonAspectCallableAdvice(): void { - $this->expectException(AspectException::class); - $this->expectExceptionMessage('unsupported non-aspect callable'); + $interceptor = GeneratedInterceptor::fromAdvice( + 'manual-advisor', + new BeforeInterceptor(static function (Joinpoint $joinpoint): void {}, 10) + ); - GeneratedInterceptor::fromAdvice( + $this->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 BeforeInterceptor(static function (Joinpoint $joinpoint): void {}) + 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/TheTest.php b/tests/Aop/Framework/TheTest.php index 61f6bd37..47c515d9 100644 --- a/tests/Aop/Framework/TheTest.php +++ b/tests/Aop/Framework/TheTest.php @@ -4,12 +4,19 @@ namespace Go\Aop\Framework; +use Closure; +use Go\Aop\Advice; +use Go\Aop\AdviceTypeEnum; +use Go\Aop\Advisor; use Go\Aop\Aspect; +use Go\Aop\AspectException; use Go\Core\AspectContainer; use Go\Core\AspectKernel; use Go\Core\Container; +use OutOfBoundsException; use PHPUnit\Framework\TestCase; use ReflectionProperty; +use stdClass; final class TheTest extends TestCase { @@ -26,6 +33,90 @@ public function testReturnsRegisteredAspectInstance(): void $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 */ 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 a3c6c40e..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 @@ -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 861d9efc..8f4bdb19 100644 --- a/tests/Instrument/Transformer/_files/class-proxy.php +++ b/tests/Instrument/Transformer/_files/class-proxy.php @@ -4,7 +4,6 @@ use Go\Aop\Framework\InterceptorInjector; use Go\Aop\Framework\Interceptor; use Go\Aop\Framework\The; -use Test\ns1\TestClass; use Go\Aop\Intercept\DynamicMethodInvocation; use Go\Aop\Intercept\StaticMethodInvocation; class TestClass implements \Go\Aop\Proxy @@ -25,7 +24,7 @@ public function publicMethod() self::class, 'publicMethod', [ - Interceptor::before(The::aspect(TestClass::class)->publicMethod(...)), + Interceptor::before(The::advice('advisor.Test\ns1\TestClass->publicMethod')), ], $this->__aop__publicMethod(...), ); @@ -38,7 +37,7 @@ protected function protectedMethod() self::class, 'protectedMethod', [ - Interceptor::before(The::aspect(TestClass::class)->protectedMethod(...)), + Interceptor::before(The::advice('advisor.Test\ns1\TestClass->protectedMethod')), ], $this->__aop__protectedMethod(...), ); @@ -51,7 +50,7 @@ public static function publicStaticMethod() self::class, 'publicStaticMethod', [ - Interceptor::before(The::aspect(TestClass::class)->publicStaticMethod(...)), + Interceptor::before(The::advice('advisor.Test\ns1\TestClass->publicStaticMethod')), ], self::__aop__publicStaticMethod(...), ); @@ -64,7 +63,7 @@ protected static function protectedStaticMethod() self::class, 'protectedStaticMethod', [ - Interceptor::before(The::aspect(TestClass::class)->protectedStaticMethod(...)), + Interceptor::before(The::advice('advisor.Test\ns1\TestClass->protectedStaticMethod')), ], self::__aop__protectedStaticMethod(...), ); @@ -77,7 +76,7 @@ public function publicMethodDynamicArguments($a, &$b) self::class, 'publicMethodDynamicArguments', [ - Interceptor::before(The::aspect(TestClass::class)->publicMethodDynamicArguments(...)), + Interceptor::before(The::advice('advisor.Test\ns1\TestClass->publicMethodDynamicArguments')), ], $this->__aop__publicMethodDynamicArguments(...), ); @@ -90,7 +89,7 @@ public function publicMethodFixedArguments($a, $b, $c = null) self::class, 'publicMethodFixedArguments', [ - Interceptor::before(The::aspect(TestClass::class)->publicMethodFixedArguments(...)), + Interceptor::before(The::advice('advisor.Test\ns1\TestClass->publicMethodFixedArguments')), ], $this->__aop__publicMethodFixedArguments(...), ); @@ -103,7 +102,7 @@ public function methodWithSpecialTypeArguments(self $instance) self::class, 'methodWithSpecialTypeArguments', [ - Interceptor::before(The::aspect(TestClass::class)->methodWithSpecialTypeArguments(...)), + Interceptor::before(The::advice('advisor.Test\ns1\TestClass->methodWithSpecialTypeArguments')), ], $this->__aop__methodWithSpecialTypeArguments(...), ); diff --git a/tests/Instrument/Transformer/_files/final-readonly-class-proxy.php b/tests/Instrument/Transformer/_files/final-readonly-class-proxy.php index 365e2446..94467ba2 100644 --- a/tests/Instrument/Transformer/_files/final-readonly-class-proxy.php +++ b/tests/Instrument/Transformer/_files/final-readonly-class-proxy.php @@ -4,7 +4,6 @@ use Go\Aop\Framework\InterceptorInjector; use Go\Aop\Framework\Interceptor; use Go\Aop\Framework\The; -use Test\ns1\TestReadonlyClass; use Go\Aop\Intercept\DynamicMethodInvocation; use Go\Aop\Intercept\StaticMethodInvocation; final readonly class TestReadonlyClass implements \Go\Aop\Proxy @@ -21,7 +20,7 @@ public function publicMethod(): string self::class, 'publicMethod', [ - Interceptor::before(The::aspect(TestReadonlyClass::class)->publicMethod(...)), + Interceptor::before(The::advice('advisor.Test\ns1\TestReadonlyClass->publicMethod')), ], $this->__aop__publicMethod(...), ); @@ -34,7 +33,7 @@ public function anotherMethod(int $x): int self::class, 'anotherMethod', [ - Interceptor::before(The::aspect(TestReadonlyClass::class)->anotherMethod(...)), + Interceptor::before(The::advice('advisor.Test\ns1\TestReadonlyClass->anotherMethod')), ], $this->__aop__anotherMethod(...), ); @@ -47,7 +46,7 @@ public static function staticMethod(): string self::class, 'staticMethod', [ - Interceptor::before(The::aspect(TestReadonlyClass::class)->staticMethod(...)), + Interceptor::before(The::advice('advisor.Test\ns1\TestReadonlyClass->staticMethod')), ], self::__aop__staticMethod(...), ); diff --git a/tests/Instrument/Transformer/_files/php7-class-proxy.php b/tests/Instrument/Transformer/_files/php7-class-proxy.php index d2e590d1..2c8ca622 100644 --- a/tests/Instrument/Transformer/_files/php7-class-proxy.php +++ b/tests/Instrument/Transformer/_files/php7-class-proxy.php @@ -4,7 +4,6 @@ use Go\Aop\Framework\InterceptorInjector; use Go\Aop\Framework\Interceptor; use Go\Aop\Framework\The; -use Test\ns1\TestPhp7Class; use Go\Aop\Intercept\DynamicMethodInvocation; class TestPhp7Class implements \Go\Aop\Proxy { @@ -34,7 +33,7 @@ public function stringSth(string $arg) self::class, 'stringSth', [ - Interceptor::before(The::aspect(TestPhp7Class::class)->stringSth(...)), + Interceptor::before(The::advice('advisor.Test\ns1\TestPhp7Class->stringSth')), ], $this->__aop__stringSth(...), ); @@ -47,7 +46,7 @@ public function floatSth(float $arg) self::class, 'floatSth', [ - Interceptor::before(The::aspect(TestPhp7Class::class)->floatSth(...)), + Interceptor::before(The::advice('advisor.Test\ns1\TestPhp7Class->floatSth')), ], $this->__aop__floatSth(...), ); @@ -60,7 +59,7 @@ public function boolSth(bool $arg) self::class, 'boolSth', [ - Interceptor::before(The::aspect(TestPhp7Class::class)->boolSth(...)), + Interceptor::before(The::advice('advisor.Test\ns1\TestPhp7Class->boolSth')), ], $this->__aop__boolSth(...), ); @@ -73,7 +72,7 @@ public function intSth(int $arg) self::class, 'intSth', [ - Interceptor::before(The::aspect(TestPhp7Class::class)->intSth(...)), + Interceptor::before(The::advice('advisor.Test\ns1\TestPhp7Class->intSth')), ], $this->__aop__intSth(...), ); @@ -86,7 +85,7 @@ public function callableSth(callable $arg) self::class, 'callableSth', [ - Interceptor::before(The::aspect(TestPhp7Class::class)->callableSth(...)), + Interceptor::before(The::advice('advisor.Test\ns1\TestPhp7Class->callableSth')), ], $this->__aop__callableSth(...), ); @@ -99,7 +98,7 @@ public function arraySth(array $arg) self::class, 'arraySth', [ - Interceptor::before(The::aspect(TestPhp7Class::class)->arraySth(...)), + Interceptor::before(The::advice('advisor.Test\ns1\TestPhp7Class->arraySth')), ], $this->__aop__arraySth(...), ); @@ -112,7 +111,7 @@ public function variadicStringSthByRef(string &...$args) self::class, 'variadicStringSthByRef', [ - Interceptor::before(The::aspect(TestPhp7Class::class)->variadicStringSthByRef(...)), + Interceptor::before(The::advice('advisor.Test\ns1\TestPhp7Class->variadicStringSthByRef')), ], $this->__aop__variadicStringSthByRef(...), ); @@ -125,7 +124,7 @@ public function exceptionArg(\Exception $exception, \Test\ns1\Exception $localEx self::class, 'exceptionArg', [ - Interceptor::before(The::aspect(TestPhp7Class::class)->exceptionArg(...)), + Interceptor::before(The::advice('advisor.Test\ns1\TestPhp7Class->exceptionArg')), ], $this->__aop__exceptionArg(...), ); @@ -138,7 +137,7 @@ public function stringRth(string $arg): string self::class, 'stringRth', [ - Interceptor::before(The::aspect(TestPhp7Class::class)->stringRth(...)), + Interceptor::before(The::advice('advisor.Test\ns1\TestPhp7Class->stringRth')), ], $this->__aop__stringRth(...), ); @@ -151,7 +150,7 @@ public function floatRth(float $arg): float self::class, 'floatRth', [ - Interceptor::before(The::aspect(TestPhp7Class::class)->floatRth(...)), + Interceptor::before(The::advice('advisor.Test\ns1\TestPhp7Class->floatRth')), ], $this->__aop__floatRth(...), ); @@ -164,7 +163,7 @@ public function boolRth(bool $arg): bool self::class, 'boolRth', [ - Interceptor::before(The::aspect(TestPhp7Class::class)->boolRth(...)), + Interceptor::before(The::advice('advisor.Test\ns1\TestPhp7Class->boolRth')), ], $this->__aop__boolRth(...), ); @@ -177,7 +176,7 @@ public function intRth(int $arg): int self::class, 'intRth', [ - Interceptor::before(The::aspect(TestPhp7Class::class)->intRth(...)), + Interceptor::before(The::advice('advisor.Test\ns1\TestPhp7Class->intRth')), ], $this->__aop__intRth(...), ); @@ -190,7 +189,7 @@ public function callableRth(callable $arg): callable self::class, 'callableRth', [ - Interceptor::before(The::aspect(TestPhp7Class::class)->callableRth(...)), + Interceptor::before(The::advice('advisor.Test\ns1\TestPhp7Class->callableRth')), ], $this->__aop__callableRth(...), ); @@ -203,7 +202,7 @@ public function arrayRth(array $arg): array self::class, 'arrayRth', [ - Interceptor::before(The::aspect(TestPhp7Class::class)->arrayRth(...)), + Interceptor::before(The::advice('advisor.Test\ns1\TestPhp7Class->arrayRth')), ], $this->__aop__arrayRth(...), ); @@ -216,7 +215,7 @@ public function exceptionRth(\Exception $exception): \Exception self::class, 'exceptionRth', [ - Interceptor::before(The::aspect(TestPhp7Class::class)->exceptionRth(...)), + Interceptor::before(The::advice('advisor.Test\ns1\TestPhp7Class->exceptionRth')), ], $this->__aop__exceptionRth(...), ); @@ -229,7 +228,7 @@ public function noRth(\Test\ns1\LocalException $exception) self::class, 'noRth', [ - Interceptor::before(The::aspect(TestPhp7Class::class)->noRth(...)), + Interceptor::before(The::advice('advisor.Test\ns1\TestPhp7Class->noRth')), ], $this->__aop__noRth(...), ); @@ -242,7 +241,7 @@ public function returnSelf(): self self::class, 'returnSelf', [ - Interceptor::before(The::aspect(TestPhp7Class::class)->returnSelf(...)), + Interceptor::before(The::advice('advisor.Test\ns1\TestPhp7Class->returnSelf')), ], $this->__aop__returnSelf(...), ); diff --git a/tests/Instrument/Transformer/_files/php80-82-syntax-proxy.php b/tests/Instrument/Transformer/_files/php80-82-syntax-proxy.php index e8b90b38..c2f04918 100644 --- a/tests/Instrument/Transformer/_files/php80-82-syntax-proxy.php +++ b/tests/Instrument/Transformer/_files/php80-82-syntax-proxy.php @@ -1,10 +1,10 @@ __construct(...)), + Interceptor::before(The::advice('advisor.Test\ns1\TestPhp80To82SyntaxClass->__construct')), ], $this->__aop____construct(...), ); @@ -38,10 +38,10 @@ public function describe(?\ArrayObject $extra = null): string self::class, 'describe', [ - Interceptor::before(The::aspect(TestPhp80To82SyntaxClass::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 fba47801..fc3b4128 100644 --- a/tests/Instrument/Transformer/_files/php80-promoted-property-proxy.php +++ b/tests/Instrument/Transformer/_files/php80-promoted-property-proxy.php @@ -1,10 +1,10 @@ name(...)), + Interceptor::before(The::advice('advisor.Go\Tests\TestProject\Application\PromotedPropertyClass->name')), ], ); return $__joinPoint->__invoke($this, FieldAccessType::READ, $this->name); @@ -36,7 +36,7 @@ class PromotedPropertyClass implements \Go\Aop\Proxy self::class, 'name', [ - Interceptor::before(The::aspect(PromotedPropertyClass::class)->name(...)), + Interceptor::before(The::advice('advisor.Go\Tests\TestProject\Application\PromotedPropertyClass->name')), ], ); $this->name = $__joinPoint->__invoke($this, FieldAccessType::WRITE, $value, $this->name); @@ -49,7 +49,7 @@ class PromotedPropertyClass implements \Go\Aop\Proxy self::class, 'counter', [ - Interceptor::before(The::aspect(PromotedPropertyClass::class)->counter(...)), + Interceptor::before(The::advice('advisor.Go\Tests\TestProject\Application\PromotedPropertyClass->counter')), ], ); return $__joinPoint->__invoke($this, FieldAccessType::READ, $this->counter); @@ -60,7 +60,7 @@ class PromotedPropertyClass implements \Go\Aop\Proxy self::class, 'counter', [ - Interceptor::before(The::aspect(PromotedPropertyClass::class)->counter(...)), + Interceptor::before(The::advice('advisor.Go\Tests\TestProject\Application\PromotedPropertyClass->counter')), ], ); $this->counter = $__joinPoint->__invoke($this, FieldAccessType::WRITE, $value, $this->counter); @@ -73,7 +73,7 @@ public function __construct(string $name = 'initial', int $counter = 1, ?\ArrayO self::class, '__construct', [ - Interceptor::before(The::aspect(PromotedPropertyClass::class)->__construct(...)), + Interceptor::before(The::advice('advisor.Go\Tests\TestProject\Application\PromotedPropertyClass->__construct')), ], $this->__aop____construct(...), ); @@ -86,10 +86,10 @@ public function getName(): string self::class, 'getName', [ - Interceptor::before(The::aspect(PromotedPropertyClass::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 d9388d21..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 @@ -1,10 +1,10 @@ tag(...)), + Interceptor::before(The::advice('advisor.Go\Tests\TestProject\Application\SingleLinePromotedClass->tag')), ], ); return $__joinPoint->__invoke($this, FieldAccessType::READ, $this->tag); @@ -35,7 +35,7 @@ class SingleLinePromotedClass implements \Go\Aop\Proxy self::class, 'tag', [ - Interceptor::before(The::aspect(SingleLinePromotedClass::class)->tag(...)), + Interceptor::before(The::advice('advisor.Go\Tests\TestProject\Application\SingleLinePromotedClass->tag')), ], ); $this->tag = $__joinPoint->__invoke($this, FieldAccessType::WRITE, $value, $this->tag); @@ -48,10 +48,10 @@ public function __construct(string $tag = 'default') self::class, '__construct', [ - Interceptor::before(The::aspect(SingleLinePromotedClass::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 c1abd696..60f4be19 100644 --- a/tests/Instrument/Transformer/_files/php81-attr-args-proxy.php +++ b/tests/Instrument/Transformer/_files/php81-attr-args-proxy.php @@ -1,10 +1,10 @@ tagged(...)), + Interceptor::before(The::advice('advisor.Test\ns1\TestAttributeArgsClass->tagged')), ], $this->__aop__tagged(...), ); @@ -37,10 +37,10 @@ public function collected(): array self::class, 'collected', [ - Interceptor::before(The::aspect(TestAttributeArgsClass::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 a9a30364..11988376 100644 --- a/tests/Instrument/Transformer/_files/php81-enum-const-expr-proxy.php +++ b/tests/Instrument/Transformer/_files/php81-enum-const-expr-proxy.php @@ -1,10 +1,10 @@ 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 b2deb9ad..0396b4ed 100644 --- a/tests/Instrument/Transformer/_files/php81-enum-proxy.php +++ b/tests/Instrument/Transformer/_files/php81-enum-proxy.php @@ -4,7 +4,6 @@ use Go\Aop\Framework\InterceptorInjector; use Go\Aop\Framework\Interceptor; use Go\Aop\Framework\The; -use Test\ns1\TestStatus; use Go\Aop\Intercept\DynamicMethodInvocation; enum TestStatus : string implements \Go\Aop\Proxy { @@ -20,7 +19,7 @@ public function label(): string self::class, 'label', [ - Interceptor::before(The::aspect(TestStatus::class)->label(...)), + Interceptor::before(The::advice('advisor.Test\ns1\TestStatus->label')), ], $this->__aop__label(...), ); diff --git a/tests/Instrument/Transformer/_files/php83-override-proxy.php b/tests/Instrument/Transformer/_files/php83-override-proxy.php index 8f21ae57..7c1e8678 100644 --- a/tests/Instrument/Transformer/_files/php83-override-proxy.php +++ b/tests/Instrument/Transformer/_files/php83-override-proxy.php @@ -4,7 +4,6 @@ use Go\Aop\Framework\InterceptorInjector; use Go\Aop\Framework\Interceptor; use Go\Aop\Framework\The; -use Test\ns1\TestClassWithOverride; use Go\Aop\Intercept\DynamicMethodInvocation; /** * PHP 8.3 — class with #[\Override] on an intercepted method. @@ -25,7 +24,7 @@ public function overriddenMethod(): string self::class, 'overriddenMethod', [ - Interceptor::before(The::aspect(TestClassWithOverride::class)->overriddenMethod(...)), + Interceptor::before(The::advice('advisor.Test\ns1\TestClassWithOverride->overriddenMethod')), ], $this->__aop__overriddenMethod(...), ); @@ -38,7 +37,7 @@ public function normalMethod(): int self::class, 'normalMethod', [ - Interceptor::before(The::aspect(TestClassWithOverride::class)->normalMethod(...)), + Interceptor::before(The::advice('advisor.Test\ns1\TestClassWithOverride->normalMethod')), ], $this->__aop__normalMethod(...), ); diff --git a/tests/PhpUnit/ProxyClassReflectionHelper.php b/tests/PhpUnit/ProxyClassReflectionHelper.php index 2de3b817..5cf26077 100644 --- a/tests/PhpUnit/ProxyClassReflectionHelper.php +++ b/tests/PhpUnit/ProxyClassReflectionHelper.php @@ -250,9 +250,23 @@ private static function extractAdviceNamesFromGeneratedFactories(mixed $advicesN continue; } $aspectCall = $adviceCall->var; - if (!$aspectCall instanceof StaticCall || !$aspectCall->class instanceof Name || !str_ends_with($aspectCall->class->toString(), 'The') || !isset($aspectCall->args[0])) { + 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; diff --git a/tests/Proxy/ClassProxyGeneratorTest.php b/tests/Proxy/ClassProxyGeneratorTest.php index 7f1568d4..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()] ] ]; @@ -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()], ] ]; @@ -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()], ], ]; @@ -214,8 +216,8 @@ public function testGenerateWithParentPropertyInterceptionIncludesPublicAndProte $reflectionClass = new ReflectionClass(PropertyInheritanceChild::class); $classAdvices = [ 'prop' => [ - 'parentPublic' => ['test'], - 'parentProtected' => ['test'], + 'parentPublic' => [self::testAdvice()], + 'parentProtected' => [self::testAdvice()], ], ]; @@ -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 ], ]; @@ -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 ], ]; @@ -410,7 +412,7 @@ public function testGenerateProxyForInheritedMethodDoesNotCreateTraitAlias(): vo $reflectionClass = new ReflectionClass(FirstStatic::class); $classAdvices = [ 'method' => [ - 'publicMethod' => ['test'], + 'publicMethod' => [self::testAdvice()], ], ]; @@ -446,7 +448,7 @@ public function testGenerateProxyForInheritedStaticMethodUsesParentCallable(): v $reflectionClass = new ReflectionClass(FirstStatic::class); $classAdvices = [ 'static' => [ - 'staticSelfPublic' => ['test'], + 'staticSelfPublic' => [self::testAdvice()], ], ]; @@ -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..7d02999b --- /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 dcca5db7..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')], ], ]; @@ -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')], ], ]; @@ -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 {})); + } } From 9f0e81cd46d7544646ee3f46e4220497da429f5e Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 29 Aug 2026 18:54:57 +0000 Subject: [PATCH 5/7] docs: document first-class callable advices Add 4.0.0 changelog entries for the first-class callable advices feature and its two breaking changes (public advice methods, advice marker interfaces replaced by AdviceTypeEnum). Rework the README aspect guide to present first-class callable advices as the main approach: advice methods are public, woven directly as closures on the aspect instance via The::aspect(), with The::advice() covering container-registered closure advices. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_019M9gpum1Rgasc2KtBZ3UhD --- CHANGELOG.md | 3 +++ README.md | 33 ++++++++++++++++++++++++++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) 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: From 0c315cbea9e3f161ef2a4140e4ba7e7157c2cbf3 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 29 Aug 2026 18:59:08 +0000 Subject: [PATCH 6/7] docs: refresh AGENTS.md for first-class callable advice wiring Update the agent guides to describe the new proxy dispatch: interceptor lists built from first-class advice callables (The::aspect()/The::advice()), the Interceptor factory facade, GeneratedInterceptor descriptors, AdviceTypeEnum and the public advice method requirement. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_019M9gpum1Rgasc2KtBZ3UhD --- AGENTS.md | 2 +- src/Aop/AGENTS.md | 7 +++++++ src/Core/AGENTS.md | 2 +- src/Instrument/AGENTS.md | 11 ++++++++++- 4 files changed, 19 insertions(+), 3 deletions(-) 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/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/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/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) From e6fc8f20f04b950d59f4ade891549598f9dee1f5 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 17:31:09 +0000 Subject: [PATCH 7/7] refactor(proxy): use ::class imports and a named default indent in generators Address review feedback on #620: replace string-literal FQCNs in addUse() calls with ::class references (imports added to the four proxy generators), and give InterceptorListGenerator::generate() a documented default indent constant so call sites no longer pass a bare whitespace string. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_019M9gpum1Rgasc2KtBZ3UhD --- src/Proxy/ClassProxyGenerator.php | 33 ++++++++++++------- src/Proxy/EnumProxyGenerator.php | 17 ++++++---- src/Proxy/FunctionProxyGenerator.php | 14 +++++--- .../Generator/InterceptorListGenerator.php | 11 ++++++- src/Proxy/TraitProxyGenerator.php | 23 ++++++++----- .../InterceptorListGeneratorTest.php | 18 +++++----- 6 files changed, 75 insertions(+), 41 deletions(-) diff --git a/src/Proxy/ClassProxyGenerator.php b/src/Proxy/ClassProxyGenerator.php index cf80f033..138995af 100644 --- a/src/Proxy/ClassProxyGenerator.php +++ b/src/Proxy/ClassProxyGenerator.php @@ -14,7 +14,16 @@ 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; @@ -182,9 +191,9 @@ 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. - $classGenerator->addUse('Go\Aop\Framework\InterceptorInjector'); - $classGenerator->addUse('Go\Aop\Framework\Interceptor'); - $classGenerator->addUse('Go\Aop\Framework\The'); + $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); @@ -192,20 +201,20 @@ public function __construct( } 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; @@ -318,7 +327,7 @@ protected function getJoinpointInvocationBody(ReflectionMethod $method, ?Reflect $adviceNames = $this->adviceNames[$prefix][$method->name] ?? ($isStatic ? ($this->adviceNames[AspectContainer::METHOD_PREFIX][$method->name] ?? []) : []); - $advicesCode = (new InterceptorListGenerator($adviceNames))->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. @@ -375,7 +384,7 @@ protected function getJoinpointInvocationBody(ReflectionMethod $method, ?Reflect */ private function createStaticInitializationMethod(array $advisorNames): MethodGenerator { - $advicesCode = (new InterceptorListGenerator($advisorNames))->generate(' '); + $advicesCode = (new InterceptorListGenerator($advisorNames))->generate(); $method = new MethodGenerator('__aop__staticInitialization'); $method->setStatic(true); @@ -397,7 +406,7 @@ private function createStaticInitializationMethod(array $advisorNames): MethodGe */ private function createInitializationMethod(array $advisorNames): MethodGenerator { - $advicesCode = (new InterceptorListGenerator($advisorNames))->generate(' '); + $advicesCode = (new InterceptorListGenerator($advisorNames))->generate(); $method = new MethodGenerator('__aop__initialization'); $method->setStatic(true); diff --git a/src/Proxy/EnumProxyGenerator.php b/src/Proxy/EnumProxyGenerator.php index a6f7f702..771c3b90 100644 --- a/src/Proxy/EnumProxyGenerator.php +++ b/src/Proxy/EnumProxyGenerator.php @@ -14,6 +14,11 @@ 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; @@ -161,9 +166,9 @@ 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('Go\Aop\Framework\Interceptor'); - $enumGenerator->addUse('Go\Aop\Framework\The'); + $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); @@ -171,9 +176,9 @@ public function __construct( } 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); } } @@ -230,7 +235,7 @@ protected function getJoinpointInvocationBody(ReflectionMethod $method, ?Reflect $adviceNames = $this->adviceNames[$prefix][$method->name] ?? ($isStatic ? ($this->adviceNames[AspectContainer::METHOD_PREFIX][$method->name] ?? []) : []); - $advicesCode = (new InterceptorListGenerator($adviceNames))->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. diff --git a/src/Proxy/FunctionProxyGenerator.php b/src/Proxy/FunctionProxyGenerator.php index f5be2dd7..8625539f 100644 --- a/src/Proxy/FunctionProxyGenerator.php +++ b/src/Proxy/FunctionProxyGenerator.php @@ -13,6 +13,10 @@ 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; @@ -56,10 +60,10 @@ 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\Framework\Interceptor'); - $this->fileGenerator->addUse('Go\Aop\Framework\The'); - $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); @@ -109,7 +113,7 @@ protected function getJoinpointInvocationBody(ReflectionFunction $function): str } $functionAdvices = $this->adviceNames[AspectContainer::FUNCTION_PREFIX][$function->name]; - $advicesCode = (new InterceptorListGenerator(array_values($functionAdvices)))->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 diff --git a/src/Proxy/Generator/InterceptorListGenerator.php b/src/Proxy/Generator/InterceptorListGenerator.php index 716c48e0..a90a3c2b 100644 --- a/src/Proxy/Generator/InterceptorListGenerator.php +++ b/src/Proxy/Generator/InterceptorListGenerator.php @@ -34,6 +34,15 @@ */ 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 */ @@ -74,7 +83,7 @@ public static function aspectClasses(array $interceptors): array return array_values($classes); } - public function generate(string $indent): string + public function generate(string $indent = self::JOINPOINT_ARGUMENT_INDENT): string { if ($this->interceptors === []) { return '[]'; diff --git a/src/Proxy/TraitProxyGenerator.php b/src/Proxy/TraitProxyGenerator.php index 645dbb7c..003c3e0a 100644 --- a/src/Proxy/TraitProxyGenerator.php +++ b/src/Proxy/TraitProxyGenerator.php @@ -14,6 +14,13 @@ 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; @@ -86,9 +93,9 @@ 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('Go\Aop\Framework\Interceptor'); - $traitGenerator->addUse('Go\Aop\Framework\The'); + $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); @@ -96,15 +103,15 @@ public function __construct( } 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 @@ -139,7 +146,7 @@ protected function getJoinpointInvocationBody(ReflectionMethod $method, ?Reflect $adviceNames = $this->adviceNames[$prefix][$method->name] ?? ($isStatic ? ($this->adviceNames[AspectContainer::METHOD_PREFIX][$method->name] ?? []) : []); - $advicesCode = (new InterceptorListGenerator($adviceNames))->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. diff --git a/tests/Proxy/Generator/InterceptorListGeneratorTest.php b/tests/Proxy/Generator/InterceptorListGeneratorTest.php index 7d02999b..60b599fc 100644 --- a/tests/Proxy/Generator/InterceptorListGeneratorTest.php +++ b/tests/Proxy/Generator/InterceptorListGeneratorTest.php @@ -21,14 +21,14 @@ public function testGeneratesContainerAdviceForClosureBackedAdvice(): void new AroundInterceptor(static fn(): mixed => null, 20) ); - $code = (new InterceptorListGenerator([$descriptor]))->generate(' '); + $code = (new InterceptorListGenerator([$descriptor]))->generate(); $this->assertSame([], InterceptorListGenerator::aspectClasses([$descriptor])); $this->assertSame( <<<'PHP' [ - Interceptor::around(The::advice('manual.around'), order: 20), - ] + Interceptor::around(The::advice('manual.around'), order: 20), + ] PHP, $code ); @@ -44,16 +44,16 @@ public function testGeneratesMatchingFactoryCallForEveryAdviceType(): void GeneratedInterceptor::fromAdvice('manual.afterThrowing', new AfterThrowingInterceptor($noop)), ]; - $code = (new InterceptorListGenerator($descriptors))->generate(' '); + $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')), - ] + 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 );