When generating the code for proxies, Invocation implementations (MethodInvocation, FunctionInvocation) should call somehow original methods or functions. Current version of framework uses extra reflection/closure binding technologies in constructors to be able to call original method or function.
Example of intercepted method - it has self::class, 'getReport' arguments, which are inlined into the method call
public function getReport(string $from): string
{
/** @var DynamicMethodInvocation<self, string> $__joinPoint */
static $__joinPoint = InterceptorInjector::forMethod(self::class, 'getReport', ['Demo\Aspect\CachingAspect->aroundCacheable']);
return $__joinPoint->__invoke($this, [$from]);
}
Implementation of DynamicMethodInvocation (DynamicTraitAliasMethodInvocation class) looks for original method to call:
public function __construct(array $advices, string $className, string $methodName)
{
parent::__construct($advices, $className, $methodName);
$aliasName = self::TRAIT_ALIAS_PREFIX . $methodName;
if (method_exists($className, $aliasName)) {
$methodToCall = new ReflectionMethod($className, $aliasName);
} elseif ($this->reflectionMethod->hasPrototype()) {
$methodToCall = $this->reflectionMethod->getPrototype();
} else {
throw new \LogicException("Cannot proceed with method invocation for {$methodName}: no trait alias and no method prototype found for {$className}");
}
$this->originalMethodToCall = $methodToCall;
}
and then call it later inside proceed() method (schematic, real code may be different):
public function proceed(): mixed
{
if (isset($this->advices[$this->current])) {
return $this->advices[$this->current++]->invoke($this);
}
return $this->originalMethodToCall->invoke($this->instance, ...$this->arguments);
}
Overall, this mechanism is tricky and contains hidden knowledge about trait adoptation with alias. There is better approach with First-Class-Callable syntax: https://www.php.net/manual/en/functions.first_class_callable_syntax.php
First-class callable original method references
Idea is to create first-class callable closure directly from our proxy class to the aliased trait method (or to parent method). See example below:
public function getReport(string $from): string
{
/** @var DynamicMethodInvocation<self, string> $__joinPoint */
static $__joinPoint = InterceptorInjector::forMethod(self::class, 'getReport', ['Demo\Aspect\CachingAspect->aroundCacheable'], $this->__aop__getReport(...));
return $__joinPoint->__invoke($this, [$from]);
}
if we intercept a method from the parent class (without trait aliasing), then reference will be to the same method, but from parent context:
public function getReport(string $from): string
{
/** @var DynamicMethodInvocation<self, string> $__joinPoint */
static $__joinPoint = InterceptorInjector::forMethod(self::class, 'getReport', ['Demo\Aspect\CachingAspect->aroundCacheable'], parent::getReport(...));
return $__joinPoint->__invoke($this, [$from]);
}
4th argument will be first-class-callable, created from the proper class context, allowing access to private/protected/public methods. Now we can drop all our logic inside method invocation classes and just use universal $closureToCall.
For dynamic method invocations we should use Closure->call() to bind new $this everytime (add a test that for several different objects, $this inside objects is really different via spl_object_id($this) returns, otherwise our static initialization of joinpoint will capture only first $this and will keep it indefintely). According to the performance tests, ReflectionMethod->invoke($this->instance, ...$args) works faster (see https://3v4l.org/DYj84), but we can accept this temporarily to make code quality better.
return $this->closureToCall->call($this->instance, ...$this->arguments);
For static methods, this will be the same, we can use self::__aop__originalMethod(...) syntax for aliased methods and parent::originalMethod() for interception of inherited static parent methods, ensure that LSB is working (if static method is intercepted and we have two several different child classes, that call this method statically from several scopes. To make LSB working during initialization we should create a wrapper (unfortunately) that will delegate a call with new scope to our created $closureToCall (original Closure, created from method prevents us from doing Closure->bindTo(null, $this->scope)):
$this->closureToCall = static fn(array $argumentsToCall): mixed => forward_static_call($closureToCall, ...$argumentsToCall);
and invocation will contain a scope binding call to ensure that several different scopes for the same method is supported (eg static singletone factory method) - write tests for it:
//... inside proceed() method
return $this->closureToCall->bindTo(null, $this->scope)->__invoke($this->arguments);
For functions - it is the same, we will use function name as first-class callable, eg file_get_contents(...), but we don't need to bind/call closure here, just call it effectively
When generating the code for proxies, Invocation implementations (MethodInvocation, FunctionInvocation) should call somehow original methods or functions. Current version of framework uses extra reflection/closure binding technologies in constructors to be able to call original method or function.
Example of intercepted method - it has
self::class, 'getReport'arguments, which are inlined into the method callImplementation of
DynamicMethodInvocation(DynamicTraitAliasMethodInvocationclass) looks for original method to call:and then call it later inside
proceed()method (schematic, real code may be different):Overall, this mechanism is tricky and contains hidden knowledge about trait adoptation with alias. There is better approach with First-Class-Callable syntax: https://www.php.net/manual/en/functions.first_class_callable_syntax.php
First-class callable original method references
Idea is to create first-class callable closure directly from our proxy class to the aliased trait method (or to parent method). See example below:
if we intercept a method from the parent class (without trait aliasing), then reference will be to the same method, but from parent context:
4th argument will be first-class-callable, created from the proper class context, allowing access to private/protected/public methods. Now we can drop all our logic inside method invocation classes and just use universal $closureToCall.
For dynamic method invocations we should use
Closure->call()to bind new$thiseverytime (add a test that for several different objects, $this inside objects is really different via spl_object_id($this) returns, otherwise our static initialization of joinpoint will capture only first $this and will keep it indefintely). According to the performance tests,ReflectionMethod->invoke($this->instance, ...$args)works faster (see https://3v4l.org/DYj84), but we can accept this temporarily to make code quality better.For static methods, this will be the same, we can use
self::__aop__originalMethod(...)syntax for aliased methods andparent::originalMethod()for interception of inherited static parent methods, ensure that LSB is working (if static method is intercepted and we have two several different child classes, that call this method statically from several scopes. To make LSB working during initialization we should create a wrapper (unfortunately) that will delegate a call with new scope to our created $closureToCall (original Closure, created from method prevents us from doing Closure->bindTo(null, $this->scope)):and invocation will contain a scope binding call to ensure that several different scopes for the same method is supported (eg static singletone factory method) - write tests for it:
For functions - it is the same, we will use function name as first-class callable, eg
file_get_contents(...), but we don't need to bind/call closure here, just call it effectively