Skip to content

Commit 8fe06dd

Browse files
committed
[BC Break] New improved Pointcut sub-system
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.
1 parent f04e776 commit 8fe06dd

File tree

66 files changed

+1838
-2055
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

66 files changed

+1838
-2055
lines changed

Diff for: phpstan-baseline.php

-5
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,6 @@
11
<?php declare(strict_types = 1);
22

33
$ignoreErrors = [];
4-
$ignoreErrors[] = [
5-
'message' => '#^Instanceof between stdClass and Go\\\\ParserReflection\\\\ReflectionFileNamespace will always evaluate to false\\.$#',
6-
'count' => 1,
7-
'path' => __DIR__ . '/src/Aop/Support/SimpleNamespaceFilter.php',
8-
];
94
$ignoreErrors[] = [
105
'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\\{\\}\\.$#',
116
'count' => 1,

Diff for: src/Aop/Framework/DynamicInvocationMatcherInterceptor.php

+4-4
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
use Go\Aop\Intercept\Interceptor;
1616
use Go\Aop\Intercept\Joinpoint;
1717
use Go\Aop\Intercept\MethodInvocation;
18-
use Go\Aop\PointFilter;
18+
use Go\Aop\Pointcut;
1919
use ReflectionClass;
2020

2121
/**
@@ -27,10 +27,10 @@
2727
readonly class DynamicInvocationMatcherInterceptor implements Interceptor
2828
{
2929
/**
30-
* Dynamic matcher constructor
30+
* Dynamic invocation matcher constructor
3131
*/
3232
public function __construct(
33-
private PointFilter $pointFilter,
33+
private Pointcut $pointcut,
3434
private Interceptor $interceptor
3535
){}
3636

@@ -40,7 +40,7 @@ final public function invoke(Joinpoint $joinpoint): mixed
4040
$method = $joinpoint->getMethod();
4141
$context = $joinpoint->getThis() ?? $joinpoint->getScope();
4242
$contextClass = new ReflectionClass($context);
43-
if ($this->pointFilter->matches($method, $contextClass, $context, $joinpoint->getArguments())) {
43+
if ($this->pointcut->matches($contextClass, $method, $context, $joinpoint->getArguments())) {
4444
return $this->interceptor->invoke($joinpoint);
4545
}
4646
}

Diff for: src/Aop/IntroductionAdvisor.php

-31
This file was deleted.

Diff for: src/Aop/PointFilter.php

-57
This file was deleted.

Diff for: src/Aop/Pointcut.php

+68-8
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,77 @@
1212

1313
namespace Go\Aop;
1414

15+
use Go\ParserReflection\ReflectionFileNamespace;
16+
use ReflectionClass;
17+
use ReflectionFunction;
18+
use ReflectionMethod;
19+
use ReflectionProperty;
20+
1521
/**
16-
* Pointcut realization for PHP
22+
* Pointcut is responsible for matching any reflection items both statically and dynamically.
23+
*
24+
* Pointcut may be evaluated statically or at runtime (dynamically).
25+
* Matcher uses smart technique of matching elements, consisting of several stages described below.
26+
*
27+
* <b>Static matching</b>
28+
*
29+
* First stage of static matching involves context only (just one argument). This pre-stage is used to optimize
30+
* filtering on matcher side to avoid nested loops of checks. For example, if we have a method pointcut, but
31+
* it doesn't match first with class, then we don't need to scan all methods at all and can exit earlier.
32+
*
33+
* Here is a mapping of context for different static joinpoints:
34+
* - For any traits or classes, context will be `ReflectionClass` corresponding to the given class or trait.
35+
* - For any functions, context will be `ReflectionFileNamespace` where internal function is analyzed.
36+
* - For any methods or properties, context will be `ReflectionClass` which is currently analysed (even for inherited items)
37+
*
38+
* Second stage of static matching uses exactly two arguments (context and reflector). Filter then fully checks
39+
* static information from reflection to make a decision about matching of given point.
40+
*
41+
* At this stage we can verify names, attributes, signature, parameters, types, etc.
1742
*
18-
* Pointcuts are defined as a predicate over the syntax-tree of the program, and define an interface that constrains
19-
* which elements of the base program are exposed by the pointcut. A pointcut picks out certain join points and values
20-
* at those points
43+
* If point filter is not dynamic {@see self::KIND_DYNAMIC}, then evaluation ends here statically,
44+
* and generated code will not contain any runtime checks for given point filter, allowing for better performance.
45+
*
46+
* <b>Dynamic matching</b>
47+
*
48+
* If instance of filter is dynamic and uses {@see self::KIND_DYNAMIC} flag, then after static matching which has been
49+
* used to prepare a dynamic hook, framework will call our pointcut again in runtime for dynamic matching.
50+
*
51+
* This dynamic matching stage uses full information about given join point, including possible instance/scope and
52+
* arguments for a particular point.
2153
*/
22-
interface Pointcut extends PointFilter
54+
interface Pointcut
2355
{
56+
public const KIND_METHOD = 1;
57+
public const KIND_PROPERTY = 2;
58+
public const KIND_CLASS = 4;
59+
public const KIND_TRAIT = 8;
60+
public const KIND_FUNCTION = 16;
61+
public const KIND_INIT = 32;
62+
public const KIND_STATIC_INIT = 64;
63+
public const KIND_ALL = 127;
64+
public const KIND_DYNAMIC = 256;
65+
public const KIND_INTRODUCTION = 512;
66+
67+
/**
68+
* Returns the kind of point filter
69+
*/
70+
public function getKind(): int;
71+
2472
/**
25-
* Return the class filter for this pointcut.
73+
* Performs matching of point of code, returns true if point matches
74+
*
75+
* @param ReflectionClass<T>|ReflectionFileNamespace $context Related context, can be class or file namespace
76+
* @param ReflectionMethod|ReflectionProperty|ReflectionFunction|null $reflector Specific part of code, can be any Reflection class
77+
* @param null|(string&class-string<T>)|(object&T) $instanceOrScope Invocation instance or string for static calls
78+
* @param null|array<mixed> $arguments Dynamic arguments for method
79+
*
80+
* @template T of object
2681
*/
27-
public function getClassFilter(): PointFilter;
28-
}
82+
public function matches(
83+
ReflectionClass|ReflectionFileNamespace $context,
84+
ReflectionMethod|ReflectionProperty|ReflectionFunction $reflector = null,
85+
object|string $instanceOrScope = null,
86+
array $arguments = null
87+
): bool;
88+
}

