Skip to content

Commit

Permalink
[BC Break] New improved Pointcut sub-system
Browse files Browse the repository at this point in the history
This major refactoring removed the duplication around PointFilter and Pointcut.
PHP8 features and types have been applied to the namespace for strong typing.
Now only one Pointcut interface is responsible for matching of joinpoint.
New tests were added, PhpStan level 9 check achieved for the Pointcut namespace now.

Signature of Pointcut has been changed to highlight logic of 3-stage matching.
Now first element of Pointcut->matches() is always context to streamline optimizations.

IntroductionAdvisor has been removed too to unify logic inside GenericPointcutAdvisor.
All filters from the Support namespace have been removed or refactored/renamed into pointcuts.
  • Loading branch information
lisachenko committed Apr 13, 2024
1 parent f04e776 commit ca01ea8
Show file tree
Hide file tree
Showing 67 changed files with 1,879 additions and 2,053 deletions.
5 changes: 0 additions & 5 deletions phpstan-baseline.php
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
<?php declare(strict_types = 1);

$ignoreErrors = [];
$ignoreErrors[] = [
'message' => '#^Instanceof between stdClass and Go\\\\ParserReflection\\\\ReflectionFileNamespace will always evaluate to false\\.$#',
'count' => 1,
'path' => __DIR__ . '/src/Aop/Support/SimpleNamespaceFilter.php',
];
$ignoreErrors[] = [
'message' => '#^Property Doctrine\\\\ORM\\\\Mapping\\\\ClassMetadataInfo\\<object\\>\\:\\:\\$table \\(array\\{name\\: string, schema\\?\\: string, indexes\\?\\: array, uniqueConstraints\\?\\: array, options\\?\\: array\\<string, mixed\\>, quoted\\?\\: bool\\}\\) does not accept array\\{\\}\\.$#',
'count' => 1,
Expand Down
8 changes: 4 additions & 4 deletions src/Aop/Framework/DynamicInvocationMatcherInterceptor.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
use Go\Aop\Intercept\Interceptor;
use Go\Aop\Intercept\Joinpoint;
use Go\Aop\Intercept\MethodInvocation;
use Go\Aop\PointFilter;
use Go\Aop\Pointcut;
use ReflectionClass;

/**
Expand All @@ -27,10 +27,10 @@
readonly class DynamicInvocationMatcherInterceptor implements Interceptor
{
/**
* Dynamic matcher constructor
* Dynamic invocation matcher constructor
*/
public function __construct(
private PointFilter $pointFilter,
private Pointcut $pointcut,
private Interceptor $interceptor
){}

Expand All @@ -40,7 +40,7 @@ final public function invoke(Joinpoint $joinpoint): mixed
$method = $joinpoint->getMethod();
$context = $joinpoint->getThis() ?? $joinpoint->getScope();
$contextClass = new ReflectionClass($context);
if ($this->pointFilter->matches($method, $contextClass, $context, $joinpoint->getArguments())) {
if ($this->pointcut->matches($contextClass, $method, $context, $joinpoint->getArguments())) {
return $this->interceptor->invoke($joinpoint);
}
}
Expand Down
31 changes: 0 additions & 31 deletions src/Aop/IntroductionAdvisor.php

This file was deleted.

57 changes: 0 additions & 57 deletions src/Aop/PointFilter.php

This file was deleted.

76 changes: 68 additions & 8 deletions src/Aop/Pointcut.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,77 @@

namespace Go\Aop;

use Go\ParserReflection\ReflectionFileNamespace;
use ReflectionClass;
use ReflectionFunction;
use ReflectionMethod;
use ReflectionProperty;

/**
* Pointcut realization for PHP
* Pointcut is responsible for matching any reflection items both statically and dynamically.
*
* Pointcut may be evaluated statically or at runtime (dynamically).
* Matcher uses smart technique of matching elements, consisting of several stages described below.
*
* <b>Static matching</b>
*
* First stage of static matching involves context only (just one argument). This pre-stage is used to optimize
* filtering on matcher side to avoid nested loops of checks. For example, if we have a method pointcut, but
* it doesn't match first with class, then we don't need to scan all methods at all and can exit earlier.
*
* Here is a mapping of context for different static joinpoints:
* - For any traits or classes, context will be `ReflectionClass` corresponding to the given class or trait.
* - For any functions, context will be `ReflectionFileNamespace` where internal function is analyzed.
* - For any methods or properties, context will be `ReflectionClass` which is currently analysed (even for inherited items)
*
* Second stage of static matching uses exactly two arguments (context and reflector). Filter then fully checks
* static information from reflection to make a decision about matching of given point.
*
* At this stage we can verify names, attributes, signature, parameters, types, etc.
*
* Pointcuts are defined as a predicate over the syntax-tree of the program, and define an interface that constrains
* which elements of the base program are exposed by the pointcut. A pointcut picks out certain join points and values
* at those points
* If point filter is not dynamic {@see self::KIND_DYNAMIC}, then evaluation ends here statically,
* and generated code will not contain any runtime checks for given point filter, allowing for better performance.
*
* <b>Dynamic matching</b>
*
* If instance of filter is dynamic and uses {@see self::KIND_DYNAMIC} flag, then after static matching which has been
* used to prepare a dynamic hook, framework will call our pointcut again in runtime for dynamic matching.
*
* This dynamic matching stage uses full information about given join point, including possible instance/scope and
* arguments for a particular point.
*/
interface Pointcut extends PointFilter
interface Pointcut
{
public const KIND_METHOD = 1;
public const KIND_PROPERTY = 2;
public const KIND_CLASS = 4;
public const KIND_TRAIT = 8;
public const KIND_FUNCTION = 16;
public const KIND_INIT = 32;
public const KIND_STATIC_INIT = 64;
public const KIND_ALL = 127;
public const KIND_DYNAMIC = 256;
public const KIND_INTRODUCTION = 512;

/**
* Returns the kind of point filter
*/
public function getKind(): int;

/**
* Return the class filter for this pointcut.
* Performs matching of point of code, returns true if point matches
*
* @param ReflectionClass<T>|ReflectionFileNamespace $context Related context, can be class or file namespace
* @param ReflectionMethod|ReflectionProperty|ReflectionFunction|null $reflector Specific part of code, can be any Reflection class
* @param null|(string&class-string<T>)|(object&T) $instanceOrScope Invocation instance or string for static calls
* @param null|array<mixed> $arguments Dynamic arguments for method
*
* @template T of object
*/
public function getClassFilter(): PointFilter;
}
public function matches(
ReflectionClass|ReflectionFileNamespace $context,
ReflectionMethod|ReflectionProperty|ReflectionFunction $reflector = null,
object|string $instanceOrScope = null,
array $arguments = null
): bool;
}
97 changes: 36 additions & 61 deletions src/Aop/Pointcut/AndPointcut.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php

