Skip to content

Commit

Permalink
[BC Break] Refactor framework core for PHP8.2 features and phpstan le…
Browse files Browse the repository at this point in the history
…vel 9 checks
  • Loading branch information
lisachenko committed Apr 1, 2024
1 parent 72ce6a0 commit 4117b7d
Show file tree
Hide file tree
Showing 32 changed files with 320 additions and 497 deletions.
90 changes: 45 additions & 45 deletions src/Aop/Framework/AbstractInterceptor.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
namespace Go\Aop\Framework;

use Closure;
use Go\Aop\AspectException;
use Go\Aop\Intercept\Interceptor;
use Go\Aop\OrderedAdvice;
use Go\Core\AspectKernel;
use ReflectionFunction;
use ReflectionMethod;
Expand All @@ -31,7 +33,8 @@
*
* After and before interceptors are simple closures that will be invoked after and before main invocation.
*
* Framework models an interceptor as an PHP-closure, maintaining a chain of interceptors "around" the joinpoint:
* Framework models an interceptor as an PHP {@see Closure}, maintaining a chain of interceptors "around" the joinpoint:
* <pre>
* public function (Joinpoint $joinPoint)
* {
* echo 'Before action';
Expand All @@ -41,98 +44,95 @@
*
* return $result;
* }
* </pre>
*/
abstract class AbstractInterceptor implements Interceptor, OrderedAdvice
{
/**
* Local cache of advices for faster unserialization on big projects
*
* @var array<Closure>
*/
protected static array $localAdvicesCache = [];

/**
* Pointcut expression string which was used for this interceptor
*/
protected string $pointcutExpression;

/**
* Closure to call
*/
protected Closure $adviceMethod;

/**
* Advice order
* @var (array&array<string, Closure>) Local hashmap of advices for faster unserialization
*/
private int $adviceOrder;
private static array $localAdvicesCache = [];

/**
* Default constructor for interceptor
*/
public function __construct(Closure $adviceMethod, int $adviceOrder = 0, string $pointcutExpression = '')
{
$this->adviceMethod = $adviceMethod;
$this->adviceOrder = $adviceOrder;
$this->pointcutExpression = $pointcutExpression;
}
public function __construct(
protected readonly Closure $adviceMethod,
private readonly int $adviceOrder = 0,
protected readonly string $pointcutExpression = ''
) {}

/**
* Serialize advice method into array
* Serializes advice closure into array
*
* @return array{name: string, class?: string}
*/
public static function serializeAdvice(Closure $adviceMethod): array
{
$refAdvice = new ReflectionFunction($adviceMethod);
$reflectionAdvice = new ReflectionFunction($adviceMethod);
$scopeReflectionClass = $reflectionAdvice->getClosureScopeClass();

$packedAdvice = ['name' => $reflectionAdvice->name];
if (!isset($scopeReflectionClass)) {
throw new AspectException('Could not pack an interceptor without aspect name');
}
$packedAdvice['class'] = $scopeReflectionClass->name;

return [
'method' => $refAdvice->name,
'class' => $refAdvice->getClosureScopeClass()->name
];
return $packedAdvice;
}

/**
* Unserialize an advice
*
* @param array $adviceData Information about advice
* @param array{name: string, class?: string} $adviceData Information about advice
*/
public static function unserializeAdvice(array $adviceData): Closure
{
// General unpacking supports only aspect's advices
if (!isset($adviceData['class'])) {
throw new AspectException('Could not unpack an interceptor without aspect name');
}
$aspectName = $adviceData['class'];
$methodName = $adviceData['method'];
$methodName = $adviceData['name'];

if (!isset(static::$localAdvicesCache["$aspectName->$methodName"])) {
$aspect = AspectKernel::getInstance()->getContainer()->getAspect($aspectName);
$refMethod = new ReflectionMethod($aspectName, $methodName);
$advice = $refMethod->getClosure($aspect);
// With aspect name and method name, we can restore back a closure for it
if (!isset(self::$localAdvicesCache["$aspectName->$methodName"])) {
$aspect = AspectKernel::getInstance()->getContainer()->getAspect($aspectName);
$advice = (new ReflectionMethod($aspectName, $methodName))->getClosure($aspect);

static::$localAdvicesCache["$aspectName->$methodName"] = $advice;
assert(isset($advice), 'getClosure() can not be null on modern PHP versions');
self::$localAdvicesCache["$aspectName->$methodName"] = $advice;
}

return static::$localAdvicesCache["$aspectName->$methodName"];
return self::$localAdvicesCache["$aspectName->$methodName"];
}

/**
* Returns the advice order
*/
public function getAdviceOrder(): int
{
return $this->adviceOrder;
}

/**
* Getter for extracting the advice closure from Interceptor
*
* @internal
*/
public function getRawAdvice(): Closure
{
return $this->adviceMethod;
}

/**
* Serializes an interceptor into it's representation
* Serializes an interceptor into it's array shape representation
*
* @return non-empty-array<string, mixed>
*/
final public function __serialize(): array
{
// Compressing state representation to avoid default values, eg pointcutExpression = '' or adviceOrder = 0
$state = array_filter(get_object_vars($this));

// Override closure with array representation to enable serialization
$state['adviceMethod'] = static::serializeAdvice($this->adviceMethod);

return $state;
Expand All @@ -141,7 +141,7 @@ final public function __serialize(): array
/**
* Un-serializes an interceptor from it's stored state
*
* @param array $state The stored representation of the interceptor.
* @param array{adviceMethod: array{name: string, class?: string}} $state The stored representation of the interceptor.
*/
final public function __unserialize(array $state): void
{
Expand Down
16 changes: 3 additions & 13 deletions src/Aop/Framework/AbstractInvocation.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,26 +20,16 @@
abstract class AbstractInvocation extends AbstractJoinpoint implements Invocation
{
/**
* Arguments for invocation
* @var array<mixed> Arguments for invocation, can be mutated by the {@see setArguments()} method
*/
protected array $arguments = [];

/**
* Gets arguments for current invocation
*
* @api
*/
public function getArguments(): array
final public function getArguments(): array
{
return $this->arguments;
}

/**
* Sets arguments for current invocation
*
* @api
*/
public function setArguments(array $arguments): void
final public function setArguments(array $arguments): void
{
$this->arguments = $arguments;
}
Expand Down
50 changes: 11 additions & 39 deletions src/Aop/Framework/AbstractJoinpoint.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@
use Go\Aop\AdviceBefore;
use Go\Aop\Intercept\Interceptor;
use Go\Aop\Intercept\Joinpoint;

use function is_array;
use Go\Aop\OrderedAdvice;

/**
* Abstract joinpoint for framework
Expand All @@ -33,26 +32,11 @@
*/
abstract class AbstractJoinpoint implements Joinpoint
{
/**
* List of advices (interceptors)
*
* NB: All current children assume that each advice is Interceptor now.
* Whereas, it isn't correct logically, this can be used to satisfy PHPStan check now.
*
* @var array<Interceptor>
*/
protected array $advices = [];

/**
* Current advice index
*/
protected int $current = 0;

/**
* Stack frames to work with recursive calls or with cross-calls inside object
*/
protected array $stackFrames = [];

/**
* Recursion level for invocation
*/
Expand All @@ -63,10 +47,7 @@ abstract class AbstractJoinpoint implements Joinpoint
*
* @param array<Interceptor> $advices List of advices (interceptors)
*/
public function __construct(array $advices)
{
$this->advices = $advices;
}
public function __construct(protected readonly array $advices = []) {}

/**
* Sorts advices by priority
Expand All @@ -80,23 +61,12 @@ public static function sortAdvices(array $advices): array
$sortedAdvices = $advices;
uasort(
$sortedAdvices,
function (Advice $first, Advice $second) {
switch (true) {
case $first instanceof AdviceBefore && !($second instanceof AdviceBefore):
return -1;

case $first instanceof AdviceAround && !($second instanceof AdviceAround):
return 1;

case $first instanceof AdviceAfter && !($second instanceof AdviceAfter):
return $second instanceof AdviceBefore ? 1 : -1;

case ($first instanceof OrderedAdvice && $second instanceof OrderedAdvice):
return $first->getAdviceOrder() - $second->getAdviceOrder();

default:
return 0;
}
fn(Advice $first, Advice $second) => match (true) {
$first instanceof AdviceBefore && !($second instanceof AdviceBefore) => -1,
$first instanceof AdviceAround && !($second instanceof AdviceAround) => 1,
$first instanceof AdviceAfter && !($second instanceof AdviceAfter) => $second instanceof AdviceBefore ? 1 : -1,
$first instanceof OrderedAdvice && $second instanceof OrderedAdvice => $first->getAdviceOrder() - $second->getAdviceOrder(),
default => 0,
}
);

Expand All @@ -106,7 +76,9 @@ function (Advice $first, Advice $second) {
/**
* Replace concrete advices with list of ids
*
* @param Advice[][][] $advices List of advices
* @param array<array<array<string, Advice|Interceptor>>> $advices List of advices
*
* @return array<array<array<string>>> Sorted identifier of advices/interceptors
*/
public static function flatAndSortAdvices(array $advices): array
{
Expand Down
59 changes: 25 additions & 34 deletions src/Aop/Framework/AbstractMethodInvocation.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

namespace Go\Aop\Framework;

use Go\Aop\Intercept\Interceptor;
use Go\Aop\Intercept\MethodInvocation;
use ReflectionMethod;
use function array_merge;
Expand All @@ -23,20 +24,7 @@
*/
abstract class AbstractMethodInvocation extends AbstractInvocation implements MethodInvocation
{
/**
* Instance of object for invoking
*/
protected ?object $instance;

/**
* Instance of reflection method for invocation
*/
protected ReflectionMethod $reflectionMethod;

/**
* Class name scope for static invocation
*/
protected string $scope = '';
protected readonly ReflectionMethod $reflectionMethod;

/**
* This static string variable holds the name of field to use to avoid extra "if" section in the __invoke method
Expand All @@ -45,30 +33,36 @@ abstract class AbstractMethodInvocation extends AbstractInvocation implements Me
*/
protected static string $propertyName;

/**
* Stack frames to work with recursive calls or with cross-calls inside object
*
* @var (array&array<int, array{array<mixed>, object|class-string, int}>)
*/
private array $stackFrames = [];

/**
* Constructor for method invocation
*
* @param array $advices List of advices for this invocation
* @param array<Interceptor> $advices List of advices for this invocation
* @param (string&class-string) $className Class, containing method to invoke
* @param (string&non-empty-string) $methodName Name of the method to invoke
*/
public function __construct(array $advices, string $className, string $methodName)
{
parent::__construct($advices);
$this->reflectionMethod = new ReflectionMethod($className, $methodName);
$reflectionMethod = new ReflectionMethod($className, $methodName);

// If we have method inside AOP proxy class, we would like to use prototype instead
if ($reflectionMethod->hasPrototype()) {
$reflectionMethod = $reflectionMethod->getPrototype();
}
$this->reflectionMethod = $reflectionMethod;
}

/**
* Invokes current method invocation with all interceptors
*
* @param null|object|string $instance Invocation instance (class name for static methods)
* @param array $arguments List of arguments for method invocation
* @param array $variadicArguments Additional list of variadic arguments
*
* @return mixed Result of invocation
*/
final public function __invoke($instance = null, array $arguments = [], array $variadicArguments = [])
final public function __invoke(object|string $instanceOrScope, array $arguments = [], array $variadicArguments = []): mixed
{
if ($this->level > 0) {
$this->stackFrames[] = [$this->arguments, $this->scope, $this->instance, $this->current];
$this->stackFrames[] = [$this->arguments, $this->{static::$propertyName}, $this->current];
}

if (count($variadicArguments) > 0) {
Expand All @@ -81,24 +75,21 @@ final public function __invoke($instance = null, array $arguments = [], array $v
$this->current = 0;
$this->arguments = $arguments;

$this->{static::$propertyName} = $instance;
$this->{static::$propertyName} = $instanceOrScope;

return $this->proceed();
} finally {
--$this->level;

if ($this->level > 0) {
[$this->arguments, $this->scope, $this->instance, $this->current] = array_pop($this->stackFrames);
if ($this->level > 0 && ($stackFrame = array_pop($this->stackFrames))) {
[$this->arguments, $this->{static::$propertyName}, $this->current] = $stackFrame;
} else {
$this->instance = null;
unset($this->{static::$propertyName});
$this->arguments = [];
}
}
}

/**
* Gets the method being called.
*/
public function getMethod(): ReflectionMethod
{
return $this->reflectionMethod;
Expand Down
Loading

0 comments on commit 4117b7d

Please sign in to comment.