Diff for: src/Aop/Pointcut/AndPointcut.php

+36-61
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?php
22

3-
declare(strict_types=1);
3+
declare(strict_types = 1);
44
/*
55
* Go! AOP framework
66
*
@@ -13,87 +13,62 @@
1313
namespace Go\Aop\Pointcut;
1414

1515
use Go\Aop\Pointcut;
16-
use Go\Aop\Support\AndPointFilter;
16+
use Go\ParserReflection\ReflectionFileNamespace;
1717
use ReflectionClass;
18+
use ReflectionFunction;
1819
use ReflectionMethod;
1920
use ReflectionProperty;
2021

2122
/**
22-
* Logical "AND" pointcut that combines two simple pointcuts
23+
* Logical "and" pointcut filter.
2324
*/
24-
class AndPointcut implements Pointcut
25+
final readonly class AndPointcut implements Pointcut
2526
{
26-
use PointcutClassFilterTrait;
27-
28-
/**
29-
* First pointcut
30-
*/
31-
protected Pointcut $first;
32-
3327
/**
34-
* Second pointcut
28+
* Kind of pointcut
3529
*/
36-
protected Pointcut $second;
30+
private int $pointcutKind;
3731

3832
/**
39-
* Returns pointcut kind
33+
* List of Pointcut to combine with "AND"
34+
*
35+
* @var array<Pointcut>
4036
*/
41-
protected int $kind;
37+
private array $pointcuts;
4238

4339
/**
44-
* "And" pointcut constructor
40+
* And constructor
4541
*/
46-
public function __construct(Pointcut $first, Pointcut $second)
42+
public function __construct(int $pointcutKind = null, Pointcut ...$pointcuts)
4743
{
48-
$this->first = $first;
49-
$this->second = $second;
50-
$this->kind = $first->getKind() & $second->getKind();
51-
52-
$this->classFilter = new AndPointFilter($first->getClassFilter(), $second->getClassFilter());
44+
// If we don't have specified kind, it will be calculated as intersection then
45+
if (!isset($pointcutKind)) {
46+
$pointcutKind = -1;
47+
foreach ($pointcuts as $singlePointcut) {
48+
$pointcutKind &= $singlePointcut->getKind();
49+
}
50+
}
51+
$this->pointcutKind = $pointcutKind;
52+
$this->pointcuts = $pointcuts;
5353
}
5454

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

69-
/**
70-
* Returns the kind of point filter
71-
*/
7270
public function getKind(): int
7371
{
74-
return $this->kind;
75-
}
76-
77-
/**
78-
* Checks if point filter matches the point
79-
*
80-
* @param Pointcut $pointcut
81-
* @param ReflectionMethod|ReflectionProperty|ReflectionClass $point
82-
* @param mixed $context Related context, can be class or namespace
83-
* @param object|string|null $instance [Optional] Instance for dynamic matching
84-
* @param array|null $arguments [Optional] Extra arguments for dynamic
85-
* matching
86-
*
87-
* @return bool
88-
*/
89-
protected function matchPart(
90-
Pointcut $pointcut,
91-
$point,
92-
$context = null,
93-
$instance = null,
94-
array $arguments = null
95-
): bool {
96-
return $pointcut->matches($point, $context, $instance, $arguments)
97-
&& $pointcut->getClassFilter()->matches($context);
72+
return $this->pointcutKind;
9873
}
9974
}

0 commit comments

Comments
 (0)