declare(strict_types=1);
declare(strict_types = 1);
/*
* Go! AOP framework
*
Expand All @@ -13,87 +13,62 @@
namespace Go\Aop\Pointcut;

use Go\Aop\Pointcut;
use Go\Aop\Support\AndPointFilter;
use Go\ParserReflection\ReflectionFileNamespace;
use ReflectionClass;
use ReflectionFunction;
use ReflectionMethod;
use ReflectionProperty;

/**
* Logical "AND" pointcut that combines two simple pointcuts
* Logical "and" pointcut filter.
*/
class AndPointcut implements Pointcut
final readonly class AndPointcut implements Pointcut
{
use PointcutClassFilterTrait;

/**
* First pointcut
*/
protected Pointcut $first;

/**
* Second pointcut
* Kind of pointcut
*/
protected Pointcut $second;
private int $pointcutKind;

/**
* Returns pointcut kind
* List of Pointcut to combine with "AND"
*
* @var array<Pointcut>
*/
protected int $kind;
private array $pointcuts;

/**
* "And" pointcut constructor
* And constructor
*/
public function __construct(Pointcut $first, Pointcut $second)
public function __construct(int $pointcutKind = null, Pointcut ...$pointcuts)
{
$this->first = $first;
$this->second = $second;
$this->kind = $first->getKind() & $second->getKind();

$this->classFilter = new AndPointFilter($first->getClassFilter(), $second->getClassFilter());
// If we don't have specified kind, it will be calculated as intersection then
if (!isset($pointcutKind)) {
$pointcutKind = -1;
foreach ($pointcuts as $singlePointcut) {
$pointcutKind &= $singlePointcut->getKind();
}
}
$this->pointcutKind = $pointcutKind;
$this->pointcuts = $pointcuts;
}

/**
* Performs matching of point of code
*
* @param mixed $point Specific part of code, can be any Reflection class
* @param null|mixed $context Related context, can be class or namespace
* @param null|string|object $instance Invocation instance or string for static calls
* @param null|array $arguments Dynamic arguments for method
*/
public function matches($point, $context = null, $instance = null, array $arguments = null): bool
{
return $this->matchPart($this->first, $point, $context, $instance, $arguments)
&& $this->matchPart($this->second, $point, $context, $instance, $arguments);
public function matches(
ReflectionClass|ReflectionFileNamespace $context,
ReflectionMethod|ReflectionProperty|ReflectionFunction $reflector = null,
object|string $instanceOrScope = null,
array $arguments = null
): bool {
foreach ($this->pointcuts as $singlePointcut) {
if (!$singlePointcut->matches($context, $reflector, $instanceOrScope, $arguments)) {
return false;
}
}

return true;
}

/**
* Returns the kind of point filter
*/
public function getKind(): int
{
return $this->kind;
}

/**
* Checks if point filter matches the point
*
* @param Pointcut $pointcut
* @param ReflectionMethod|ReflectionProperty|ReflectionClass $point
* @param mixed $context Related context, can be class or namespace
* @param object|string|null $instance [Optional] Instance for dynamic matching
* @param array|null $arguments [Optional] Extra arguments for dynamic
* matching
*
* @return bool
*/
protected function matchPart(
Pointcut $pointcut,
$point,
$context = null,
$instance = null,
array $arguments = null
): bool {
return $pointcut->matches($point, $context, $instance, $arguments)
&& $pointcut->getClassFilter()->matches($context);
return $this->pointcutKind;
}
}
Loading

0 comments on commit ca01ea8

Please sign in to comment.