From f04e7761053bf44486e60577ce1365e98fd2046b Mon Sep 17 00:00:00 2001 From: Alexander Lisachenko Date: Tue, 2 Apr 2024 21:36:54 +0300 Subject: [PATCH 1/2] [BC Break] Refactor ClassMemberReference to the readonly PHP8.2 DTO --- src/Aop/Pointcut/ClassMemberReference.php | 77 +++-------------------- src/Aop/Pointcut/PointcutGrammar.php | 32 +++++----- 2 files changed, 26 insertions(+), 83 deletions(-) diff --git a/src/Aop/Pointcut/ClassMemberReference.php b/src/Aop/Pointcut/ClassMemberReference.php index 7e1633df..e00dde01 100644 --- a/src/Aop/Pointcut/ClassMemberReference.php +++ b/src/Aop/Pointcut/ClassMemberReference.php @@ -16,79 +16,22 @@ use Go\Aop\Support\ModifierMatcherFilter; /** - * Data transfer object for storing a reference to the class member (property or method) + * Readonly data transfer object for storing a reference to the class member (property or method) */ -class ClassMemberReference +readonly class ClassMemberReference { - /** - * Filter for class names - */ - private PointFilter $classFilter; - - /** - * Member visibility filter (public/protected/etc) - */ - private ModifierMatcherFilter $visibilityFilter; - - /** - * Filter for access type (statically "::" or dynamically "->") - */ - private ModifierMatcherFilter $accessTypeFilter; - - /** - * Member name pattern - */ - private string $memberNamePattern; - /** * Default constructor * - * @param PointFilter $classFilter - * @param ModifierMatcherFilter $visibilityFilter Public/protected/etc - * @param ModifierMatcherFilter $accessTypeFilter Static or dynamic + * @param PointFilter $classFilter Filter for class names + * @param ModifierMatcherFilter $visibilityFilter Member visibility filter (public/protected/etc) + * @param ModifierMatcherFilter $accessTypeFilter Filter for access type (statically "::" or dynamically "->") * @param string $memberNamePattern Expression for the name */ public function __construct( - PointFilter $classFilter, - ModifierMatcherFilter $visibilityFilter, - ModifierMatcherFilter $accessTypeFilter, - string $memberNamePattern - ) { - $this->classFilter = $classFilter; - $this->visibilityFilter = $visibilityFilter; - $this->accessTypeFilter = $accessTypeFilter; - $this->memberNamePattern = $memberNamePattern; - } - - /** - * Returns the filter for class - */ - public function getClassFilter(): PointFilter - { - return $this->classFilter; - } - - /** - * Returns the filter for visibility: public/protected/private - */ - public function getVisibilityFilter(): ModifierMatcherFilter - { - return $this->visibilityFilter; - } - - /** - * Returns the filter for access type: static/dynamic - */ - public function getAccessTypeFilter(): ModifierMatcherFilter - { - return $this->accessTypeFilter; - } - - /** - * Returns the pattern for member name - */ - public function getMemberNamePattern(): string - { - return $this->memberNamePattern; - } + public PointFilter $classFilter, + public ModifierMatcherFilter $visibilityFilter, + public ModifierMatcherFilter $accessTypeFilter, + public string $memberNamePattern + ) {} } diff --git a/src/Aop/Pointcut/PointcutGrammar.php b/src/Aop/Pointcut/PointcutGrammar.php index 5499b20d..e3531843 100644 --- a/src/Aop/Pointcut/PointcutGrammar.php +++ b/src/Aop/Pointcut/PointcutGrammar.php @@ -181,12 +181,12 @@ function ($_0, $_1, $classFilter) { ->call( function ($_0, $_1, ClassMemberReference $reference) { $memberFilter = new AndPointFilter( - $reference->getVisibilityFilter(), - $reference->getAccessTypeFilter() + $reference->visibilityFilter, + $reference->accessTypeFilter ); - $pointcut = new MagicMethodPointcut($reference->getMemberNamePattern(), $memberFilter); - $pointcut->setClassFilter($reference->getClassFilter()); + $pointcut = new MagicMethodPointcut($reference->memberNamePattern, $memberFilter); + $pointcut->setClassFilter($reference->classFilter); return $pointcut; } @@ -203,17 +203,17 @@ function ($_0, $_1, ClassMemberReference $reference) { ->call( function (ClassMemberReference $reference) { $memberFilter = new AndPointFilter( - $reference->getVisibilityFilter(), - $reference->getAccessTypeFilter() + $reference->visibilityFilter, + $reference->accessTypeFilter ); $pointcut = new SignaturePointcut( PointFilter::KIND_PROPERTY, - $reference->getMemberNamePattern(), + $reference->memberNamePattern, $memberFilter ); - $pointcut->setClassFilter($reference->getClassFilter()); + $pointcut->setClassFilter($reference->classFilter); return $pointcut; } @@ -225,17 +225,17 @@ function (ClassMemberReference $reference) { ->call( function (ClassMemberReference $reference) { $memberFilter = new AndPointFilter( - $reference->getVisibilityFilter(), - $reference->getAccessTypeFilter() + $reference->visibilityFilter, + $reference->accessTypeFilter ); $pointcut = new SignaturePointcut( PointFilter::KIND_METHOD, - $reference->getMemberNamePattern(), + $reference->memberNamePattern, $memberFilter ); - $pointcut->setClassFilter($reference->getClassFilter()); + $pointcut->setClassFilter($reference->classFilter); return $pointcut; } @@ -244,18 +244,18 @@ function (ClassMemberReference $reference) { ->call( function (ClassMemberReference $reference, $_0, $_1, $_2, $_3, $returnType) { $memberFilter = new AndPointFilter( - $reference->getVisibilityFilter(), - $reference->getAccessTypeFilter(), + $reference->visibilityFilter, + $reference->accessTypeFilter, new ReturnTypeFilter($returnType) ); $pointcut = new SignaturePointcut( PointFilter::KIND_METHOD, - $reference->getMemberNamePattern(), + $reference->memberNamePattern, $memberFilter ); - $pointcut->setClassFilter($reference->getClassFilter()); + $pointcut->setClassFilter($reference->classFilter); return $pointcut; } From ca01ea80e4703ec4a8ab0b4e2995e5abf0a4a328 Mon Sep 17 00:00:00 2001 From: Alexander Lisachenko Date: Sat, 13 Apr 2024 15:28:34 +0300 Subject: [PATCH 2/2] [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. --- phpstan-baseline.php | 5 - .../DynamicInvocationMatcherInterceptor.php | 8 +- src/Aop/IntroductionAdvisor.php | 31 --- src/Aop/PointFilter.php | 57 ------ src/Aop/Pointcut.php | 76 +++++++- src/Aop/Pointcut/AndPointcut.php | 97 ++++------ src/Aop/Pointcut/AttributePointcut.php | 91 ++++----- src/Aop/Pointcut/CFlowBelowMethodPointcut.php | 91 --------- src/Aop/Pointcut/ClassInheritancePointcut.php | 53 ++++++ src/Aop/Pointcut/ClassMemberReference.php | 21 +-- src/Aop/Pointcut/FunctionPointcut.php | 101 ---------- .../Pointcut/MagicMethodDynamicPointcut.php | 93 +++++++++ src/Aop/Pointcut/MagicMethodPointcut.php | 91 --------- src/Aop/Pointcut/MatchInheritedPointcut.php | 45 +++-- .../ModifierPointcut.php} | 51 +++-- src/Aop/Pointcut/NamePointcut.php | 79 ++++++++ src/Aop/Pointcut/NotPointcut.php | 64 +++---- src/Aop/Pointcut/OrPointcut.php | 78 ++++---- src/Aop/Pointcut/PointcutClassFilterTrait.php | 49 ----- src/Aop/Pointcut/PointcutGrammar.php | 176 +++++++----------- src/Aop/Pointcut/PointcutLexer.php | 3 +- src/Aop/Pointcut/PointcutParseTable.php | 4 +- src/Aop/Pointcut/PointcutParser.php | 20 +- src/Aop/Pointcut/PointcutReference.php | 68 +++---- src/Aop/Pointcut/ReturnTypePointcut.php | 92 +++++++++ src/Aop/Pointcut/SignaturePointcut.php | 88 --------- src/Aop/Pointcut/TruePointcut.php | 42 ++--- src/Aop/PointcutAdvisor.php | 2 - src/Aop/Support/AbstractGenericAdvisor.php | 43 ----- src/Aop/Support/AndPointFilter.php | 71 ------- src/Aop/Support/DeclareParentsAdvisor.php | 35 ---- ...Advisor.php => GenericPointcutAdvisor.php} | 34 +--- src/Aop/Support/InheritanceClassFilter.php | 60 ------ src/Aop/Support/LazyPointcutAdvisor.php | 39 ++-- .../Support/NamespacedReflectionFunction.php | 49 ----- src/Aop/Support/NotPointFilter.php | 61 ------ src/Aop/Support/OrPointFilter.php | 71 ------- src/Aop/Support/PointcutBuilder.php | 15 +- src/Aop/Support/ReturnTypeFilter.php | 75 -------- src/Aop/Support/SimpleNamespaceFilter.php | 73 -------- src/Aop/Support/TruePointFilter.php | 57 ------ src/Core/AbstractAspectLoaderExtension.php | 57 +++--- src/Core/AdviceMatcher.php | 96 +++++----- src/Core/AspectLoaderExtension.php | 6 +- src/Core/AttributeAspectLoaderExtension.php | 55 +++--- src/Core/IntroductionAspectExtension.php | 62 +++--- src/Lang/Attribute/DeclareError.php | 2 +- .../AndPointcutTest.php} | 32 ++-- .../Go/Aop/Pointcut/AttributePointcutTest.php | 136 ++++++++++++++ .../Pointcut/ClassInheritancePointcutTest.php | 57 ++++++ .../MagicMethodDynamicPointcutTest.php | 136 ++++++++++++++ .../Pointcut/MatchInheritedPointcutTest.php | 104 +++++++++++ .../Go/Aop/Pointcut/ModifierPointcutTest.php | 129 +++++++++++++ tests/Go/Aop/Pointcut/NamePointcutTest.php | 141 ++++++++++++++ tests/Go/Aop/Pointcut/NotPointcutTest.php | 35 +++- .../OrPointcutTest.php} | 34 ++-- tests/Go/Aop/Pointcut/PointcutParserTest.php | 3 - .../Aop/Pointcut/ReturnTypePointcutTest.php | 83 +++++++++ .../Go/Aop/Pointcut/SignaturePointcutTest.php | 122 ------------ tests/Go/Aop/Pointcut/TruePointcutTest.php | 41 ++-- tests/Go/Aop/Support/NotPointFilterTest.php | 38 ---- tests/Go/Aop/Support/TruePointFilterTest.php | 43 ----- tests/Go/Core/AdviceMatcherTest.php | 58 +++--- tests/Go/Stubs/ClassWithMagicMethods.php | 47 +++++ tests/Go/Stubs/First.php | 20 ++ tests/Go/Stubs/FirstStatic.php | 15 ++ tests/Go/Stubs/StubAttribute.php | 21 +++ 67 files changed, 1879 insertions(+), 2053 deletions(-) delete mode 100644 src/Aop/IntroductionAdvisor.php delete mode 100644 src/Aop/PointFilter.php delete mode 100644 src/Aop/Pointcut/CFlowBelowMethodPointcut.php create mode 100644 src/Aop/Pointcut/ClassInheritancePointcut.php delete mode 100644 src/Aop/Pointcut/FunctionPointcut.php create mode 100644 src/Aop/Pointcut/MagicMethodDynamicPointcut.php delete mode 100644 src/Aop/Pointcut/MagicMethodPointcut.php rename src/Aop/{Support/ModifierMatcherFilter.php => Pointcut/ModifierPointcut.php} (53%) create mode 100644 src/Aop/Pointcut/NamePointcut.php delete mode 100644 src/Aop/Pointcut/PointcutClassFilterTrait.php create mode 100644 src/Aop/Pointcut/ReturnTypePointcut.php delete mode 100644 src/Aop/Pointcut/SignaturePointcut.php delete mode 100644 src/Aop/Support/AbstractGenericAdvisor.php delete mode 100644 src/Aop/Support/AndPointFilter.php delete mode 100644 src/Aop/Support/DeclareParentsAdvisor.php rename src/Aop/Support/{DefaultPointcutAdvisor.php => GenericPointcutAdvisor.php} (51%) delete mode 100644 src/Aop/Support/InheritanceClassFilter.php delete mode 100644 src/Aop/Support/NamespacedReflectionFunction.php delete mode 100644 src/Aop/Support/NotPointFilter.php delete mode 100644 src/Aop/Support/OrPointFilter.php delete mode 100644 src/Aop/Support/ReturnTypeFilter.php delete mode 100644 src/Aop/Support/SimpleNamespaceFilter.php delete mode 100644 src/Aop/Support/TruePointFilter.php rename tests/Go/Aop/{Support/AndPointFilterTest.php => Pointcut/AndPointcutTest.php} (50%) create mode 100644 tests/Go/Aop/Pointcut/AttributePointcutTest.php create mode 100644 tests/Go/Aop/Pointcut/ClassInheritancePointcutTest.php create mode 100644 tests/Go/Aop/Pointcut/MagicMethodDynamicPointcutTest.php create mode 100644 tests/Go/Aop/Pointcut/MatchInheritedPointcutTest.php create mode 100644 tests/Go/Aop/Pointcut/ModifierPointcutTest.php create mode 100644 tests/Go/Aop/Pointcut/NamePointcutTest.php rename tests/Go/Aop/{Support/OrPointFilterTest.php => Pointcut/OrPointcutTest.php} (50%) create mode 100644 tests/Go/Aop/Pointcut/ReturnTypePointcutTest.php delete mode 100644 tests/Go/Aop/Pointcut/SignaturePointcutTest.php delete mode 100644 tests/Go/Aop/Support/NotPointFilterTest.php delete mode 100644 tests/Go/Aop/Support/TruePointFilterTest.php create mode 100644 tests/Go/Stubs/ClassWithMagicMethods.php create mode 100644 tests/Go/Stubs/StubAttribute.php diff --git a/phpstan-baseline.php b/phpstan-baseline.php index 47c7b57f..1038f02d 100644 --- a/phpstan-baseline.php +++ b/phpstan-baseline.php @@ -1,11 +1,6 @@ '#^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\\\\:\\:\\$table \\(array\\{name\\: string, schema\\?\\: string, indexes\\?\\: array, uniqueConstraints\\?\\: array, options\\?\\: array\\, quoted\\?\\: bool\\}\\) does not accept array\\{\\}\\.$#', 'count' => 1, diff --git a/src/Aop/Framework/DynamicInvocationMatcherInterceptor.php b/src/Aop/Framework/DynamicInvocationMatcherInterceptor.php index f66ddc3f..12e53666 100644 --- a/src/Aop/Framework/DynamicInvocationMatcherInterceptor.php +++ b/src/Aop/Framework/DynamicInvocationMatcherInterceptor.php @@ -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; /** @@ -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 ){} @@ -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); } } diff --git a/src/Aop/IntroductionAdvisor.php b/src/Aop/IntroductionAdvisor.php deleted file mode 100644 index 5a1e179a..00000000 --- a/src/Aop/IntroductionAdvisor.php +++ /dev/null @@ -1,31 +0,0 @@ - - * - * This source file is subject to the license that is bundled - * with this source code in the file LICENSE. - */ - -namespace Go\Aop; - -/** - * Superinterface for advisors that perform one or more AOP introductions. - * - * This interface cannot be implemented directly; subinterfaces must provide the advice type - * implementing the introduction. - * - * Introduction is the implementation of additional interfaces (not implemented by a target) via AOP advice. - */ -interface IntroductionAdvisor extends Advisor -{ - /** - * Returns the filter determining which target classes this introduction should apply to. - * - * This represents the class part of a pointcut. Note that method matching doesn't make sense to introductions. - */ - public function getClassFilter(): PointFilter; -} diff --git a/src/Aop/PointFilter.php b/src/Aop/PointFilter.php deleted file mode 100644 index bb0db164..00000000 --- a/src/Aop/PointFilter.php +++ /dev/null @@ -1,57 +0,0 @@ - - * - * This source file is subject to the license that is bundled - * with this source code in the file LICENSE. - */ - -namespace Go\Aop; - -/** - * Filter that restricts matching of a pointcut or introduction to a given set of reflection points. - * - * A PointFilter may be evaluated statically or at runtime (dynamically). - * - * Static matching involves point and context. Dynamic matching also provides an instance and arguments for - * a particular invocation. - * - * If point filter is not dynamic (self::KIND_DYNAMIC), evaluation can be performed statically, - * and the result will be the same for all invocations of this joinpoint, whatever their arguments. - * - * If an implementation returns true from its 2-arg matches() method and filter is self::KIND_DYNAMIC, - * the 3-arg matches() method will be invoked immediately before each potential execution of the related advice, - * to decide whether the advice should run. All previous advice, such as earlier interceptors in an interceptor chain, - * will have run, so any state changes they have produced in parameters will be available at the time of evaluation. - */ -interface PointFilter -{ - 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; - - /** - * Performs matching of point of code, returns true if point matches - * - * @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; - - /** - * Returns the kind of point filter - */ - public function getKind(): int; -} diff --git a/src/Aop/Pointcut.php b/src/Aop/Pointcut.php index 0cdfff7e..5d40f4b3 100644 --- a/src/Aop/Pointcut.php +++ b/src/Aop/Pointcut.php @@ -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. + * + * Static matching + * + * 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. + * + * Dynamic matching + * + * 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|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)|(object&T) $instanceOrScope Invocation instance or string for static calls + * @param null|array $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; +} \ No newline at end of file diff --git a/src/Aop/Pointcut/AndPointcut.php b/src/Aop/Pointcut/AndPointcut.php index cbc411d8..c1502655 100644 --- a/src/Aop/Pointcut/AndPointcut.php +++ b/src/Aop/Pointcut/AndPointcut.php @@ -1,6 +1,6 @@ */ - 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; } } diff --git a/src/Aop/Pointcut/AttributePointcut.php b/src/Aop/Pointcut/AttributePointcut.php index 9bab0aa8..b6feff14 100644 --- a/src/Aop/Pointcut/AttributePointcut.php +++ b/src/Aop/Pointcut/AttributePointcut.php @@ -13,79 +13,62 @@ namespace Go\Aop\Pointcut; use Go\Aop\Pointcut; -use InvalidArgumentException; +use Go\ParserReflection\ReflectionFileNamespace; use ReflectionClass; +use ReflectionFunction; use ReflectionMethod; use ReflectionProperty; /** - * Annotation property pointcut checks property annotation + * Attribute property pointcut checks joinpoint attributes + * + * @see https://www.php.net/manual/en/reflectionfunctionabstract.getattributes.php + * @see https://www.php.net/manual/en/reflectionclass.getattributes.php + * @see https://www.php.net/manual/en/reflectionproperty.getattributes.php + * */ -class AttributePointcut implements Pointcut +final readonly class AttributePointcut implements Pointcut { - use PointcutClassFilterTrait; - - /** - * Attribute class to match - */ - protected string $attributeClassName; - - /** - * Kind of current filter, can be KIND_CLASS, KIND_METHOD, KIND_PROPERTY, KIND_TRAIT - */ - protected int $filterKind; - - /** - * Specifies name of the expected class to receive - */ - protected string $expectedClass; - - /** - * Static mappings of kind to expected class - */ - protected static array $mappings = [ - self::KIND_CLASS => ReflectionClass::class, - self::KIND_TRAIT => ReflectionClass::class, - self::KIND_METHOD => ReflectionMethod::class, - self::KIND_PROPERTY => ReflectionProperty::class, - ]; - /** * Attribute matcher constructor * - * @param int $filterKind Kind of filter, e.g. KIND_CLASS + * @param int $pointcutKind Kind of current filter, can be KIND_CLASS, KIND_METHOD, KIND_PROPERTY, KIND_TRAIT + * @param (string&class-string) $attributeClassName Attribute class to match */ - public function __construct(int $filterKind, string $attributeClassName) - { - if (!isset(self::$mappings[$filterKind])) { - throw new InvalidArgumentException("Unsupported filter kind {$filterKind}"); + public function __construct( + private int $pointcutKind, + private string $attributeClassName, + private bool $useContextForMatching = false, + ) {} + + final public function matches( + ReflectionClass|ReflectionFileNamespace $context, + ReflectionMethod|ReflectionProperty|ReflectionFunction $reflector = null, + object|string $instanceOrScope = null, + array $arguments = null + ): bool { + // If we don't use context for matching and we do static check, then always match + if (!$this->useContextForMatching && !isset($reflector)) { + return true; } - $this->filterKind = $filterKind; - $this->attributeClassName = $attributeClassName; - $this->expectedClass = self::$mappings[$filterKind]; - } - /** - * @param ReflectionClass|ReflectionMethod|ReflectionProperty $point - * {@inheritdoc} - */ - public function matches($point, $context = null, $instance = null, array $arguments = null): bool - { - $expectedClass = $this->expectedClass; - if (!$point instanceof $expectedClass) { - return false; + // Otherwise we select either context for matching (eg for @within) or reflector (eg for @execution) + if ($this->useContextForMatching) { + $instanceToCheck = $context; + } else { + $instanceToCheck = $reflector; } - $attributes = $point->getAttributes($this->attributeClassName); + if (!isset($instanceToCheck) || !method_exists($instanceToCheck, 'getAttributes')) { + return false; + } - return count($attributes) > 0; + // Final static matching by checking attributes for given reflector + return count($instanceToCheck->getAttributes($this->attributeClassName)) > 0; } - /** - * Returns the kind of point filter - */ public function getKind(): int { - return $this->filterKind; + return $this->pointcutKind; } } diff --git a/src/Aop/Pointcut/CFlowBelowMethodPointcut.php b/src/Aop/Pointcut/CFlowBelowMethodPointcut.php deleted file mode 100644 index 6cdf0342..00000000 --- a/src/Aop/Pointcut/CFlowBelowMethodPointcut.php +++ /dev/null @@ -1,91 +0,0 @@ - - * - * This source file is subject to the license that is bundled - * with this source code in the file LICENSE. - */ - -namespace Go\Aop\Pointcut; - -use Go\Aop\Pointcut; -use Go\Aop\PointFilter; -use InvalidArgumentException; -use ReflectionClass; - -/** - * Flow pointcut is a dynamic checker that verifies stack trace to understand is it matches or not - */ -class CFlowBelowMethodPointcut implements PointFilter, Pointcut -{ - use PointcutClassFilterTrait; - - /** - * Filter for the class - */ - protected PointFilter $internalClassFilter; - - /** - * Filter for the points - */ - protected Pointcut $internalPointFilter; - - /** - * Control flow below constructor - * - * @throws InvalidArgumentException if filter doesn't support methods - */ - public function __construct(Pointcut $pointcut) - { - $this->internalClassFilter = $pointcut->getClassFilter(); - $this->internalPointFilter = $pointcut; - if (($this->internalPointFilter->getKind() & PointFilter::KIND_METHOD) === 0) { - throw new InvalidArgumentException('Only method filters are valid for control flow'); - } - } - - /** - * 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 - { - // With single parameter (statically) always matches - if ($instance === null) { - return true; - } - - $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); - foreach ($trace as $stackFrame) { - if (!isset($stackFrame['class'])) { - continue; - } - $refClass = new ReflectionClass($stackFrame['class']); - if (!$this->internalClassFilter->matches($refClass)) { - continue; - } - $refMethod = $refClass->getMethod($stackFrame['function']); - if ($this->internalPointFilter->matches($refMethod)) { - return true; - } - } - - return false; - } - - /** - * Returns the kind of point filter - */ - public function getKind(): int - { - return PointFilter::KIND_METHOD | PointFilter::KIND_DYNAMIC; - } -} diff --git a/src/Aop/Pointcut/ClassInheritancePointcut.php b/src/Aop/Pointcut/ClassInheritancePointcut.php new file mode 100644 index 00000000..df2b1644 --- /dev/null +++ b/src/Aop/Pointcut/ClassInheritancePointcut.php @@ -0,0 +1,53 @@ + + * + * This source file is subject to the license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Go\Aop\Pointcut; + +use Go\Aop\Pointcut; +use Go\ParserReflection\ReflectionFileNamespace; +use ReflectionClass; +use ReflectionFunction; +use ReflectionMethod; +use ReflectionProperty; +use function in_array; + +/** + * Inheritance pointcut that matches any child for given parent or implements given interface + */ +final readonly class ClassInheritancePointcut implements Pointcut +{ + /** + * Inheritance class matcher constructor + * @param (string&class-string) $parentClassOrInterfaceName Parent class or interface name to match in hierarchy + */ + public function __construct(private string $parentClassOrInterfaceName) {} + + public function matches( + ReflectionClass|ReflectionFileNamespace $context, + ReflectionMethod|ReflectionProperty|ReflectionFunction $reflector = null, + object|string $instanceOrScope = null, + array $arguments = null + ): bool { + // We match only with ReflectionClass as a context + if (!$context instanceof ReflectionClass) { + return false; + } + + // Otherwise, we match only if given context is child of given previously class name (either interface or class) + return $context->isSubclassOf($this->parentClassOrInterfaceName) || in_array($this->parentClassOrInterfaceName, $context->getInterfaceNames()); + } + + public function getKind(): int + { + return self::KIND_CLASS; + } +} diff --git a/src/Aop/Pointcut/ClassMemberReference.php b/src/Aop/Pointcut/ClassMemberReference.php index e00dde01..e18afbbd 100644 --- a/src/Aop/Pointcut/ClassMemberReference.php +++ b/src/Aop/Pointcut/ClassMemberReference.php @@ -12,26 +12,25 @@ namespace Go\Aop\Pointcut; -use Go\Aop\PointFilter; -use Go\Aop\Support\ModifierMatcherFilter; +use Go\Aop\Pointcut; /** * Readonly data transfer object for storing a reference to the class member (property or method) */ -readonly class ClassMemberReference +final readonly class ClassMemberReference { /** * Default constructor * - * @param PointFilter $classFilter Filter for class names - * @param ModifierMatcherFilter $visibilityFilter Member visibility filter (public/protected/etc) - * @param ModifierMatcherFilter $accessTypeFilter Filter for access type (statically "::" or dynamically "->") - * @param string $memberNamePattern Expression for the name + * @param Pointcut $classFilter Filter for class names + * @param ModifierPointcut $visibilityFilter Member visibility filter (public/protected/etc) + * @param ModifierPointcut $accessTypeFilter Filter for access type (statically "::" or dynamically "->") + * @param string $memberNamePattern Expression for the name */ public function __construct( - public PointFilter $classFilter, - public ModifierMatcherFilter $visibilityFilter, - public ModifierMatcherFilter $accessTypeFilter, - public string $memberNamePattern + public Pointcut $classFilter, + public ModifierPointcut $visibilityFilter, + public ModifierPointcut $accessTypeFilter, + public string $memberNamePattern ) {} } diff --git a/src/Aop/Pointcut/FunctionPointcut.php b/src/Aop/Pointcut/FunctionPointcut.php deleted file mode 100644 index ed4d7f0d..00000000 --- a/src/Aop/Pointcut/FunctionPointcut.php +++ /dev/null @@ -1,101 +0,0 @@ - - * - * This source file is subject to the license that is bundled - * with this source code in the file LICENSE. - */ - -namespace Go\Aop\Pointcut; - -use Go\Aop\Pointcut; -use Go\Aop\PointFilter; -use ReflectionFunction; - -/** - * Function pointcut checks function signature (namespace and name) to match it - */ -class FunctionPointcut implements Pointcut -{ - protected ?PointFilter $nsFilter = null; - - /** - * Function name to match, can contain wildcards *,? - */ - protected string $functionName = ''; - - /** - * Regular expression for matching - */ - protected string $regexp; - - /** - * Additional return type filter (if present) - */ - protected ?PointFilter $returnTypeFilter = null; - - /** - * Function matcher constructor - */ - public function __construct(string $functionName, PointFilter $returnTypeFilter = null) - { - $this->functionName = $functionName; - $this->returnTypeFilter = $returnTypeFilter; - $this->regexp = strtr( - preg_quote($this->functionName, '/'), - [ - '\\*' => '.*?', - '\\?' => '.' - ] - ); - } - - /** - * Performs matching of point of code - * - * @param mixed $function Specific part of code, can be any Reflection class - * @param 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($function, $context = null, $instance = null, array $arguments = null): bool - { - if (!$function instanceof ReflectionFunction) { - return false; - } - - if (($this->returnTypeFilter !== null) && !$this->returnTypeFilter->matches($function, $context)) { - return false; - } - - return ($function->name === $this->functionName) || (bool)preg_match("/^{$this->regexp}$/", $function->name); - } - - /** - * Returns the kind of point filter - */ - public function getKind(): int - { - return self::KIND_FUNCTION; - } - - /** - * Return the class filter for this pointcut. - */ - public function getClassFilter(): PointFilter - { - return $this->nsFilter; - } - - /** - * Configures the namespace filter, used as pre-filter for functions - */ - public function setNamespaceFilter(PointFilter $nsFilter): void - { - $this->nsFilter = $nsFilter; - } -} diff --git a/src/Aop/Pointcut/MagicMethodDynamicPointcut.php b/src/Aop/Pointcut/MagicMethodDynamicPointcut.php new file mode 100644 index 00000000..268ceb53 --- /dev/null +++ b/src/Aop/Pointcut/MagicMethodDynamicPointcut.php @@ -0,0 +1,93 @@ + + * + * This source file is subject to the license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Go\Aop\Pointcut; + +use Go\Aop\Pointcut; +use Go\ParserReflection\ReflectionFileNamespace; +use ReflectionClass; +use ReflectionFunction; +use ReflectionMethod; +use ReflectionProperty; + +/** + * Magic method pointcut is a dynamic checker that verifies calls for __call and __callStatic + * + * With one (or two) arguments it always statically matches with __call and __callStatic methods. + * With four arguments, it takes real argument for invocation and matches it again dynamically. + */ +final readonly class MagicMethodDynamicPointcut implements Pointcut +{ + /** + * Compiled regular expression for matching + */ + private string $regexp; + + /** + * Magic method matcher constructor + * + * @param string $methodName Method name to match, can contain wildcards "*","?" or "|" + */ + public function __construct(private string $methodName) { + $this->regexp = '/^(' . strtr( + preg_quote($this->methodName, '/'), + [ + '\\*' => '.*?', + '\\?' => '.', + '\\|' => '|' + ] + ) . ')$/'; + } + + public function matches( + ReflectionClass|ReflectionFileNamespace $context, + ReflectionMethod|ReflectionProperty|ReflectionFunction $reflector = null, + object|string $instanceOrScope = null, + array $arguments = null + ): bool { + // Magic methods can be only inside class context + if (!$context instanceof ReflectionClass) { + return false; + } + + // For pre-filter we match only with context that has magic methods + if (!isset($reflector)) { + return $context->hasMethod('__call') || $context->hasMethod('__callStatic'); + } + + // If we receive something not expected here (ReflectionMethod), we should not match + if (!$reflector instanceof ReflectionMethod) { + return false; + } + + // With single parameter (statically) always matches for __call, __callStatic methods + if ($instanceOrScope === null) { + return ($reflector->name === '__call' || $reflector->name === '__callStatic'); + } + + // If for some reason we don't have arguments, or first argument is not a string with valid function name + if (!isset($arguments) || count($arguments) < 1 || !is_string($arguments[0])) { + return false; + } + + // for __call and __callStatic method name is the first argument on invocation + [$methodName] = $arguments; + + // Perform final dynamic check + return ($methodName === $this->methodName) || preg_match($this->regexp, $methodName); + } + + public function getKind(): int + { + return Pointcut::KIND_METHOD | Pointcut::KIND_DYNAMIC; + } +} diff --git a/src/Aop/Pointcut/MagicMethodPointcut.php b/src/Aop/Pointcut/MagicMethodPointcut.php deleted file mode 100644 index a307b01c..00000000 --- a/src/Aop/Pointcut/MagicMethodPointcut.php +++ /dev/null @@ -1,91 +0,0 @@ - - * - * This source file is subject to the license that is bundled - * with this source code in the file LICENSE. - */ - -namespace Go\Aop\Pointcut; - -use Go\Aop\Pointcut; -use Go\Aop\PointFilter; - -/** - * Magic method pointcut is a dynamic checker that verifies calls for __call and __callStatic - */ -class MagicMethodPointcut implements PointFilter, Pointcut -{ - use PointcutClassFilterTrait; - - /** - * Method name to match, can contain wildcards *,? - */ - protected string $methodName = ''; - - /** - * Regular expression for matching - */ - protected string $regexp; - - /** - * Modifier filter for method - */ - protected PointFilter $modifierFilter; - - /** - * Magic method matcher constructor - * - * NB: only public methods can be matched as __call and __callStatic are public - */ - public function __construct(string $methodName, PointFilter $modifierFilter) - { - $this->methodName = $methodName; - $this->regexp = strtr( - preg_quote($this->methodName, '/'), - [ - '\\*' => '.*?', - '\\?' => '.', - '\\|' => '|' - ] - ); - $this->modifierFilter = $modifierFilter; - } - - /** - * 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 - { - // With single parameter (statically) always matches for __call, __callStatic - if ($instance === null) { - return ($point->name === '__call' || $point->name === '__callStatic'); - } - - if (!$this->modifierFilter->matches($point)) { - return false; - } - - // for __call and __callStatic method name is the first argument on invocation - [$methodName] = $arguments; - - return ($methodName === $this->methodName) || (bool)preg_match("/^(?:{$this->regexp})$/", $methodName); - } - - /** - * Returns the kind of point filter - */ - public function getKind(): int - { - return PointFilter::KIND_METHOD | PointFilter::KIND_DYNAMIC; - } -} diff --git a/src/Aop/Pointcut/MatchInheritedPointcut.php b/src/Aop/Pointcut/MatchInheritedPointcut.php index 9688eae5..5d0adc07 100644 --- a/src/Aop/Pointcut/MatchInheritedPointcut.php +++ b/src/Aop/Pointcut/MatchInheritedPointcut.php @@ -13,48 +13,47 @@ namespace Go\Aop\Pointcut; use Go\Aop\Pointcut; -use Go\Aop\PointFilter; +use Go\ParserReflection\ReflectionFileNamespace; use ReflectionClass; +use ReflectionFunction; use ReflectionMethod; use ReflectionProperty; /** - * Pointcut that matches all inherited items, this is useful to filter inherited memebers via !matchInherited() + * Pointcut that matches only inherited items, this is useful to filter inherited members via !matchInherited() + * + * As it is used only inside class context for methods and properties, it rejects all other type of points */ -class MatchInheritedPointcut implements Pointcut +final class MatchInheritedPointcut implements Pointcut { - use PointcutClassFilterTrait; - - /** - * 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 - { + public function matches( + ReflectionClass|ReflectionFileNamespace $context, + ReflectionMethod|ReflectionProperty|ReflectionFunction $reflector = null, + object|string $instanceOrScope = null, + array $arguments = null + ): bool { + // Inherited items can be only inside class context if (!$context instanceof ReflectionClass) { return false; } - $isPointMethod = $point instanceof ReflectionMethod; - $isPointProperty = $point instanceof ReflectionProperty; - if (!$isPointMethod && !$isPointProperty) { + // With only one context given, we should always match, as we need more info about nested items + if (!isset($reflector)) { + return true; + } + + // Inherited items can be only methods and properties and not ReflectionFunction for example + if (!$reflector instanceof ReflectionMethod && !$reflector instanceof ReflectionProperty) { return false; } - $declaringClassName = $point->getDeclaringClass()->name; + $declaringClassName = $reflector->getDeclaringClass()->name; return $context->name !== $declaringClassName && $context->isSubclassOf($declaringClassName); } - /** - * Returns the kind of point filter - */ public function getKind(): int { - return PointFilter::KIND_METHOD | PointFilter::KIND_PROPERTY; + return Pointcut::KIND_METHOD | Pointcut::KIND_PROPERTY; } } diff --git a/src/Aop/Support/ModifierMatcherFilter.php b/src/Aop/Pointcut/ModifierPointcut.php similarity index 53% rename from src/Aop/Support/ModifierMatcherFilter.php rename to src/Aop/Pointcut/ModifierPointcut.php index 87b1d18a..ee71b0e9 100644 --- a/src/Aop/Support/ModifierMatcherFilter.php +++ b/src/Aop/Pointcut/ModifierPointcut.php @@ -10,29 +10,34 @@ * with this source code in the file LICENSE. */ -namespace Go\Aop\Support; +namespace Go\Aop\Pointcut; -use Go\Aop\PointFilter; +use Go\Aop\Pointcut; +use Go\ParserReflection\ReflectionFileNamespace; +use ReflectionClass; +use ReflectionFunction; +use ReflectionMethod; +use ReflectionProperty; /** - * ModifierMatcherFilter performs checks on modifiers for reflection point + * ModifierPointcut performs matching on modifiers for reflector */ -class ModifierMatcherFilter implements PointFilter +final class ModifierPointcut implements Pointcut { /** * Bit mask, that should be always match */ - protected int $andMask = 0; + private int $andMask; /** * Bit mask, that can be used for additional check */ - protected int $orMask = 0; + private int $orMask = 0; /** * Bit mask to exclude specific value from matching, for example, !public */ - protected int $notMask = 0; + private int $notMask = 0; /** * Initialize default filter with "and" mask @@ -45,16 +50,25 @@ public function __construct(int $initialMask = 0) } /** - * 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 + * @return ($reflector is null ? true : bool) */ - public function matches($point, $context = null, $instance = null, array $arguments = null): bool - { - $modifiers = $point->getModifiers(); + public function matches( + ReflectionClass|ReflectionFileNamespace $context, + ReflectionMethod|ReflectionProperty|ReflectionFunction $reflector = null, + object|string $instanceOrScope = null, + array $arguments = null + ): bool { + // With context only we always match, as we don't know about modifiers of given reflector + if (!isset($reflector)) { + return true; + } + + // Only ReflectionFunction doesn't have getModifiers method + if ($reflector instanceof ReflectionFunction) { + $modifiers = 0; + } else { + $modifiers = $reflector->getModifiers(); + } return !($this->notMask & $modifiers) && (($this->andMask === ($this->andMask & $modifiers)) || ($this->orMask & $modifiers)); @@ -90,11 +104,8 @@ public function notMatch(int $bitMask): self return $this; } - /** - * Returns the kind of point filter - */ public function getKind(): int { - return self::KIND_ALL; + return Pointcut::KIND_ALL; } } diff --git a/src/Aop/Pointcut/NamePointcut.php b/src/Aop/Pointcut/NamePointcut.php new file mode 100644 index 00000000..1f89d4ac --- /dev/null +++ b/src/Aop/Pointcut/NamePointcut.php @@ -0,0 +1,79 @@ + + * + * This source file is subject to the license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Go\Aop\Pointcut; + +use Go\Aop\Pointcut; +use Go\ParserReflection\ReflectionFileNamespace; +use ReflectionClass; +use ReflectionFunction; +use ReflectionMethod; +use ReflectionProperty; + +/** + * General name pointcut checks element name to match it + */ +final readonly class NamePointcut implements Pointcut +{ + /** + * Regular expression for pattern matching + */ + private string $regexp; + + /** + * Name matcher constructor + * + * @param string $name Element name to match, can contain wildcards **,*,?,| + * @param bool $useContextForMatching Switch to matching context name instead of reflector + */ + public function __construct( + private int $pointcutKind, + private string $name, + private bool $useContextForMatching = false, + ) { + // Special parenthesis is needed for stricter matching, see https://github.com/goaop/framework/issues/115 + $this->regexp = '/^(' . strtr( + preg_quote($this->name, '/'), + [ + '\\*' => '[^\\\\]+?', + '\\*\\*' => '.+?', + '\\?' => '.', + '\\|' => '|' + ] + ) . ')$/'; + } + + public function matches( + ReflectionClass|ReflectionFileNamespace $context, + ReflectionMethod|ReflectionProperty|ReflectionFunction $reflector = null, + object|string $instanceOrScope = null, + array $arguments = null + ): bool { + // Let's determine what will be used for matching - context or reflector + if ($this->useContextForMatching) { + $instanceToMatch = $context; + } elseif (!isset($reflector)) { + // Without context matching flag we should always match to get an instance of reflector + return true; + } else { + $instanceToMatch = $reflector; + } + + // Perform static check to ensure that we match our name statically + return ($instanceToMatch->getName() === $this->name) || preg_match($this->regexp, $instanceToMatch->getName()); + } + + public function getKind(): int + { + return $this->pointcutKind; + } +} diff --git a/src/Aop/Pointcut/NotPointcut.php b/src/Aop/Pointcut/NotPointcut.php index 8b8b2f4f..d217e90a 100644 --- a/src/Aop/Pointcut/NotPointcut.php +++ b/src/Aop/Pointcut/NotPointcut.php @@ -1,10 +1,10 @@ + * @copyright Copyright 2014, Lisachenko Alexander * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. @@ -13,60 +13,42 @@ namespace Go\Aop\Pointcut; use Go\Aop\Pointcut; +use Go\ParserReflection\ReflectionFileNamespace; +use ReflectionClass; +use ReflectionFunction; +use ReflectionMethod; +use ReflectionProperty; /** - * Signature method pointcut checks method signature (modifiers and name) to match it + * Logical "not" pointcut filter. */ -class NotPointcut implements Pointcut +final readonly class NotPointcut implements Pointcut { - use PointcutClassFilterTrait; - - /** - * Pointcut to invert - */ - protected Pointcut $pointcut; - - /** - * Kind of pointcut - */ - protected int $kind = 0; - /** - * Inverse pointcut matcher + * Not constructor */ - public function __construct(Pointcut $pointcut) - { - $this->pointcut = $pointcut; - $this->kind = $pointcut->getKind(); - } + public function __construct(private Pointcut $pointcut) {} /** - * 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 + * @return ($reflector is null ? true : bool) */ - public function matches($point, $context = null, $instance = null, array $arguments = null): bool - { - $isMatchesPre = $this->pointcut->getClassFilter()->matches($context); - if (!$isMatchesPre) { - return true; - } - $isMatchesPoint = $this->pointcut->matches($point, $context, $instance, $arguments); - if (!$isMatchesPoint) { + public function matches( + ReflectionClass|ReflectionFileNamespace $context, + ReflectionMethod|ReflectionProperty|ReflectionFunction $reflector = null, + object|string $instanceOrScope = null, + array $arguments = null + ): bool { + // For Logical "not" expression without reflector, we should match statically for any context + if (!isset($reflector)) { return true; } - return false; + // Otherwise we return inverted result from static/dynamic matching + return !$this->pointcut->matches($context, $reflector, $instanceOrScope, $arguments); } - /** - * Returns the kind of point filter - */ public function getKind(): int { - return $this->kind; + return $this->pointcut->getKind(); } } diff --git a/src/Aop/Pointcut/OrPointcut.php b/src/Aop/Pointcut/OrPointcut.php index 495ad7d8..c705d80e 100644 --- a/src/Aop/Pointcut/OrPointcut.php +++ b/src/Aop/Pointcut/OrPointcut.php @@ -1,6 +1,6 @@ first = $first; - $this->second = $second; - $this->kind = $first->getKind() | $second->getKind(); - - $this->classFilter = new OrPointFilter($first->getClassFilter(), $second->getClassFilter()); - } + private int $pointcutKind; /** - * Performs matching of point of code + * List of Pointcut to combine * - * @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 + * @var array */ - 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); - } + private array $pointcuts; /** - * @inheritDoc + * Or constructor */ - protected function matchPart( - Pointcut $pointcut, - $point, - $context = null, - $instance = null, - array $arguments = null - ): bool { - $pointcutKind = $pointcut->getKind(); - // We need to recheck filter kind one more time, because of OR syntax - switch (true) { - case ($point instanceof ReflectionMethod && ($pointcutKind & PointFilter::KIND_METHOD)): - case ($point instanceof ReflectionProperty && ($pointcutKind & PointFilter::KIND_PROPERTY)): - case ($point instanceof ReflectionClass && ($pointcutKind & PointFilter::KIND_CLASS)): - return parent::matchPart($pointcut, $point, $context, $instance, $arguments); + public function __construct(Pointcut ...$pointcuts) + { + $pointcutKind = 0; + foreach ($pointcuts as $singlePointcut) { + $pointcutKind |= $singlePointcut->getKind(); + } + $this->pointcutKind = $pointcutKind; + $this->pointcuts = $pointcuts; + } - default: - return false; + 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 true; + } } + + return false; + } + + public function getKind(): int + { + return $this->pointcutKind; } } diff --git a/src/Aop/Pointcut/PointcutClassFilterTrait.php b/src/Aop/Pointcut/PointcutClassFilterTrait.php deleted file mode 100644 index aa6deb7d..00000000 --- a/src/Aop/Pointcut/PointcutClassFilterTrait.php +++ /dev/null @@ -1,49 +0,0 @@ - - * - * This source file is subject to the license that is bundled - * with this source code in the file LICENSE. - */ - -namespace Go\Aop\Pointcut; - -use Go\Aop\PointFilter; -use Go\Aop\Support\TruePointFilter; - -/** - * Convenient trait for pointcuts with class filter. - * - * The "classFilter" property can be set to customize ClassFilter behavior. - */ -trait PointcutClassFilterTrait -{ - /** - * Filter for class - */ - protected ?PointFilter $classFilter = null; - - /** - * Set the ClassFilter to use for this pointcut. - */ - public function setClassFilter(PointFilter $classFilter): void - { - $this->classFilter = $classFilter; - } - - /** - * Return the class filter for this pointcut. - */ - public function getClassFilter(): PointFilter - { - if ($this->classFilter === null) { - $this->classFilter = TruePointFilter::getInstance(); - } - - return $this->classFilter; - } -} diff --git a/src/Aop/Pointcut/PointcutGrammar.php b/src/Aop/Pointcut/PointcutGrammar.php index e3531843..a65b2894 100644 --- a/src/Aop/Pointcut/PointcutGrammar.php +++ b/src/Aop/Pointcut/PointcutGrammar.php @@ -15,22 +15,15 @@ use Closure; use Dissect\Lexer\Token; use Dissect\Parser\Grammar; -use Go\Aop\PointFilter; -use Go\Aop\Support\AndPointFilter; -use Go\Aop\Support\InheritanceClassFilter; -use Go\Aop\Support\ModifierMatcherFilter; -use Go\Aop\Support\ReturnTypeFilter; -use Go\Aop\Support\SimpleNamespaceFilter; -use Go\Aop\Support\TruePointFilter; +use Go\Aop\Pointcut; use Go\Core\AspectContainer; use ReflectionMethod; - use function constant; /** * Pointcut grammar defines general structure of pointcuts and rules of parsing */ -class PointcutGrammar extends Grammar +final class PointcutGrammar extends Grammar { /** * Constructs a pointcut grammar with AST @@ -50,7 +43,7 @@ public function __construct(AspectContainer $container) $this('conjugatedExpression') ->is('conjugatedExpression', '&&', 'negatedExpression') - ->call(fn($first, $_0, $second) => new AndPointcut($first, $second)) + ->call(fn($first, $_0, $second) => new AndPointcut(null, $first, $second)) ->is('negatedExpression') ; @@ -75,7 +68,6 @@ public function __construct(AspectContainer $container) ->is('annotatedWithinPointcut') ->is('initializationPointcut') ->is('staticInitializationPointcut') - ->is('cflowbelowPointcut') ->is('dynamicExecutionPointcut') ->is('matchInheritedPointcut') ->is('pointcutReference') @@ -97,10 +89,10 @@ public function __construct(AspectContainer $container) ->is('within', '(', 'classFilter', ')') ->call( function ($_0, $_1, $classFilter) { - $pointcut = new TruePointcut(PointFilter::KIND_ALL); - $pointcut->setClassFilter($classFilter); - - return $pointcut; + return new AndPointcut( + Pointcut::KIND_ALL, + $classFilter + ); } ) ; @@ -109,9 +101,7 @@ function ($_0, $_1, $classFilter) { ->is('annotation', 'access', '(', 'namespaceName', ')') ->call( function ($_0, $_1, $_2, $attributeClassName) { - $kindProperty = PointFilter::KIND_PROPERTY; - - return new AttributePointcut($kindProperty, $attributeClassName); + return new AttributePointcut(Pointcut::KIND_PROPERTY, $attributeClassName); } ) ; @@ -120,9 +110,7 @@ function ($_0, $_1, $_2, $attributeClassName) { ->is('annotation', 'execution', '(', 'namespaceName', ')') ->call( function ($_0, $_1, $_2, $attributeClassName) { - $kindMethod = PointFilter::KIND_METHOD; - - return new AttributePointcut($kindMethod, $attributeClassName); + return new AttributePointcut(Pointcut::KIND_METHOD, $attributeClassName); } ) ; @@ -131,12 +119,7 @@ function ($_0, $_1, $_2, $attributeClassName) { ->is('annotation', 'within', '(', 'namespaceName', ')') ->call( function ($_0, $_1, $_2, $attributeClassName) { - $pointcut = new TruePointcut(PointFilter::KIND_ALL); - $kindClass = PointFilter::KIND_CLASS; - $classFilter = new AttributePointcut($kindClass, $attributeClassName); - $pointcut->setClassFilter($classFilter); - - return $pointcut; + return new AttributePointcut(Pointcut::KIND_ALL, $attributeClassName, true); } ) ; @@ -145,10 +128,10 @@ function ($_0, $_1, $_2, $attributeClassName) { ->is('initialization', '(', 'classFilter', ')') ->call( function ($_0, $_1, $classFilter) { - $pointcut = new TruePointcut(PointFilter::KIND_INIT + PointFilter::KIND_CLASS); - $pointcut->setClassFilter($classFilter); - - return $pointcut; + return new AndPointcut( + Pointcut::KIND_INIT | Pointcut::KIND_CLASS, + $classFilter + ); } ) ; @@ -157,19 +140,14 @@ function ($_0, $_1, $classFilter) { ->is('staticinitialization', '(', 'classFilter', ')') ->call( function ($_0, $_1, $classFilter) { - $pointcut = new TruePointcut(PointFilter::KIND_STATIC_INIT + PointFilter::KIND_CLASS); - $pointcut->setClassFilter($classFilter); - - return $pointcut; + return new AndPointcut( + Pointcut::KIND_STATIC_INIT | Pointcut::KIND_CLASS, + $classFilter + ); } ) ; - $this('cflowbelowPointcut') - ->is('cflowbelow', '(', 'executionPointcut', ')') - ->call(fn($_0, $_1, $pointcut) => new CFlowBelowMethodPointcut($pointcut)) - ; - $this('matchInheritedPointcut') ->is('matchInherited', '(', ')') ->call(fn() => new MatchInheritedPointcut()) @@ -180,14 +158,14 @@ function ($_0, $_1, $classFilter) { ->is('dynamic', '(', 'memberReference', '(', 'argumentList', ')', ')') ->call( function ($_0, $_1, ClassMemberReference $reference) { - $memberFilter = new AndPointFilter( + $pointcut = new AndPointcut( + Pointcut::KIND_METHOD | Pointcut::KIND_DYNAMIC, + $reference->classFilter, $reference->visibilityFilter, - $reference->accessTypeFilter + $reference->accessTypeFilter, + new MagicMethodDynamicPointcut($reference->memberNamePattern) ); - $pointcut = new MagicMethodPointcut($reference->memberNamePattern, $memberFilter); - $pointcut->setClassFilter($reference->classFilter); - return $pointcut; } ) @@ -202,20 +180,13 @@ function ($_0, $_1, ClassMemberReference $reference) { ->is('memberReference') ->call( function (ClassMemberReference $reference) { - $memberFilter = new AndPointFilter( + return new AndPointcut( + Pointcut::KIND_PROPERTY, + $reference->classFilter, $reference->visibilityFilter, - $reference->accessTypeFilter - ); - - $pointcut = new SignaturePointcut( - PointFilter::KIND_PROPERTY, - $reference->memberNamePattern, - $memberFilter + $reference->accessTypeFilter, + new NamePointcut(Pointcut::KIND_PROPERTY, $reference->memberNamePattern) ); - - $pointcut->setClassFilter($reference->classFilter); - - return $pointcut; } ) ; @@ -224,40 +195,26 @@ function (ClassMemberReference $reference) { ->is('memberReference', '(', 'argumentList', ')') ->call( function (ClassMemberReference $reference) { - $memberFilter = new AndPointFilter( + return new AndPointcut( + Pointcut::KIND_METHOD, + $reference->classFilter, $reference->visibilityFilter, - $reference->accessTypeFilter - ); - - $pointcut = new SignaturePointcut( - PointFilter::KIND_METHOD, - $reference->memberNamePattern, - $memberFilter + $reference->accessTypeFilter, + new NamePointcut(Pointcut::KIND_METHOD, $reference->memberNamePattern) ); - - $pointcut->setClassFilter($reference->classFilter); - - return $pointcut; } ) ->is('memberReference', '(', 'argumentList', ')', ':', 'namespaceName') ->call( function (ClassMemberReference $reference, $_0, $_1, $_2, $_3, $returnType) { - $memberFilter = new AndPointFilter( + return new AndPointcut( + Pointcut::KIND_METHOD, + $reference->classFilter, $reference->visibilityFilter, $reference->accessTypeFilter, - new ReturnTypeFilter($returnType) + new NamePointcut(Pointcut::KIND_METHOD, $reference->memberNamePattern), + new ReturnTypePointcut($returnType), ); - - $pointcut = new SignaturePointcut( - PointFilter::KIND_METHOD, - $reference->memberNamePattern, - $memberFilter - ); - - $pointcut->setClassFilter($reference->classFilter); - - return $pointcut; } ) ; @@ -266,22 +223,22 @@ function (ClassMemberReference $reference, $_0, $_1, $_2, $_3, $returnType) { ->is('namespacePattern', 'nsSeparator', 'namePatternPart', '(', 'argumentList', ')') ->call( function ($namespacePattern, $_0, $namePattern) { - $nsFilter = new SimpleNamespaceFilter($namespacePattern); - $pointcut = new FunctionPointcut($namePattern); - $pointcut->setNamespaceFilter($nsFilter); - - return $pointcut; + return new AndPointcut( + Pointcut::KIND_FUNCTION, + new NamePointcut(Pointcut::KIND_FUNCTION, $namespacePattern, true), + new NamePointcut(Pointcut::KIND_FUNCTION, $namePattern), + ); } ) ->is('namespacePattern', 'nsSeparator', 'namePatternPart', '(', 'argumentList', ')', ':', 'namespaceName') ->call( function ($namespacePattern, $_0, $namePattern, $_1, $_2, $_3, $_4, $returnType) { - $nsFilter = new SimpleNamespaceFilter($namespacePattern); - $typeFilter = new ReturnTypeFilter($returnType); - $pointcut = new FunctionPointcut($namePattern, $typeFilter); - $pointcut->setNamespaceFilter($nsFilter); - - return $pointcut; + return new AndPointcut( + Pointcut::KIND_FUNCTION, + new NamePointcut(Pointcut::KIND_FUNCTION, $namespacePattern, true), + new ReturnTypePointcut($returnType), + new NamePointcut(Pointcut::KIND_FUNCTION, $namePattern), + ); } ) ; @@ -290,19 +247,17 @@ function ($namespacePattern, $_0, $namePattern, $_1, $_2, $_3, $_4, $returnType) ->is('memberModifiers', 'classFilter', 'memberAccessType', 'namePatternPart') ->call( function ( - ModifierMatcherFilter $memberModifiers, - PointFilter $classFilter, - ModifierMatcherFilter $memberAccessType, - $namePattern + ModifierPointcut $memberModifiers, + Pointcut $classFilter, + ModifierPointcut $memberAccessType, + string $namePattern ) { - $reference = new ClassMemberReference( + return new ClassMemberReference( $classFilter, $memberModifiers, $memberAccessType, $namePattern ); - - return $reference; } ) ; @@ -311,15 +266,14 @@ function ( ->is('namespacePattern') ->call( function ($pattern) { - $truePointFilter = TruePointFilter::getInstance(); return $pattern === '**' - ? $truePointFilter - : new SignaturePointcut(PointFilter::KIND_CLASS, $pattern, $truePointFilter); + ? new TruePointcut() + : new NamePointcut(Pointcut::KIND_ALL, $pattern, true); } ) ->is('namespacePattern', '+') - ->call(fn($parentClassName) => new InheritanceClassFilter($parentClassName)) + ->call(fn($parentClassName) => new ClassInheritancePointcut($parentClassName)) ; $this('argumentList') @@ -327,11 +281,11 @@ function ($pattern) { $this('memberAccessType') ->is('::') - ->call(fn() => new ModifierMatcherFilter(ReflectionMethod::IS_STATIC)) + ->call(fn() => new ModifierPointcut(ReflectionMethod::IS_STATIC)) ->is('->') ->call( function () { - $modifierMatcherFilter = new ModifierMatcherFilter(); + $modifierMatcherFilter = new ModifierPointcut(); $modifierMatcherFilter->notMatch(ReflectionMethod::IS_STATIC); return $modifierMatcherFilter; @@ -371,11 +325,11 @@ function () { $this('memberModifiers') ->is('memberModifier', '|', 'memberModifiers') - ->call(fn($modifier, $_0, ModifierMatcherFilter $matcher) => $matcher->orMatch($modifier)) + ->call(fn($modifier, $_0, ModifierPointcut $matcher) => $matcher->orMatch($modifier)) ->is('memberModifier', 'memberModifiers') - ->call(fn($modifier, ModifierMatcherFilter $matcher) => $matcher->andMatch($modifier)) + ->call(fn($modifier, ModifierPointcut $matcher) => $matcher->andMatch($modifier)) ->is('memberModifier') - ->call(fn($modifier) => new ModifierMatcherFilter($modifier)) + ->call(fn($modifier) => new ModifierPointcut($modifier)) ; $converter = $this->getModifierConverter(); @@ -418,7 +372,11 @@ private function getNodeToStringConverter(): callable private function getModifierConverter(): Closure { return function (Token $token) { - $name = strtoupper($token->getValue()); + $value = $token->getValue(); + if (!is_string($value)) { + throw new \InvalidArgumentException('Token value must be a string'); + } + $name = strtoupper($value); return constant("ReflectionMethod::IS_{$name}"); }; diff --git a/src/Aop/Pointcut/PointcutLexer.php b/src/Aop/Pointcut/PointcutLexer.php index 9c9ebd0d..2293df21 100644 --- a/src/Aop/Pointcut/PointcutLexer.php +++ b/src/Aop/Pointcut/PointcutLexer.php @@ -17,7 +17,7 @@ /** * This class defines a lexer for pointcut expression */ -class PointcutLexer extends SimpleLexer +final class PointcutLexer extends SimpleLexer { /** * Lexer token definitions @@ -29,7 +29,6 @@ public function __construct() $this->token('dynamic'); $this->token('within'); $this->token('access'); - $this->token('cflowbelow'); $this->token('initialization'); $this->token('staticinitialization'); $this->token('matchInherited'); diff --git a/src/Aop/Pointcut/PointcutParseTable.php b/src/Aop/Pointcut/PointcutParseTable.php index 4e294fac..98dab96f 100644 --- a/src/Aop/Pointcut/PointcutParseTable.php +++ b/src/Aop/Pointcut/PointcutParseTable.php @@ -3,7 +3,7 @@ /* * Go! AOP framework * - * @copyright Copyright 2013, Lisachenko Alexander + * @copyright Copyright 2024, Lisachenko Alexander * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. @@ -12,4 +12,4 @@ /** * This table was generated for production use, do not touch it */ -return ['action' => [0 => ['!' =>4, '(' =>6, 'access' =>20, 'annotation' =>21, 'execution' =>22, 'within' =>23, 'initialization' =>24, 'staticinitialization' =>25, 'cflowbelow' =>26, 'dynamic' =>27, 'matchInherited' =>28, 'namePart' =>30,], 1 => ['||' =>31, '$eof' =>0,], 2 => ['&&' =>32, '$eof' =>-3, '||' =>-3, ')' =>-3,], 4 => ['(' =>6, 'access' =>20, 'annotation' =>21, 'execution' =>22, 'within' =>23, 'initialization' =>24, 'staticinitialization' =>25, 'cflowbelow' =>26, 'dynamic' =>27, 'matchInherited' =>28, 'namePart' =>30,], 6 => ['!' =>4, '(' =>6, 'access' =>20, 'annotation' =>21, 'execution' =>22, 'within' =>23, 'initialization' =>24, 'staticinitialization' =>25, 'cflowbelow' =>26, 'dynamic' =>27, 'matchInherited' =>28, 'namePart' =>30,], 20 => ['(' =>35,], 21 => ['access' =>36, 'execution' =>37, 'within' =>38,], 22 => ['(' =>39,], 23 => ['(' =>40,], 24 => ['(' =>41,], 25 => ['(' =>42,], 26 => ['(' =>43,], 27 => ['(' =>44,], 28 => ['(' =>45,], 29 => ['->' =>46, 'nsSeparator' =>47,], 31 => ['!' =>4, '(' =>6, 'access' =>20, 'annotation' =>21, 'execution' =>22, 'within' =>23, 'initialization' =>24, 'staticinitialization' =>25, 'cflowbelow' =>26, 'dynamic' =>27, 'matchInherited' =>28, 'namePart' =>30,], 32 => ['!' =>4, '(' =>6, 'access' =>20, 'annotation' =>21, 'execution' =>22, 'within' =>23, 'initialization' =>24, 'staticinitialization' =>25, 'cflowbelow' =>26, 'dynamic' =>27, 'matchInherited' =>28, 'namePart' =>30,], 34 => [')' =>50, '||' =>31,], 35 => ['public' =>55, 'protected' =>56, 'private' =>57, 'final' =>58,], 36 => ['(' =>59,], 37 => ['(' =>60,], 38 => ['(' =>61,], 39 => ['**' =>66, '*' =>68, 'namePart' =>69, 'public' =>55, 'protected' =>56, 'private' =>57, 'final' =>58,], 40 => ['**' =>66, '*' =>68, 'namePart' =>69,], 41 => ['**' =>66, '*' =>68, 'namePart' =>69,], 42 => ['**' =>66, '*' =>68, 'namePart' =>69,], 43 => ['execution' =>22,], 44 => ['public' =>55, 'protected' =>56, 'private' =>57, 'final' =>58,], 45 => [')' =>76,], 46 => ['*' =>68, 'namePart' =>69,], 47 => ['namePart' =>78,], 48 => ['&&' =>32, '$eof' =>-2, '||' =>-2, ')' =>-2,], 51 => [')' =>79,], 53 => ['**' =>66, '*' =>68, 'namePart' =>69,], 54 => ['|' =>81, 'public' =>55, 'protected' =>56, 'private' =>57, 'final' =>58, '**' =>-59, '*' =>-59, 'namePart' =>-59,], 59 => ['namePart' =>30,], 60 => ['namePart' =>30,], 61 => ['namePart' =>30,], 62 => [')' =>86,], 63 => [')' =>87,], 64 => ['(' =>88,], 65 => ['nsSeparator' =>89,], 67 => ['*' =>90, 'namePart' =>91, '|' =>92, 'nsSeparator' =>-47, ')' =>-47, '+' =>-47, '::' =>-47, '->' =>-47,], 70 => [')' =>93,], 71 => ['+' =>94, 'nsSeparator' =>95, ')' =>-41, '::' =>-41, '->' =>-41,], 72 => [')' =>96,], 73 => [')' =>97,], 74 => [')' =>98,], 75 => ['(' =>99,], 77 => ['*' =>90, 'namePart' =>91, '|' =>92, '$eof' =>-34, '||' =>-34, '&&' =>-34, ')' =>-34,], 80 => ['::' =>101, '->' =>102,], 81 => ['public' =>55, 'protected' =>56, 'private' =>57, 'final' =>58,], 83 => [')' =>104, 'nsSeparator' =>47,], 84 => [')' =>105, 'nsSeparator' =>47,], 85 => [')' =>106, 'nsSeparator' =>47,], 88 => ['*' =>108,], 89 => ['**' =>110, '*' =>68, 'namePart' =>69,], 92 => ['namePart' =>111,], 95 => ['**' =>110, '*' =>68, 'namePart' =>69,], 99 => ['*' =>108,], 100 => ['*' =>68, 'namePart' =>69,], 107 => [')' =>115,], 109 => ['(' =>116, '*' =>90, 'namePart' =>91, '|' =>92, 'nsSeparator' =>-48,], 112 => ['*' =>90, 'namePart' =>91, '|' =>92, ')' =>-48, '+' =>-48, 'nsSeparator' =>-48, '::' =>-48, '->' =>-48,], 113 => [')' =>117,], 114 => ['*' =>90, 'namePart' =>91, '|' =>92, ')' =>-40, '(' =>-40,], 115 => [':' =>118, ')' =>-36,], 116 => ['*' =>108,], 117 => [')' =>120,], 118 => ['namePart' =>30,], 119 => [')' =>122,], 121 => ['nsSeparator' =>47, ')' =>-37,], 122 => [':' =>123, ')' =>-38,], 123 => ['namePart' =>30,], 124 => ['nsSeparator' =>47, ')' =>-39,], 3 => ['$eof' =>-5, '||' =>-5, '&&' =>-5, ')' =>-5,], 5 => ['$eof' =>-7, '||' =>-7, '&&' =>-7, ')' =>-7,], 7 => ['$eof' =>-9, '||' =>-9, '&&' =>-9, ')' =>-9,], 8 => ['$eof' =>-10, '||' =>-10, '&&' =>-10, ')' =>-10,], 9 => ['$eof' =>-11, '||' =>-11, '&&' =>-11, ')' =>-11,], 10 => ['$eof' =>-12, '||' =>-12, '&&' =>-12, ')' =>-12,], 11 => ['$eof' =>-13, '||' =>-13, '&&' =>-13, ')' =>-13,], 12 => ['$eof' =>-14, '||' =>-14, '&&' =>-14, ')' =>-14,], 13 => ['$eof' =>-15, '||' =>-15, '&&' =>-15, ')' =>-15,], 14 => ['$eof' =>-16, '||' =>-16, '&&' =>-16, ')' =>-16,], 15 => ['$eof' =>-17, '||' =>-17, '&&' =>-17, ')' =>-17,], 16 => ['$eof' =>-18, '||' =>-18, '&&' =>-18, ')' =>-18,], 17 => ['$eof' =>-19, '||' =>-19, '&&' =>-19, ')' =>-19,], 18 => ['$eof' =>-20, '||' =>-20, '&&' =>-20, ')' =>-20,], 19 => ['$eof' =>-21, '||' =>-21, '&&' =>-21, ')' =>-21,], 30 => ['->' =>-55, 'nsSeparator' =>-55, ')' =>-55,], 33 => ['$eof' =>-6, '||' =>-6, '&&' =>-6, ')' =>-6,], 49 => ['$eof' =>-4, '||' =>-4, '&&' =>-4, ')' =>-4,], 50 => ['$eof' =>-8, '||' =>-8, '&&' =>-8, ')' =>-8,], 52 => [')' =>-35,], 55 => ['**' =>-60, '*' =>-60, 'namePart' =>-60, '|' =>-60, 'public' =>-60, 'protected' =>-60, 'private' =>-60, 'final' =>-60,], 56 => ['**' =>-61, '*' =>-61, 'namePart' =>-61, '|' =>-61, 'public' =>-61, 'protected' =>-61, 'private' =>-61, 'final' =>-61,], 57 => ['**' =>-62, '*' =>-62, 'namePart' =>-62, '|' =>-62, 'public' =>-62, 'protected' =>-62, 'private' =>-62, 'final' =>-62,], 58 => ['**' =>-63, '*' =>-63, 'namePart' =>-63, '|' =>-63, 'public' =>-63, 'protected' =>-63, 'private' =>-63, 'final' =>-63,], 66 => ['nsSeparator' =>-46, ')' =>-46, '+' =>-46, '::' =>-46, '->' =>-46,], 68 => ['$eof' =>-50, '||' =>-50, '&&' =>-50, ')' =>-50, '(' =>-50, 'nsSeparator' =>-50, '*' =>-50, 'namePart' =>-50, '|' =>-50, '+' =>-50, '::' =>-50, '->' =>-50,], 69 => ['$eof' =>-51, '||' =>-51, '&&' =>-51, ')' =>-51, '(' =>-51, 'nsSeparator' =>-51, '*' =>-51, 'namePart' =>-51, '|' =>-51, '+' =>-51, '::' =>-51, '->' =>-51,], 76 => ['$eof' =>-32, '||' =>-32, '&&' =>-32, ')' =>-32,], 78 => ['->' =>-56, 'nsSeparator' =>-56, ')' =>-56,], 79 => ['$eof' =>-22, '||' =>-22, '&&' =>-22, ')' =>-22,], 82 => ['**' =>-58, '*' =>-58, 'namePart' =>-58,], 86 => ['$eof' =>-23, '||' =>-23, '&&' =>-23, ')' =>-23,], 87 => ['$eof' =>-24, '||' =>-24, '&&' =>-24, ')' =>-24,], 90 => ['$eof' =>-52, '||' =>-52, '&&' =>-52, ')' =>-52, '(' =>-52, 'nsSeparator' =>-52, '*' =>-52, 'namePart' =>-52, '|' =>-52, '+' =>-52, '::' =>-52, '->' =>-52,], 91 => ['$eof' =>-53, '||' =>-53, '&&' =>-53, ')' =>-53, '(' =>-53, 'nsSeparator' =>-53, '*' =>-53, 'namePart' =>-53, '|' =>-53, '+' =>-53, '::' =>-53, '->' =>-53,], 93 => ['$eof' =>-25, '||' =>-25, '&&' =>-25, ')' =>-25,], 94 => [')' =>-42, '::' =>-42, '->' =>-42,], 96 => ['$eof' =>-29, '||' =>-29, '&&' =>-29, ')' =>-29,], 97 => ['$eof' =>-30, '||' =>-30, '&&' =>-30, ')' =>-30,], 98 => ['$eof' =>-31, '||' =>-31, '&&' =>-31, ')' =>-31,], 101 => ['*' =>-44, 'namePart' =>-44,], 102 => ['*' =>-45, 'namePart' =>-45,], 103 => ['**' =>-57, '*' =>-57, 'namePart' =>-57,], 104 => ['$eof' =>-26, '||' =>-26, '&&' =>-26, ')' =>-26,], 105 => ['$eof' =>-27, '||' =>-27, '&&' =>-27, ')' =>-27,], 106 => ['$eof' =>-28, '||' =>-28, '&&' =>-28, ')' =>-28,], 108 => [')' =>-43,], 110 => ['nsSeparator' =>-49, ')' =>-49, '+' =>-49, '::' =>-49, '->' =>-49,], 111 => ['$eof' =>-54, '||' =>-54, '&&' =>-54, ')' =>-54, '(' =>-54, 'nsSeparator' =>-54, '*' =>-54, 'namePart' =>-54, '|' =>-54, '+' =>-54, '::' =>-54, '->' =>-54,], 120 => ['$eof' =>-33, '||' =>-33, '&&' =>-33, ')' =>-33,],], 'goto' => [0 => ['pointcutExpression' =>1, 'conjugatedExpression' =>2, 'negatedExpression' =>3, 'brakedExpression' =>5, 'singlePointcut' =>7, 'accessPointcut' =>8, 'annotatedAccessPointcut' =>9, 'executionPointcut' =>10, 'annotatedExecutionPointcut' =>11, 'withinPointcut' =>12, 'annotatedWithinPointcut' =>13, 'initializationPointcut' =>14, 'staticInitializationPointcut' =>15, 'cflowbelowPointcut' =>16, 'dynamicExecutionPointcut' =>17, 'matchInheritedPointcut' =>18, 'pointcutReference' =>19, 'namespaceName' =>29,], 4 => ['brakedExpression' =>33, 'singlePointcut' =>7, 'accessPointcut' =>8, 'annotatedAccessPointcut' =>9, 'executionPointcut' =>10, 'annotatedExecutionPointcut' =>11, 'withinPointcut' =>12, 'annotatedWithinPointcut' =>13, 'initializationPointcut' =>14, 'staticInitializationPointcut' =>15, 'cflowbelowPointcut' =>16, 'dynamicExecutionPointcut' =>17, 'matchInheritedPointcut' =>18, 'pointcutReference' =>19, 'namespaceName' =>29,], 6 => ['pointcutExpression' =>34, 'conjugatedExpression' =>2, 'negatedExpression' =>3, 'brakedExpression' =>5, 'singlePointcut' =>7, 'accessPointcut' =>8, 'annotatedAccessPointcut' =>9, 'executionPointcut' =>10, 'annotatedExecutionPointcut' =>11, 'withinPointcut' =>12, 'annotatedWithinPointcut' =>13, 'initializationPointcut' =>14, 'staticInitializationPointcut' =>15, 'cflowbelowPointcut' =>16, 'dynamicExecutionPointcut' =>17, 'matchInheritedPointcut' =>18, 'pointcutReference' =>19, 'namespaceName' =>29,], 31 => ['conjugatedExpression' =>48, 'negatedExpression' =>3, 'brakedExpression' =>5, 'singlePointcut' =>7, 'accessPointcut' =>8, 'annotatedAccessPointcut' =>9, 'executionPointcut' =>10, 'annotatedExecutionPointcut' =>11, 'withinPointcut' =>12, 'annotatedWithinPointcut' =>13, 'initializationPointcut' =>14, 'staticInitializationPointcut' =>15, 'cflowbelowPointcut' =>16, 'dynamicExecutionPointcut' =>17, 'matchInheritedPointcut' =>18, 'pointcutReference' =>19, 'namespaceName' =>29,], 32 => ['negatedExpression' =>49, 'brakedExpression' =>5, 'singlePointcut' =>7, 'accessPointcut' =>8, 'annotatedAccessPointcut' =>9, 'executionPointcut' =>10, 'annotatedExecutionPointcut' =>11, 'withinPointcut' =>12, 'annotatedWithinPointcut' =>13, 'initializationPointcut' =>14, 'staticInitializationPointcut' =>15, 'cflowbelowPointcut' =>16, 'dynamicExecutionPointcut' =>17, 'matchInheritedPointcut' =>18, 'pointcutReference' =>19, 'namespaceName' =>29,], 35 => ['propertyAccessReference' =>51, 'memberReference' =>52, 'memberModifiers' =>53, 'memberModifier' =>54,], 39 => ['methodExecutionReference' =>62, 'functionExecutionReference' =>63, 'memberReference' =>64, 'namespacePattern' =>65, 'memberModifiers' =>53, 'namePatternPart' =>67, 'memberModifier' =>54,], 40 => ['classFilter' =>70, 'namespacePattern' =>71, 'namePatternPart' =>67,], 41 => ['classFilter' =>72, 'namespacePattern' =>71, 'namePatternPart' =>67,], 42 => ['classFilter' =>73, 'namespacePattern' =>71, 'namePatternPart' =>67,], 43 => ['executionPointcut' =>74,], 44 => ['memberReference' =>75, 'memberModifiers' =>53, 'memberModifier' =>54,], 46 => ['namePatternPart' =>77,], 53 => ['classFilter' =>80, 'namespacePattern' =>71, 'namePatternPart' =>67,], 54 => ['memberModifiers' =>82, 'memberModifier' =>54,], 59 => ['namespaceName' =>83,], 60 => ['namespaceName' =>84,], 61 => ['namespaceName' =>85,], 80 => ['memberAccessType' =>100,], 81 => ['memberModifiers' =>103, 'memberModifier' =>54,], 88 => ['argumentList' =>107,], 89 => ['namePatternPart' =>109,], 95 => ['namePatternPart' =>112,], 99 => ['argumentList' =>113,], 100 => ['namePatternPart' =>114,], 116 => ['argumentList' =>119,], 118 => ['namespaceName' =>121,], 123 => ['namespaceName' =>124,],]]; +return ['action' => [0 => ['!' => 4, '(' => 6, 'access' => 19, 'annotation' => 20, 'execution' => 21, 'within' => 22, 'initialization' => 23, 'staticinitialization' => 24, 'dynamic' => 25, 'matchInherited' => 26, 'namePart' => 28,], 1 => ['||' => 29, '$eof' => 0,], 2 => ['&&' => 30, '$eof' => -3, '||' => -3, ')' => -3,], 4 => ['(' => 6, 'access' => 19, 'annotation' => 20, 'execution' => 21, 'within' => 22, 'initialization' => 23, 'staticinitialization' => 24, 'dynamic' => 25, 'matchInherited' => 26, 'namePart' => 28,], 6 => ['!' => 4, '(' => 6, 'access' => 19, 'annotation' => 20, 'execution' => 21, 'within' => 22, 'initialization' => 23, 'staticinitialization' => 24, 'dynamic' => 25, 'matchInherited' => 26, 'namePart' => 28,], 19 => ['(' => 33,], 20 => ['access' => 34, 'execution' => 35, 'within' => 36,], 21 => ['(' => 37,], 22 => ['(' => 38,], 23 => ['(' => 39,], 24 => ['(' => 40,], 25 => ['(' => 41,], 26 => ['(' => 42,], 27 => ['->' => 43, 'nsSeparator' => 44,], 29 => ['!' => 4, '(' => 6, 'access' => 19, 'annotation' => 20, 'execution' => 21, 'within' => 22, 'initialization' => 23, 'staticinitialization' => 24, 'dynamic' => 25, 'matchInherited' => 26, 'namePart' => 28,], 30 => ['!' => 4, '(' => 6, 'access' => 19, 'annotation' => 20, 'execution' => 21, 'within' => 22, 'initialization' => 23, 'staticinitialization' => 24, 'dynamic' => 25, 'matchInherited' => 26, 'namePart' => 28,], 32 => [')' => 47, '||' => 29,], 33 => ['public' => 52, 'protected' => 53, 'private' => 54, 'final' => 55,], 34 => ['(' => 56,], 35 => ['(' => 57,], 36 => ['(' => 58,], 37 => ['**' => 63, '*' => 65, 'namePart' => 66, 'public' => 52, 'protected' => 53, 'private' => 54, 'final' => 55,], 38 => ['**' => 63, '*' => 65, 'namePart' => 66,], 39 => ['**' => 63, '*' => 65, 'namePart' => 66,], 40 => ['**' => 63, '*' => 65, 'namePart' => 66,], 41 => ['public' => 52, 'protected' => 53, 'private' => 54, 'final' => 55,], 42 => [')' => 72,], 43 => ['*' => 65, 'namePart' => 66,], 44 => ['namePart' => 74,], 45 => ['&&' => 30, '$eof' => -2, '||' => -2, ')' => -2,], 48 => [')' => 75,], 50 => ['**' => 63, '*' => 65, 'namePart' => 66,], 51 => ['|' => 77, 'public' => 52, 'protected' => 53, 'private' => 54, 'final' => 55, '**' => -57, '*' => -57, 'namePart' => -57,], 56 => ['namePart' => 28,], 57 => ['namePart' => 28,], 58 => ['namePart' => 28,], 59 => [')' => 82,], 60 => [')' => 83,], 61 => ['(' => 84,], 62 => ['nsSeparator' => 85,], 64 => ['*' => 86, 'namePart' => 87, '|' => 88, 'nsSeparator' => -45, ')' => -45, '+' => -45, '::' => -45, '->' => -45,], 67 => [')' => 89,], 68 => ['+' => 90, 'nsSeparator' => 91, ')' => -39, '::' => -39, '->' => -39,], 69 => [')' => 92,], 70 => [')' => 93,], 71 => ['(' => 94,], 73 => ['*' => 86, 'namePart' => 87, '|' => 88, '$eof' => -32, '||' => -32, '&&' => -32, ')' => -32,], 76 => ['::' => 96, '->' => 97,], 77 => ['public' => 52, 'protected' => 53, 'private' => 54, 'final' => 55,], 79 => [')' => 99, 'nsSeparator' => 44,], 80 => [')' => 100, 'nsSeparator' => 44,], 81 => [')' => 101, 'nsSeparator' => 44,], 84 => ['*' => 103,], 85 => ['**' => 105, '*' => 65, 'namePart' => 66,], 88 => ['namePart' => 106,], 91 => ['**' => 105, '*' => 65, 'namePart' => 66,], 94 => ['*' => 103,], 95 => ['*' => 65, 'namePart' => 66,], 102 => [')' => 110,], 104 => ['(' => 111, '*' => 86, 'namePart' => 87, '|' => 88, 'nsSeparator' => -46,], 107 => ['*' => 86, 'namePart' => 87, '|' => 88, ')' => -46, '+' => -46, 'nsSeparator' => -46, '::' => -46, '->' => -46,], 108 => [')' => 112,], 109 => ['*' => 86, 'namePart' => 87, '|' => 88, ')' => -38, '(' => -38,], 110 => [':' => 113, ')' => -34,], 111 => ['*' => 103,], 112 => [')' => 115,], 113 => ['namePart' => 28,], 114 => [')' => 117,], 116 => ['nsSeparator' => 44, ')' => -35,], 117 => [':' => 118, ')' => -36,], 118 => ['namePart' => 28,], 119 => ['nsSeparator' => 44, ')' => -37,], 3 => ['$eof' => -5, '||' => -5, '&&' => -5, ')' => -5,], 5 => ['$eof' => -7, '||' => -7, '&&' => -7, ')' => -7,], 7 => ['$eof' => -9, '||' => -9, '&&' => -9, ')' => -9,], 8 => ['$eof' => -10, '||' => -10, '&&' => -10, ')' => -10,], 9 => ['$eof' => -11, '||' => -11, '&&' => -11, ')' => -11,], 10 => ['$eof' => -12, '||' => -12, '&&' => -12, ')' => -12,], 11 => ['$eof' => -13, '||' => -13, '&&' => -13, ')' => -13,], 12 => ['$eof' => -14, '||' => -14, '&&' => -14, ')' => -14,], 13 => ['$eof' => -15, '||' => -15, '&&' => -15, ')' => -15,], 14 => ['$eof' => -16, '||' => -16, '&&' => -16, ')' => -16,], 15 => ['$eof' => -17, '||' => -17, '&&' => -17, ')' => -17,], 16 => ['$eof' => -18, '||' => -18, '&&' => -18, ')' => -18,], 17 => ['$eof' => -19, '||' => -19, '&&' => -19, ')' => -19,], 18 => ['$eof' => -20, '||' => -20, '&&' => -20, ')' => -20,], 28 => ['->' => -53, 'nsSeparator' => -53, ')' => -53,], 31 => ['$eof' => -6, '||' => -6, '&&' => -6, ')' => -6,], 46 => ['$eof' => -4, '||' => -4, '&&' => -4, ')' => -4,], 47 => ['$eof' => -8, '||' => -8, '&&' => -8, ')' => -8,], 49 => [')' => -33,], 52 => ['**' => -58, '*' => -58, 'namePart' => -58, '|' => -58, 'public' => -58, 'protected' => -58, 'private' => -58, 'final' => -58,], 53 => ['**' => -59, '*' => -59, 'namePart' => -59, '|' => -59, 'public' => -59, 'protected' => -59, 'private' => -59, 'final' => -59,], 54 => ['**' => -60, '*' => -60, 'namePart' => -60, '|' => -60, 'public' => -60, 'protected' => -60, 'private' => -60, 'final' => -60,], 55 => ['**' => -61, '*' => -61, 'namePart' => -61, '|' => -61, 'public' => -61, 'protected' => -61, 'private' => -61, 'final' => -61,], 63 => ['nsSeparator' => -44, ')' => -44, '+' => -44, '::' => -44, '->' => -44,], 65 => ['$eof' => -48, '||' => -48, '&&' => -48, ')' => -48, '(' => -48, 'nsSeparator' => -48, '*' => -48, 'namePart' => -48, '|' => -48, '+' => -48, '::' => -48, '->' => -48,], 66 => ['$eof' => -49, '||' => -49, '&&' => -49, ')' => -49, '(' => -49, 'nsSeparator' => -49, '*' => -49, 'namePart' => -49, '|' => -49, '+' => -49, '::' => -49, '->' => -49,], 72 => ['$eof' => -30, '||' => -30, '&&' => -30, ')' => -30,], 74 => ['->' => -54, 'nsSeparator' => -54, ')' => -54,], 75 => ['$eof' => -21, '||' => -21, '&&' => -21, ')' => -21,], 78 => ['**' => -56, '*' => -56, 'namePart' => -56,], 82 => ['$eof' => -22, '||' => -22, '&&' => -22, ')' => -22,], 83 => ['$eof' => -23, '||' => -23, '&&' => -23, ')' => -23,], 86 => ['$eof' => -50, '||' => -50, '&&' => -50, ')' => -50, '(' => -50, 'nsSeparator' => -50, '*' => -50, 'namePart' => -50, '|' => -50, '+' => -50, '::' => -50, '->' => -50,], 87 => ['$eof' => -51, '||' => -51, '&&' => -51, ')' => -51, '(' => -51, 'nsSeparator' => -51, '*' => -51, 'namePart' => -51, '|' => -51, '+' => -51, '::' => -51, '->' => -51,], 89 => ['$eof' => -24, '||' => -24, '&&' => -24, ')' => -24,], 90 => [')' => -40, '::' => -40, '->' => -40,], 92 => ['$eof' => -28, '||' => -28, '&&' => -28, ')' => -28,], 93 => ['$eof' => -29, '||' => -29, '&&' => -29, ')' => -29,], 96 => ['*' => -42, 'namePart' => -42,], 97 => ['*' => -43, 'namePart' => -43,], 98 => ['**' => -55, '*' => -55, 'namePart' => -55,], 99 => ['$eof' => -25, '||' => -25, '&&' => -25, ')' => -25,], 100 => ['$eof' => -26, '||' => -26, '&&' => -26, ')' => -26,], 101 => ['$eof' => -27, '||' => -27, '&&' => -27, ')' => -27,], 103 => [')' => -41,], 105 => ['nsSeparator' => -47, ')' => -47, '+' => -47, '::' => -47, '->' => -47,], 106 => ['$eof' => -52, '||' => -52, '&&' => -52, ')' => -52, '(' => -52, 'nsSeparator' => -52, '*' => -52, 'namePart' => -52, '|' => -52, '+' => -52, '::' => -52, '->' => -52,], 115 => ['$eof' => -31, '||' => -31, '&&' => -31, ')' => -31,],], 'goto' => [0 => ['pointcutExpression' => 1, 'conjugatedExpression' => 2, 'negatedExpression' => 3, 'brakedExpression' => 5, 'singlePointcut' => 7, 'accessPointcut' => 8, 'annotatedAccessPointcut' => 9, 'executionPointcut' => 10, 'annotatedExecutionPointcut' => 11, 'withinPointcut' => 12, 'annotatedWithinPointcut' => 13, 'initializationPointcut' => 14, 'staticInitializationPointcut' => 15, 'dynamicExecutionPointcut' => 16, 'matchInheritedPointcut' => 17, 'pointcutReference' => 18, 'namespaceName' => 27,], 4 => ['brakedExpression' => 31, 'singlePointcut' => 7, 'accessPointcut' => 8, 'annotatedAccessPointcut' => 9, 'executionPointcut' => 10, 'annotatedExecutionPointcut' => 11, 'withinPointcut' => 12, 'annotatedWithinPointcut' => 13, 'initializationPointcut' => 14, 'staticInitializationPointcut' => 15, 'dynamicExecutionPointcut' => 16, 'matchInheritedPointcut' => 17, 'pointcutReference' => 18, 'namespaceName' => 27,], 6 => ['pointcutExpression' => 32, 'conjugatedExpression' => 2, 'negatedExpression' => 3, 'brakedExpression' => 5, 'singlePointcut' => 7, 'accessPointcut' => 8, 'annotatedAccessPointcut' => 9, 'executionPointcut' => 10, 'annotatedExecutionPointcut' => 11, 'withinPointcut' => 12, 'annotatedWithinPointcut' => 13, 'initializationPointcut' => 14, 'staticInitializationPointcut' => 15, 'dynamicExecutionPointcut' => 16, 'matchInheritedPointcut' => 17, 'pointcutReference' => 18, 'namespaceName' => 27,], 29 => ['conjugatedExpression' => 45, 'negatedExpression' => 3, 'brakedExpression' => 5, 'singlePointcut' => 7, 'accessPointcut' => 8, 'annotatedAccessPointcut' => 9, 'executionPointcut' => 10, 'annotatedExecutionPointcut' => 11, 'withinPointcut' => 12, 'annotatedWithinPointcut' => 13, 'initializationPointcut' => 14, 'staticInitializationPointcut' => 15, 'dynamicExecutionPointcut' => 16, 'matchInheritedPointcut' => 17, 'pointcutReference' => 18, 'namespaceName' => 27,], 30 => ['negatedExpression' => 46, 'brakedExpression' => 5, 'singlePointcut' => 7, 'accessPointcut' => 8, 'annotatedAccessPointcut' => 9, 'executionPointcut' => 10, 'annotatedExecutionPointcut' => 11, 'withinPointcut' => 12, 'annotatedWithinPointcut' => 13, 'initializationPointcut' => 14, 'staticInitializationPointcut' => 15, 'dynamicExecutionPointcut' => 16, 'matchInheritedPointcut' => 17, 'pointcutReference' => 18, 'namespaceName' => 27,], 33 => ['propertyAccessReference' => 48, 'memberReference' => 49, 'memberModifiers' => 50, 'memberModifier' => 51,], 37 => ['methodExecutionReference' => 59, 'functionExecutionReference' => 60, 'memberReference' => 61, 'namespacePattern' => 62, 'memberModifiers' => 50, 'namePatternPart' => 64, 'memberModifier' => 51,], 38 => ['classFilter' => 67, 'namespacePattern' => 68, 'namePatternPart' => 64,], 39 => ['classFilter' => 69, 'namespacePattern' => 68, 'namePatternPart' => 64,], 40 => ['classFilter' => 70, 'namespacePattern' => 68, 'namePatternPart' => 64,], 41 => ['memberReference' => 71, 'memberModifiers' => 50, 'memberModifier' => 51,], 43 => ['namePatternPart' => 73,], 50 => ['classFilter' => 76, 'namespacePattern' => 68, 'namePatternPart' => 64,], 51 => ['memberModifiers' => 78, 'memberModifier' => 51,], 56 => ['namespaceName' => 79,], 57 => ['namespaceName' => 80,], 58 => ['namespaceName' => 81,], 76 => ['memberAccessType' => 95,], 77 => ['memberModifiers' => 98, 'memberModifier' => 51,], 84 => ['argumentList' => 102,], 85 => ['namePatternPart' => 104,], 91 => ['namePatternPart' => 107,], 94 => ['argumentList' => 108,], 95 => ['namePatternPart' => 109,], 111 => ['argumentList' => 114,], 113 => ['namespaceName' => 116,], 118 => ['namespaceName' => 119,],]]; diff --git a/src/Aop/Pointcut/PointcutParser.php b/src/Aop/Pointcut/PointcutParser.php index 73a3eab6..e68a2c08 100644 --- a/src/Aop/Pointcut/PointcutParser.php +++ b/src/Aop/Pointcut/PointcutParser.php @@ -12,19 +12,31 @@ namespace Go\Aop\Pointcut; +use Dissect\Lexer\TokenStream\TokenStream; use Dissect\Parser\LALR1\Parser; +use Go\Aop\Pointcut; /** * Pointcut parser extends the default parser with parse table and strict typehint for grammar */ -class PointcutParser extends Parser +final class PointcutParser extends Parser { - /** - * {@inheritDoc} - */ public function __construct(PointcutGrammar $grammar) { $parseTable = include __DIR__ . '/PointcutParseTable.php'; parent::__construct($grammar, $parseTable); } + + /** + * @return Pointcut Covariant, always {@see Pointcut} + */ + public function parse(TokenStream $stream): Pointcut + { + $result = parent::parse($stream); + if (!$result instanceof Pointcut) { + throw new \UnexpectedValueException("Expected instance of Pointcut to be received during parsing"); + } + + return $result; + } } diff --git a/src/Aop/Pointcut/PointcutReference.php b/src/Aop/Pointcut/PointcutReference.php index d53bd7ec..32c22a72 100644 --- a/src/Aop/Pointcut/PointcutReference.php +++ b/src/Aop/Pointcut/PointcutReference.php @@ -13,9 +13,13 @@ namespace Go\Aop\Pointcut; use Go\Aop\Pointcut; -use Go\Aop\PointFilter; use Go\Core\AspectContainer; use Go\Core\AspectKernel; +use Go\ParserReflection\ReflectionFileNamespace; +use ReflectionClass; +use ReflectionFunction; +use ReflectionMethod; +use ReflectionProperty; /** * Reference to the pointcut holds an id of pointcut to fetch when needed @@ -24,66 +28,36 @@ final class PointcutReference implements Pointcut { private ?Pointcut $pointcut = null; - /** - * Name of the pointcut to fetch from the container - */ - private string $pointcutId; - - /** - * Instance of aspect container - */ - private AspectContainer $container; - /** * Pointcut reference constructor - */ - public function __construct(AspectContainer $container, string $pointcutId) - { - $this->container = $container; - $this->pointcutId = $pointcutId; - } - - /** - * 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 + * @param string $pointcutId Name of the pointcut to fetch from the container */ - public function matches($point, $context = null, $instance = null, array $arguments = null): bool - { - return $this->getPointcut()->matches($point, $context, $instance, $arguments); + public function __construct( + private AspectContainer $container, + private readonly string $pointcutId + ) {} + + public function matches( + ReflectionClass|ReflectionFileNamespace $context, + ReflectionMethod|ReflectionProperty|ReflectionFunction $reflector = null, + object|string $instanceOrScope = null, + array $arguments = null + ): bool { + return $this->getPointcut()->matches($context, $reflector, $instanceOrScope, $arguments); } - /** - * Returns the kind of point filter - */ public function getKind(): int { return $this->getPointcut()->getKind(); } - /** - * Return the class filter for this pointcut. - */ - public function getClassFilter(): PointFilter - { - return $this->getPointcut()->getClassFilter(); - } - - /** - * @inheritdoc - */ - public function __sleep() + public function __sleep(): array { return ['pointcutId']; } - /** - * @inheritdoc - */ - public function __wakeup() + public function __wakeup(): void { $this->container = AspectKernel::getInstance()->getContainer(); } @@ -93,7 +67,7 @@ public function __wakeup() */ private function getPointcut(): Pointcut { - if (!$this->pointcut) { + if (!isset($this->pointcut)) { $this->pointcut = $this->container->getPointcut($this->pointcutId); } diff --git a/src/Aop/Pointcut/ReturnTypePointcut.php b/src/Aop/Pointcut/ReturnTypePointcut.php new file mode 100644 index 00000000..ff20f6f6 --- /dev/null +++ b/src/Aop/Pointcut/ReturnTypePointcut.php @@ -0,0 +1,92 @@ + + * + * This source file is subject to the license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Go\Aop\Pointcut; + +use Go\Aop\Pointcut; +use Go\ParserReflection\ReflectionFileNamespace; +use ReflectionClass; +use ReflectionFunction; +use ReflectionFunctionAbstract; +use ReflectionMethod; +use ReflectionProperty; + +/** + * Return type filter matcher methods and function with specific return type + * + * Type name can contain wildcards '*', '**' and '?' + * + * This implementation currently doesn't support properly matching of complex types, + * thus union/intersection/DNF types are not supported yet here. + */ +final readonly class ReturnTypePointcut implements Pointcut +{ + /** + * Return type name to match, can contain wildcards *,? + */ + private string $typeName; + + /** + * Pattern for regular expression matching + */ + private string $regexp; + + /** + * Return type name matcher constructor accepts name or glob pattern of the type to match + * + * @param (string&non-empty-string) $returnTypeName + */ + public function __construct(string $returnTypeName) + { + $returnTypeName = trim($returnTypeName, '\\'); + if (strlen($returnTypeName) === 0) { + throw new \InvalidArgumentException("Return type name must not be empty"); + } + $this->typeName = $returnTypeName; + $this->regexp = '/^(' . strtr(preg_quote($this->typeName, '/'), [ + '\\*' => '[^\\\\]+', + '\\?' => '.', + ]) . ')$/'; + } + + public function matches( + ReflectionClass|ReflectionFileNamespace $context, + ReflectionMethod|ReflectionProperty|ReflectionFunction $reflector = null, + object|string $instanceOrScope = null, + array $arguments = null + ): bool { + // With only static context we always match, as we don't have any information about concrete reflector + if (!isset($reflector)) { + return true; + } + + // We don't support anything that is not function-like + if (!$reflector instanceof ReflectionFunctionAbstract) { + return false; + } + + // If reflector doesn't have a return type, we should not match + if (!$reflector->hasReturnType()) { + return false; + } + + $returnType = (string) $reflector->getReturnType(); + + // Either we have exact type string match or type matches our regular expression + return ($returnType === $this->typeName) || preg_match($this->regexp, $returnType); + } + + public function getKind(): int + { + return Pointcut::KIND_METHOD | Pointcut::KIND_FUNCTION; + } +} diff --git a/src/Aop/Pointcut/SignaturePointcut.php b/src/Aop/Pointcut/SignaturePointcut.php deleted file mode 100644 index 0094377c..00000000 --- a/src/Aop/Pointcut/SignaturePointcut.php +++ /dev/null @@ -1,88 +0,0 @@ - - * - * This source file is subject to the license that is bundled - * with this source code in the file LICENSE. - */ - -namespace Go\Aop\Pointcut; - -use Go\Aop\Pointcut; -use Go\Aop\PointFilter; - -/** - * Signature pointcut checks element signature (modifiers and name) to match it - */ -class SignaturePointcut implements Pointcut -{ - use PointcutClassFilterTrait; - - /** - * Element name to match, can contain wildcards **,*,?,| - */ - protected string $name = ''; - - /** - * Regular expression for pattern matching - */ - protected string $regexp; - - /** - * Modifier filter for element - */ - protected PointFilter $modifierFilter; - - /** - * Filter kind, e.g. self::KIND_CLASS - */ - protected int $filterKind = 0; - - /** - * Signature matcher constructor - */ - public function __construct(int $filterKind, string $name, PointFilter $modifierFilter) - { - $this->filterKind = $filterKind; - $this->name = $name; - $this->regexp = strtr( - preg_quote($this->name, '/'), - [ - '\\*' => '[^\\\\]+?', - '\\*\\*' => '.+?', - '\\?' => '.', - '\\|' => '|' - ] - ); - $this->modifierFilter = $modifierFilter; - } - - /** - * 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 - { - if (!$this->modifierFilter->matches($point, $context)) { - return false; - } - - return ($point->name === $this->name) || (bool)preg_match("/^(?:{$this->regexp})$/", $point->name); - } - - /** - * Returns the kind of point filter - */ - public function getKind(): int - { - return $this->filterKind; - } -} diff --git a/src/Aop/Pointcut/TruePointcut.php b/src/Aop/Pointcut/TruePointcut.php index 26b201cd..1d74618b 100644 --- a/src/Aop/Pointcut/TruePointcut.php +++ b/src/Aop/Pointcut/TruePointcut.php @@ -13,45 +13,37 @@ namespace Go\Aop\Pointcut; use Go\Aop\Pointcut; +use Go\ParserReflection\ReflectionFileNamespace; +use ReflectionClass; +use ReflectionFunction; +use ReflectionMethod; +use ReflectionProperty; /** * Canonical Pointcut instance that always matches. */ -class TruePointcut implements Pointcut +final readonly class TruePointcut implements Pointcut { - use PointcutClassFilterTrait; - /** - * Filter kind + * Default constructor can be used to specify concrete pointcut kind */ - protected int $filterKind; + public function __construct(private int $pointcutKind = self::KIND_ALL) {} /** - * Default constructor can be used to specify concrete filter kind + * @inheritdoc + * @return true Covariant, always true for TruePointcut */ - public function __construct(int $filterKind = self::KIND_ALL) - { - $this->filterKind = $filterKind; - } - - /** - * 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 - { + public function matches( + ReflectionClass|ReflectionFileNamespace $context, + ReflectionMethod|ReflectionProperty|ReflectionFunction $reflector = null, + object|string $instanceOrScope = null, + array $arguments = null + ): true { return true; } - /** - * Returns the kind of point filter - */ public function getKind(): int { - return $this->filterKind; + return $this->pointcutKind; } } diff --git a/src/Aop/PointcutAdvisor.php b/src/Aop/PointcutAdvisor.php index aae3ede3..dc8a0a80 100644 --- a/src/Aop/PointcutAdvisor.php +++ b/src/Aop/PointcutAdvisor.php @@ -14,8 +14,6 @@ /** * Super-interface for all Advisors that are driven by a pointcut. - * - * This covers nearly all advisors except introduction advisors, for which method-level matching doesn't apply. */ interface PointcutAdvisor extends Advisor { diff --git a/src/Aop/Support/AbstractGenericAdvisor.php b/src/Aop/Support/AbstractGenericAdvisor.php deleted file mode 100644 index fea291ce..00000000 --- a/src/Aop/Support/AbstractGenericAdvisor.php +++ /dev/null @@ -1,43 +0,0 @@ - - * - * This source file is subject to the license that is bundled - * with this source code in the file LICENSE. - */ - -namespace Go\Aop\Support; - -use Go\Aop\Advice; -use Go\Aop\Advisor; - -/** - * Abstract generic Advisor that allows for any Advice to be configured. - */ -abstract class AbstractGenericAdvisor implements Advisor -{ - /** - * Instance of advice - */ - protected Advice $advice; - - /** - * Initializes an advisor with advice - */ - public function __construct(Advice $advice) - { - $this->advice = $advice; - } - - /** - * Returns an advice to apply - */ - public function getAdvice(): Advice - { - return $this->advice; - } -} diff --git a/src/Aop/Support/AndPointFilter.php b/src/Aop/Support/AndPointFilter.php deleted file mode 100644 index 4750a8a6..00000000 --- a/src/Aop/Support/AndPointFilter.php +++ /dev/null @@ -1,71 +0,0 @@ - - * - * This source file is subject to the license that is bundled - * with this source code in the file LICENSE. - */ - -namespace Go\Aop\Support; - -use Go\Aop\PointFilter; - -/** - * Logical "and" filter. - */ -class AndPointFilter implements PointFilter -{ - /** - * Kind of filter - */ - private int $kind = -1; - - /** - * List of PointFilters to combine with "AND" - * - * @var array - */ - private array $filters; - - /** - * And constructor - */ - public function __construct(PointFilter ...$filters) - { - foreach ($filters as $filter) { - $this->kind &= $filter->getKind(); - } - $this->filters = $filters; - } - - /** - * 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 - { - foreach ($this->filters as $filter) { - if (!$filter->matches($point, $context)) { - return false; - } - } - - return true; - } - - /** - * Returns the kind of point filter - */ - public function getKind(): int - { - return $this->kind; - } -} diff --git a/src/Aop/Support/DeclareParentsAdvisor.php b/src/Aop/Support/DeclareParentsAdvisor.php deleted file mode 100644 index ab5cd5ea..00000000 --- a/src/Aop/Support/DeclareParentsAdvisor.php +++ /dev/null @@ -1,35 +0,0 @@ - - * - * This source file is subject to the license that is bundled - * with this source code in the file LICENSE. - */ - -namespace Go\Aop\Support; - -use Go\Aop\IntroductionAdvisor; -use Go\Aop\IntroductionInfo; -use Go\Aop\Pointcut; -use Go\Aop\Pointcut\PointcutClassFilterTrait; - -/** - * Introduction advisor delegating to the given object. - */ -class DeclareParentsAdvisor extends AbstractGenericAdvisor implements IntroductionAdvisor -{ - use PointcutClassFilterTrait; - - /** - * Creates an advisor for declaring mixins via trait and interface. - */ - public function __construct(Pointcut $pointcut, IntroductionInfo $info) - { - $this->classFilter = $pointcut->getClassFilter(); - parent::__construct($info); - } -} diff --git a/src/Aop/Support/DefaultPointcutAdvisor.php b/src/Aop/Support/GenericPointcutAdvisor.php similarity index 51% rename from src/Aop/Support/DefaultPointcutAdvisor.php rename to src/Aop/Support/GenericPointcutAdvisor.php index 8ef64cc5..2affcb68 100644 --- a/src/Aop/Support/DefaultPointcutAdvisor.php +++ b/src/Aop/Support/GenericPointcutAdvisor.php @@ -17,49 +17,33 @@ use Go\Aop\Intercept\Interceptor; use Go\Aop\Pointcut; use Go\Aop\PointcutAdvisor; -use Go\Aop\PointFilter; /** * Convenient Pointcut-driven Advisor implementation. * * This is the most commonly used Advisor implementation. It can be used with any pointcut and advice type, - * except for introductions. There is normally no need to subclass this class, or to implement custom Advisors. + * including introductions. */ -class DefaultPointcutAdvisor extends AbstractGenericAdvisor implements PointcutAdvisor +final readonly class GenericPointcutAdvisor implements PointcutAdvisor { - /** - * The Pointcut targeting the Advice - */ - private Pointcut $pointcut; + public function __construct(private Pointcut $pointcut, private Advice $advice) {} - /** - * Creates a DefaultPointcutAdvisor, specifying the Advice to run when Pointcut matches - */ - public function __construct(Pointcut $pointcut, Advice $advice) - { - $this->pointcut = $pointcut; - parent::__construct($advice); - } - - /** - * {@inheritdoc} - */ public function getAdvice(): Advice { - $advice = parent::getAdvice(); - if (($advice instanceof Interceptor) && ($this->pointcut->getKind() & PointFilter::KIND_DYNAMIC)) { + // For dynamic pointcuts, we use special dynamic invocation matcher interceptor + // This part can't be moved to the constructor, as it breaks lazy-evaluation for PointcutReference + if (($this->advice instanceof Interceptor) && ($this->pointcut->getKind() & Pointcut::KIND_DYNAMIC)) { $advice = new DynamicInvocationMatcherInterceptor( $this->pointcut, - $advice + $this->advice ); + } else { + $advice = $this->advice; } return $advice; } - /** - * Get the Pointcut that drives this advisor. - */ public function getPointcut(): Pointcut { return $this->pointcut; diff --git a/src/Aop/Support/InheritanceClassFilter.php b/src/Aop/Support/InheritanceClassFilter.php deleted file mode 100644 index 55f5235c..00000000 --- a/src/Aop/Support/InheritanceClassFilter.php +++ /dev/null @@ -1,60 +0,0 @@ - - * - * This source file is subject to the license that is bundled - * with this source code in the file LICENSE. - */ - -namespace Go\Aop\Support; - -use ReflectionClass; -use Go\Aop\PointFilter; - -/** - * Inheritance class matcher that match single class name or any subclass - */ -class InheritanceClassFilter implements PointFilter -{ - /** - * Parent class or interface name to match in hierarchy - */ - protected string $parentClass; - - /** - * Inheritance class matcher constructor - */ - public function __construct(string $parentClassName) - { - $this->parentClass = $parentClassName; - } - - /** - * Performs matching of point of code - * - * @param mixed $class 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($class, $context = null, $instance = null, array $arguments = null): bool - { - if (!$class instanceof ReflectionClass) { - return false; - } - - return $class->isSubclassOf($this->parentClass) || \in_array($this->parentClass, $class->getInterfaceNames()); - } - - /** - * Returns the kind of point filter - */ - public function getKind(): int - { - return self::KIND_CLASS; - } -} diff --git a/src/Aop/Support/LazyPointcutAdvisor.php b/src/Aop/Support/LazyPointcutAdvisor.php index 3a93f552..a9da8666 100644 --- a/src/Aop/Support/LazyPointcutAdvisor.php +++ b/src/Aop/Support/LazyPointcutAdvisor.php @@ -20,39 +20,27 @@ /** * Lazy pointcut advisor is used to create a delayed pointcut only when needed */ -class LazyPointcutAdvisor extends AbstractGenericAdvisor implements PointcutAdvisor +final class LazyPointcutAdvisor implements PointcutAdvisor { /** - * Pointcut expression represented with string + * Instance of parsed pointcut, might be uninitialized if not parsed yet */ - private string $pointcutExpression; - - /** - * Instance of parsed pointcut - */ - private ?Pointcut $pointcut = null; - - /** - * Instance of aspect container - */ - private AspectContainer $container; + private Pointcut $pointcut; /** * Creates the LazyPointcutAdvisor by specifying textual pointcut expression and Advice to run when Pointcut matches. + * + * @param string $pointcutExpression Pointcut expression represented with string */ - public function __construct(AspectContainer $container, string $pointcutExpression, Advice $advice) - { - $this->container = $container; - $this->pointcutExpression = $pointcutExpression; - parent::__construct($advice); - } + public function __construct( + private readonly AspectContainer $container, + private readonly string $pointcutExpression, + private readonly Advice $advice + ) {} - /** - * Get the Pointcut that drives this advisor. - */ public function getPointcut(): Pointcut { - if ($this->pointcut === null) { + if (!isset($this->pointcut)) { // Inject these dependencies and make them lazy! /** @var Pointcut\PointcutLexer $lexer */ @@ -67,4 +55,9 @@ public function getPointcut(): Pointcut return $this->pointcut; } + + public function getAdvice(): Advice + { + return $this->advice; + } } diff --git a/src/Aop/Support/NamespacedReflectionFunction.php b/src/Aop/Support/NamespacedReflectionFunction.php deleted file mode 100644 index b0f29896..00000000 --- a/src/Aop/Support/NamespacedReflectionFunction.php +++ /dev/null @@ -1,49 +0,0 @@ - - * - * This source file is subject to the license that is bundled - * with this source code in the file LICENSE. - */ - -namespace Go\Aop\Support; - -use ReflectionFunction; - -/** - * Namespaced version of global functions - */ -class NamespacedReflectionFunction extends ReflectionFunction -{ - /** - * Custom namespace name - */ - private string $namespace; - - /** - * Extends the logic with passing the namespace name - * - * {@inheritDoc} - */ - public function __construct(string $functionName, string $namespaceName = '') - { - $this->namespace = $namespaceName; - parent::__construct($functionName); - } - - /** - * {@inheritDoc} - */ - public function getNamespaceName(): string - { - if (!empty($this->namespace)) { - return $this->namespace; - } - - return parent::getNamespaceName(); - } -} diff --git a/src/Aop/Support/NotPointFilter.php b/src/Aop/Support/NotPointFilter.php deleted file mode 100644 index 61fcccff..00000000 --- a/src/Aop/Support/NotPointFilter.php +++ /dev/null @@ -1,61 +0,0 @@ - - * - * This source file is subject to the license that is bundled - * with this source code in the file LICENSE. - */ - -namespace Go\Aop\Support; - -use Go\Aop\PointFilter; - -/** - * Logical "not" filter. - */ -class NotPointFilter implements PointFilter -{ - /** - * Kind of filter - */ - private int $kind; - - /** - * Instance of filter to negate - */ - private PointFilter $filter; - - /** - * Not constructor - */ - public function __construct(PointFilter $filter) - { - $this->kind = $filter->getKind(); - $this->filter = $filter; - } - - /** - * 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->filter->matches($point, $context); - } - - /** - * Returns the kind of point filter - */ - public function getKind(): int - { - return $this->kind; - } -} diff --git a/src/Aop/Support/OrPointFilter.php b/src/Aop/Support/OrPointFilter.php deleted file mode 100644 index 39bbdd93..00000000 --- a/src/Aop/Support/OrPointFilter.php +++ /dev/null @@ -1,71 +0,0 @@ - - * - * This source file is subject to the license that is bundled - * with this source code in the file LICENSE. - */ - -namespace Go\Aop\Support; - -use Go\Aop\PointFilter; - -/** - * Logical "or" filter. - */ -class OrPointFilter implements PointFilter -{ - /** - * Kind of filter - */ - private int $kind = 0; - - /** - * List of PointFilter to combine - * - * @var array - */ - private array $filters; - - /** - * Or constructor - */ - public function __construct(PointFilter ...$filters) - { - foreach ($filters as $filter) { - $this->kind |= $filter->getKind(); - } - $this->filters = $filters; - } - - /** - * 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 - { - foreach ($this->filters as $filter) { - if ($filter->matches($point, $context)) { - return true; - } - } - - return false; - } - - /** - * Returns the kind of point filter - */ - public function getKind(): int - { - return $this->kind; - } -} diff --git a/src/Aop/Support/PointcutBuilder.php b/src/Aop/Support/PointcutBuilder.php index df7e141b..d31f800b 100644 --- a/src/Aop/Support/PointcutBuilder.php +++ b/src/Aop/Support/PointcutBuilder.php @@ -24,20 +24,12 @@ /** * Pointcut builder provides simple DSL for declaring pointcuts in plain PHP code */ -class PointcutBuilder +final readonly class PointcutBuilder { - /** - * Instance of aspect container - */ - protected AspectContainer $container; - /** * Default constructor for the builder */ - public function __construct(AspectContainer $container) - { - $this->container = $container; - } + public function __construct(private AspectContainer $container) {} /** * Declares the "Before" hook for specific pointcut expression @@ -77,6 +69,9 @@ public function around(string $pointcutExpression, Closure $adviceToInvoke): voi /** * Declares the error message for specific pointcut expression with concrete error level + * + * @param (string&non-empty-string) $message Error message to show for this intercepton + * @param int&(E_USER_NOTICE|E_USER_WARNING|E_USER_ERROR|E_USER_DEPRECATED) $errorLevel Default level of error, only E_USER_* constants */ public function declareError(string $pointcutExpression, string $message, int $errorLevel = E_USER_ERROR): void { diff --git a/src/Aop/Support/ReturnTypeFilter.php b/src/Aop/Support/ReturnTypeFilter.php deleted file mode 100644 index fcce9475..00000000 --- a/src/Aop/Support/ReturnTypeFilter.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\Aop\Support; - -use Go\Aop\PointFilter; -use ReflectionFunctionAbstract; - -/** - * Return type filter matcher methods and function with specific return type - * - * Type name can contain wildcards '*', '**' and '?' - */ -class ReturnTypeFilter implements PointFilter -{ - /** - * Return type name to match, can contain wildcards *,? - */ - protected string $typeName; - - /** - * Pattern for regular expression matching - */ - protected string $regexp; - - /** - * Return type name matcher constructor accepts name or glob pattern of the type to match - */ - public function __construct(string $returnTypeName) - { - $returnTypeName = trim($returnTypeName, '\\'); - $this->typeName = $returnTypeName; - $this->regexp = strtr(preg_quote($this->typeName, '/'), [ - '\\*' => '[^\\\\]+', - '\\*\\*' => '.+', - '\\?' => '.', - '\\|' => '|' - ]); - } - - /** - * {@inheritdoc} - */ - public function matches($functionLike, $context = null, $instance = null, array $arguments = null): bool - { - if (!$functionLike instanceof ReflectionFunctionAbstract) { - return false; - } - - if (!$functionLike->hasReturnType()) { - return false; - } - - $returnType = (string) $functionLike->getReturnType(); - - return ($returnType === $this->typeName) || (bool) preg_match("/^(?:{$this->regexp})$/", $returnType); - } - - /** - * Returns the kind of point filter - */ - public function getKind(): int - { - return self::KIND_METHOD | self::KIND_FUNCTION; - } -} diff --git a/src/Aop/Support/SimpleNamespaceFilter.php b/src/Aop/Support/SimpleNamespaceFilter.php deleted file mode 100644 index eeee3b05..00000000 --- a/src/Aop/Support/SimpleNamespaceFilter.php +++ /dev/null @@ -1,73 +0,0 @@ - - * - * This source file is subject to the license that is bundled - * with this source code in the file LICENSE. - */ - -namespace Go\Aop\Support; - -use Go\Aop\PointFilter; -use Go\ParserReflection\ReflectionFileNamespace; - -/** - * Simple namespace matcher that match only specific namespace name - * - * Namespace name can contain wildcards '*', '**' and '?' - */ -class SimpleNamespaceFilter implements PointFilter -{ - /** - * Namespace name to match, can contain wildcards *,? - */ - protected string $nsName; - - /** - * Pattern for regular expression matching - */ - protected string $regexp; - - /** - * Namespace name matcher constructor that accepts name or glob pattern to match - */ - public function __construct(string $namespaceName) - { - $namespaceName = trim($namespaceName, '\\'); - $this->nsName = $namespaceName; - $this->regexp = strtr(preg_quote($this->nsName, '/'), [ - '\\*' => '[^\\\\]+', - '\\*\\*' => '.+', - '\\?' => '.', - '\\|' => '|' - ]); - } - - /** - * {@inheritdoc} - */ - public function matches($ns, $context = null, $instance = null, array $arguments = null): bool - { - $isNamespaceIsObject = ($ns === (object) $ns); - - if ($isNamespaceIsObject && !$ns instanceof ReflectionFileNamespace) { - return false; - } - - $nsName = ($ns instanceof ReflectionFileNamespace) ? $ns->getName() : $ns; - - return ($nsName === $this->nsName) || (bool) preg_match("/^(?:{$this->regexp})$/", $nsName); - } - - /** - * Returns the kind of point filter - */ - public function getKind(): int - { - return 0; - } -} diff --git a/src/Aop/Support/TruePointFilter.php b/src/Aop/Support/TruePointFilter.php deleted file mode 100644 index fa3ec579..00000000 --- a/src/Aop/Support/TruePointFilter.php +++ /dev/null @@ -1,57 +0,0 @@ - - * - * This source file is subject to the license that is bundled - * with this source code in the file LICENSE. - */ - -namespace Go\Aop\Support; - -use Go\Aop\PointFilter; - -/** - * Canonical PointFilter instance that matches all points. - */ -class TruePointFilter implements PointFilter -{ - /** - * Private class constructor - */ - private function __construct() - { - } - - /** - * Singleton pattern - */ - public static function getInstance(): self - { - static $instance = null; - if ($instance === null) { - $instance = new self(); - } - - return $instance; - } - - /** - * @inheritdoc - */ - public function matches($point, $context = null, $instance = null, array $arguments = null): bool - { - return true; - } - - /** - * Returns the kind of point filter - */ - public function getKind(): int - { - return self::KIND_ALL; - } -} diff --git a/src/Core/AbstractAspectLoaderExtension.php b/src/Core/AbstractAspectLoaderExtension.php index b2063d79..a989eab0 100644 --- a/src/Core/AbstractAspectLoaderExtension.php +++ b/src/Core/AbstractAspectLoaderExtension.php @@ -13,13 +13,13 @@ namespace Go\Core; use Dissect\Lexer\Exception\RecognitionException; -use Dissect\Lexer\Lexer; use Dissect\Lexer\TokenStream\TokenStream; use Dissect\Parser\Exception\UnexpectedTokenException; -use Dissect\Parser\Parser; use Go\Aop\Aspect; use Go\Aop\Pointcut; -use Go\Aop\PointFilter; +use Go\Aop\Pointcut\PointcutLexer; +use Go\Aop\Pointcut\PointcutParser; +use ReflectionClass; use ReflectionMethod; use ReflectionProperty; use UnexpectedValueException; @@ -29,35 +29,26 @@ */ abstract class AbstractAspectLoaderExtension implements AspectLoaderExtension { - /** - * Instance of pointcut lexer - */ - protected Lexer $pointcutLexer; - - /** - * Instance of pointcut parser - */ - protected Parser $pointcutParser; - /** * Default loader constructor that accepts pointcut lexer and parser */ - public function __construct(Lexer $pointcutLexer, Parser $pointcutParser) - { - $this->pointcutLexer = $pointcutLexer; - $this->pointcutParser = $pointcutParser; - } + public function __construct( + protected PointcutLexer $pointcutLexer, + protected PointcutParser $pointcutParser + ) {} /** * General method for parsing pointcuts * - * @param mixed|ReflectionMethod|ReflectionProperty $reflection Reflection of point - * * @throws UnexpectedValueException if there was an error during parsing - * @return Pointcut|PointFilter + * @param ReflectionMethod|ReflectionProperty|ReflectionClass $reflection + * @template T of Aspect */ - final protected function parsePointcut(Aspect $aspect, $reflection, string $pointcutExpression): PointFilter - { + final protected function parsePointcut( + Aspect $aspect, + ReflectionMethod|ReflectionProperty|ReflectionClass $reflection, + string $pointcutExpression + ): Pointcut { $stream = $this->makeLexicalAnalyze($aspect, $reflection, $pointcutExpression); return $this->parseTokenStream($reflection, $pointcutExpression, $stream); @@ -66,12 +57,16 @@ final protected function parsePointcut(Aspect $aspect, $reflection, string $poin /** * Performs lexical analyze of pointcut * - * @param ReflectionMethod|ReflectionProperty $reflection + * @param ReflectionMethod|ReflectionProperty|ReflectionClass $reflection + * @template T of Aspect * * @throws UnexpectedValueException */ - private function makeLexicalAnalyze(Aspect $aspect, $reflection, string $pointcutExpression): TokenStream - { + private function makeLexicalAnalyze( + Aspect $aspect, + ReflectionMethod|ReflectionProperty|ReflectionClass $reflection, + string $pointcutExpression + ): TokenStream { try { $resolvedThisPointcut = str_replace('$this', \get_class($aspect), $pointcutExpression); $stream = $this->pointcutLexer->lex($resolvedThisPointcut); @@ -97,12 +92,16 @@ private function makeLexicalAnalyze(Aspect $aspect, $reflection, string $pointcu /** * Performs parsing of pointcut * - * @param ReflectionMethod|ReflectionProperty $reflection + * @param ReflectionMethod|ReflectionProperty|ReflectionClass $reflection + * @template T of Aspect * * @throws UnexpectedValueException */ - private function parseTokenStream($reflection, string $pointcutExpression, TokenStream $stream): PointFilter - { + private function parseTokenStream( + ReflectionMethod|ReflectionProperty|ReflectionClass $reflection, + string $pointcutExpression, + TokenStream $stream + ): Pointcut { try { $pointcut = $this->pointcutParser->parse($stream); } catch (UnexpectedTokenException $e) { diff --git a/src/Core/AdviceMatcher.php b/src/Core/AdviceMatcher.php index 9d51b1b8..7de7f272 100644 --- a/src/Core/AdviceMatcher.php +++ b/src/Core/AdviceMatcher.php @@ -13,12 +13,12 @@ namespace Go\Core; use Go\Aop; -use Go\Aop\IntroductionAdvisor; +use Go\Aop\IntroductionInfo; use Go\Aop\PointcutAdvisor; -use Go\Aop\PointFilter; -use Go\Aop\Support\NamespacedReflectionFunction; +use Go\Aop\Pointcut; use Go\ParserReflection\ReflectionFileNamespace; use ReflectionClass; +use ReflectionFunction; use ReflectionMethod; use ReflectionProperty; @@ -62,8 +62,8 @@ public function getAdvicesForFunctions(ReflectionFileNamespace $namespace, array foreach ($advisors as $advisorId => $advisor) { if ($advisor instanceof PointcutAdvisor) { $pointcut = $advisor->getPointcut(); - $isFunctionAdvisor = $pointcut->getKind() & PointFilter::KIND_FUNCTION; - if ($isFunctionAdvisor && $pointcut->getClassFilter()->matches($namespace)) { + $isFunctionAdvisor = $pointcut->getKind() & Pointcut::KIND_FUNCTION; + if ($isFunctionAdvisor && $pointcut->matches($namespace)) { $advices[] = $this->getFunctionAdvicesFromAdvisor($namespace, $advisor, $advisorId, $pointcut); } } @@ -96,14 +96,12 @@ public function getAdvicesForClass(ReflectionClass $class, array $advisors): arr foreach ($advisors as $advisorId => $advisor) { if ($advisor instanceof PointcutAdvisor) { $pointcut = $advisor->getPointcut(); - if ($pointcut->getClassFilter()->matches($class)) { - $classAdvices[] = $this->getAdvicesFromAdvisor($originalClass, $advisor, $advisorId, $pointcut); + if (($pointcut->getKind() & Pointcut::KIND_CLASS) && $pointcut->matches($class)) { + $classAdvices[] = $this->getClassAdvicesFromAdvisor($originalClass, $advisor, $advisorId, $pointcut); } - } - if ($advisor instanceof IntroductionAdvisor) { - if ($advisor->getClassFilter()->matches($class)) { - $classAdvices[] = $this->getIntroductionFromAdvisor($originalClass, $advisor); + if ($pointcut->matches($class)) { + $classAdvices[] = $this->getClassLevelAdvicesFromAdvisor($originalClass, $advisor, $advisorId, $pointcut); } } } @@ -115,33 +113,48 @@ public function getAdvicesForClass(ReflectionClass $class, array $advisors): arr } /** - * Returns list of advices from advisor and point filter + * Returns list of class advices from advisor and point filter */ - private function getAdvicesFromAdvisor( + private function getClassAdvicesFromAdvisor( ReflectionClass $class, PointcutAdvisor $advisor, string $advisorId, - PointFilter $filter + Pointcut $pointcut ): array { $classAdvices = []; - $filterKind = $filter->getKind(); - - // Check class only for class filters - if (($filterKind & PointFilter::KIND_CLASS) !== 0) { - if ($filter->matches($class)) { - // Dynamic initialization - if (($filterKind & PointFilter::KIND_INIT) !== 0) { - $classAdvices[AspectContainer::INIT_PREFIX]['root'][$advisorId] = $advisor->getAdvice(); - } - // Static initalization - if (($filterKind & PointFilter::KIND_STATIC_INIT) !== 0) { - $classAdvices[AspectContainer::STATIC_INIT_PREFIX]['root'][$advisorId] = $advisor->getAdvice(); - } - } + $pointcutKind = $pointcut->getKind(); + $advice = $advisor->getAdvice(); + + // Dynamic initialization (creation of instance with new) + if (($pointcutKind & Pointcut::KIND_INIT) !== 0) { + $classAdvices[AspectContainer::INIT_PREFIX]['root'][$advisorId] = $advice; } + // Static initalization (when class just loaded) + if (($pointcutKind & Pointcut::KIND_STATIC_INIT) !== 0) { + $classAdvices[AspectContainer::STATIC_INIT_PREFIX]['root'][$advisorId] = $advice; + } + // Introduction which can add interfaces or traits + if (($pointcutKind & Pointcut::KIND_INTRODUCTION) !== 0 && $advice instanceof IntroductionInfo && !$class->isTrait()) { + $classAdvices = [...$this->getIntroductionAdvices($advice)]; + } + + return $classAdvices; + } + + /** + * Returns list of advices from advisor and point filter + */ + private function getClassLevelAdvicesFromAdvisor( + ReflectionClass $class, + PointcutAdvisor $advisor, + string $advisorId, + Pointcut $pointcut + ): array { + $classAdvices = []; + $pointcutKind = $pointcut->getKind(); // Check methods in class only for method filters - if (($filterKind & PointFilter::KIND_METHOD) !== 0) { + if (($pointcutKind & Pointcut::KIND_METHOD) !== 0) { $mask = ReflectionMethod::IS_PUBLIC | ReflectionMethod::IS_PROTECTED; foreach ($class->getMethods($mask) as $method) { // abstract and parent final methods could not be woven @@ -150,7 +163,7 @@ private function getAdvicesFromAdvisor( continue; } - if ($filter->matches($method, $class)) { + if ($pointcut->matches($class, $method)) { $prefix = $method->isStatic() ? AspectContainer::STATIC_METHOD_PREFIX : AspectContainer::METHOD_PREFIX; $classAdvices[$prefix][$method->name][$advisorId] = $advisor->getAdvice(); } @@ -158,10 +171,10 @@ private function getAdvicesFromAdvisor( } // Check properties in class only for property filters - if (($filterKind & PointFilter::KIND_PROPERTY) !== 0) { + if (($pointcutKind & Pointcut::KIND_PROPERTY) !== 0) { $mask = ReflectionProperty::IS_PUBLIC | ReflectionProperty::IS_PROTECTED | ReflectionProperty::IS_PRIVATE; foreach ($class->getProperties($mask) as $property) { - if ($filter->matches($property, $class) && !$property->isStatic()) { + if ($pointcut->matches($class, $property) && !$property->isStatic()) { $classAdvices[AspectContainer::PROPERTY_PREFIX][$property->name][$advisorId] = $advisor->getAdvice(); } } @@ -173,20 +186,11 @@ private function getAdvicesFromAdvisor( /** * Returns list of introduction advices from advisor * - * @return Aop\IntroductionInfo[][][] + * @return IntroductionInfo[][][] */ - private function getIntroductionFromAdvisor( - ReflectionClass $class, - IntroductionAdvisor $advisor - ): array { + private function getIntroductionAdvices(IntroductionInfo $introduction): array { $classAdvices = []; - // Do not make introduction for traits - if ($class->isTrait()) { - return $classAdvices; - } - /** @var Aop\IntroductionInfo $introduction */ - $introduction = $advisor->getAdvice(); $introducedTrait = $introduction->getTrait(); if (!empty($introducedTrait)) { $introducedTrait = '\\' . ltrim($introducedTrait, '\\'); @@ -210,18 +214,18 @@ private function getFunctionAdvicesFromAdvisor( ReflectionFileNamespace $namespace, PointcutAdvisor $advisor, string $advisorId, - PointFilter $pointcut + Pointcut $pointcut ): array { $functions = []; $advices = []; $listOfGlobalFunctions = get_defined_functions(); foreach ($listOfGlobalFunctions['internal'] as $functionName) { - $functions[$functionName] = new NamespacedReflectionFunction($functionName, $namespace->getName()); + $functions[$functionName] = new ReflectionFunction($functionName); } foreach ($functions as $functionName => $function) { - if ($pointcut->matches($function, $namespace)) { + if ($pointcut->matches($namespace, $function)) { $advices[AspectContainer::FUNCTION_PREFIX][$functionName][$advisorId] = $advisor->getAdvice(); } } diff --git a/src/Core/AspectLoaderExtension.php b/src/Core/AspectLoaderExtension.php index 96894d24..d6760dd9 100644 --- a/src/Core/AspectLoaderExtension.php +++ b/src/Core/AspectLoaderExtension.php @@ -25,10 +25,12 @@ interface AspectLoaderExtension /** * Loads definition from specific point of aspect into the container * - * @param Aspect $aspect Instance of aspect - * @param ReflectionClass $reflectionAspect Reflection of aspect + * @param Aspect&T $aspect Instance of aspect + * @param ReflectionClass $reflectionAspect Reflection of aspect * * @return array|array + * + * @template T of Aspect */ public function load(Aspect $aspect, ReflectionClass $reflectionAspect): array; } diff --git a/src/Core/AttributeAspectLoaderExtension.php b/src/Core/AttributeAspectLoaderExtension.php index 8a26ffd0..090037e7 100644 --- a/src/Core/AttributeAspectLoaderExtension.php +++ b/src/Core/AttributeAspectLoaderExtension.php @@ -20,8 +20,7 @@ use Go\Aop\Framework\AroundInterceptor; use Go\Aop\Framework\BeforeInterceptor; use Go\Aop\Intercept\Interceptor; -use Go\Aop\Pointcut; -use Go\Aop\Support\DefaultPointcutAdvisor; +use Go\Aop\Support\GenericPointcutAdvisor; use Go\Lang\Attribute; use Go\Lang\Attribute\After; use Go\Lang\Attribute\AfterThrowing; @@ -29,6 +28,7 @@ use Go\Lang\Attribute\AbstractInterceptor; use Go\Lang\Attribute\Before; use ReflectionClass; +use ReflectionMethod; use UnexpectedValueException; use function get_class; @@ -38,15 +38,6 @@ */ class AttributeAspectLoaderExtension extends AbstractAspectLoaderExtension { - /** - * Loads definition from specific point of aspect into the container - * - * @param ReflectionClass $reflectionAspect Reflection of point - * - * @return array|array - * - * @throws UnexpectedValueException - */ public function load(Aspect $aspect, ReflectionClass $reflectionAspect): array { $loadedItems = []; @@ -59,11 +50,10 @@ public function load(Aspect $aspect, ReflectionClass $reflectionAspect): array if ($attribute instanceof Attribute\Pointcut) { $loadedItems[$methodId] = $this->parsePointcut($aspect, $reflectionAspect, $attribute->expression); } elseif ($attribute instanceof Attribute\AbstractInterceptor) { - $pointcut = $this->parsePointcut($aspect, $reflectionAspect, $attribute->expression); - $adviceCallback = $aspectMethod->getClosure($aspect); - $interceptor = $this->getInterceptor($attribute, $adviceCallback); + $pointcut = $this->parsePointcut($aspect, $reflectionAspect, $attribute->expression); + $interceptor = $this->getAdvice($attribute, $aspect, $aspectMethod); - $loadedItems[$methodId] = new DefaultPointcutAdvisor($pointcut, $interceptor); + $loadedItems[$methodId] = new GenericPointcutAdvisor($pointcut, $interceptor); } else { throw new UnexpectedValueException('Unsupported attribute class: ' . get_class($attribute)); } @@ -74,29 +64,26 @@ public function load(Aspect $aspect, ReflectionClass $reflectionAspect): array } /** - * Returns an interceptor instance by meta-type attribute and closure + * Returns an advice (interceptor) instance by meta-type attribute and closure * * @throws UnexpectedValueException For unsupported annotations */ - protected function getInterceptor(AbstractInterceptor $interceptorAttribute, Closure $adviceCallback): Interceptor - { + protected function getAdvice( + AbstractInterceptor $interceptorAttribute, + Aspect $aspect, + ReflectionMethod $aspectMethod + ): Interceptor { + $adviceCallback = $aspectMethod->getClosure($aspect); + assert($adviceCallback instanceof Closure, "getClosure should always return Closure"); + $adviceOrder = $interceptorAttribute->order; $pointcutExpression = $interceptorAttribute->expression; - switch (true) { - case ($interceptorAttribute instanceof Before): - return new BeforeInterceptor($adviceCallback, $adviceOrder, $pointcutExpression); - - case ($interceptorAttribute instanceof After): - return new AfterInterceptor($adviceCallback, $adviceOrder, $pointcutExpression); - - case ($interceptorAttribute instanceof Around): - return new AroundInterceptor($adviceCallback, $adviceOrder, $pointcutExpression); - - case ($interceptorAttribute instanceof AfterThrowing): - return new AfterThrowingInterceptor($adviceCallback, $adviceOrder, $pointcutExpression); - - default: - throw new UnexpectedValueException('Unsupported method meta class: ' . get_class($interceptorAttribute)); - } + return match (true) { + $interceptorAttribute instanceof Before => new BeforeInterceptor($adviceCallback, $adviceOrder, $pointcutExpression), + $interceptorAttribute instanceof After => new AfterInterceptor($adviceCallback, $adviceOrder, $pointcutExpression), + $interceptorAttribute instanceof Around => new AroundInterceptor($adviceCallback, $adviceOrder, $pointcutExpression), + $interceptorAttribute instanceof AfterThrowing => new AfterThrowingInterceptor($adviceCallback, $adviceOrder, $pointcutExpression), + default => throw new UnexpectedValueException('Unsupported method meta class: ' . get_class($interceptorAttribute)), + }; } } diff --git a/src/Core/IntroductionAspectExtension.php b/src/Core/IntroductionAspectExtension.php index 44cab712..ef35b03c 100644 --- a/src/Core/IntroductionAspectExtension.php +++ b/src/Core/IntroductionAspectExtension.php @@ -12,16 +12,17 @@ namespace Go\Core; -use Go\Aop\Advisor; +use Go\Aop\Advice; use Go\Aop\Aspect; use Go\Aop\Framework\DeclareErrorInterceptor; use Go\Aop\Framework\TraitIntroductionInfo; use Go\Aop\Pointcut; -use Go\Aop\Support\DeclareParentsAdvisor; -use Go\Aop\Support\DefaultPointcutAdvisor; +use Go\Aop\Support\GenericPointcutAdvisor; +use Go\Lang\Attribute\AbstractAttribute; use Go\Lang\Attribute\DeclareError; use Go\Lang\Attribute\DeclareParents; use ReflectionClass; +use ReflectionProperty; use UnexpectedValueException; /** @@ -29,16 +30,7 @@ */ class IntroductionAspectExtension extends AbstractAspectLoaderExtension { - /** - * Loads definition from specific point of aspect into the container - * - * @param Aspect $aspect Instance of aspect - * @param ReflectionClass $reflectionAspect Reflection of point - * - * @return array|array - * - * @throws UnexpectedValueException - */ + public function load(Aspect $aspect, ReflectionClass $reflectionAspect): array { $loadedItems = []; @@ -50,22 +42,20 @@ public function load(Aspect $aspect, ReflectionClass $reflectionAspect): array $attribute = $reflectionAttribute->newInstance(); if ($attribute instanceof DeclareParents) { $pointcut = $this->parsePointcut($aspect, $aspectProperty, $attribute->expression); - - $implement = $attribute->trait; - $interface = $attribute->interface; - $introductionInfo = new TraitIntroductionInfo($implement, $interface); - $advisor = new DeclareParentsAdvisor($pointcut, $introductionInfo); + // Introduction doesn't have own syntax and uses any suitable class-filter + $pointcut = new Pointcut\AndPointcut( + Pointcut::KIND_INTRODUCTION | Pointcut::KIND_CLASS, + $pointcut + ); + $advice = $this->getAdvice($attribute, $aspect, $aspectProperty); + $advisor = new GenericPointcutAdvisor($pointcut, $advice); $loadedItems[$propertyId] = $advisor; } elseif ($attribute instanceof DeclareError) { $pointcut = $this->parsePointcut($aspect, $reflectionAspect, $attribute->expression); + $advice = $this->getAdvice($attribute, $aspect, $aspectProperty); - $errorMessage = $aspectProperty->getValue($aspect); - $errorLevel = $attribute->level; - $introductionInfo = new DeclareErrorInterceptor($errorMessage, $errorLevel, $attribute->expression); - $loadedItems[$propertyId] = new DefaultPointcutAdvisor($pointcut, $introductionInfo); - break; - + $loadedItems[$propertyId] = new GenericPointcutAdvisor($pointcut, $advice); } else { throw new UnexpectedValueException('Unsupported attribute class: ' . get_class($attribute)); } @@ -74,4 +64,28 @@ public function load(Aspect $aspect, ReflectionClass $reflectionAspect): array return $loadedItems; } + + /** + * Returns an interceptor instance by meta-type attribute and closure + * + * @throws UnexpectedValueException For unsupported annotations + */ + protected function getAdvice( + AbstractAttribute $interceptorAttribute, + Aspect $aspect, + ReflectionProperty $aspectProperty + ): Advice { + $pointcutExpression = $interceptorAttribute->expression; + switch (true) { + case ($interceptorAttribute instanceof DeclareError): + $errorMessage = $aspectProperty->getDefaultValue(); + return new DeclareErrorInterceptor($errorMessage, $interceptorAttribute->level, $pointcutExpression); + + case ($interceptorAttribute instanceof DeclareParents): + return new TraitIntroductionInfo($interceptorAttribute->trait, $interceptorAttribute->interface); + + default: + throw new UnexpectedValueException('Unsupported attribute class: ' . get_class($interceptorAttribute)); + } + } } diff --git a/src/Lang/Attribute/DeclareError.php b/src/Lang/Attribute/DeclareError.php index 989db4f3..6666fa9c 100644 --- a/src/Lang/Attribute/DeclareError.php +++ b/src/Lang/Attribute/DeclareError.php @@ -22,7 +22,7 @@ class DeclareError extends AbstractAttribute { /** * @inheritdoc - * @param int $level Error level to generate + * @param int&(\E_USER_NOTICE|\E_USER_WARNING|\E_USER_ERROR|\E_USER_DEPRECATED) $level Default level of error, only E_USER_* constants */ public function __construct( string $expression, diff --git a/tests/Go/Aop/Support/AndPointFilterTest.php b/tests/Go/Aop/Pointcut/AndPointcutTest.php similarity index 50% rename from tests/Go/Aop/Support/AndPointFilterTest.php rename to tests/Go/Aop/Pointcut/AndPointcutTest.php index 8ccf19a6..f747990a 100644 --- a/tests/Go/Aop/Support/AndPointFilterTest.php +++ b/tests/Go/Aop/Pointcut/AndPointcutTest.php @@ -10,45 +10,49 @@ * with this source code in the file LICENSE. */ -namespace Go\Aop\Support; +namespace Go\Aop\Pointcut; -use Go\Aop\PointFilter; +use Go\Aop\Pointcut; use PHPUnit\Framework\TestCase; use ReflectionClass; -class AndPointFilterTest extends TestCase +class AndPointcutTest extends TestCase { /** * Tests that filter intersect different kinds of filters */ public function testKindIsIntersected(): void { - $first = $this->createMock(PointFilter::class); + $first = $this->createMock(Pointcut::class); $first ->method('getKind') - ->willReturn(PointFilter::KIND_METHOD | PointFilter::KIND_PROPERTY); + ->willReturn(Pointcut::KIND_METHOD | Pointcut::KIND_PROPERTY); - $second = $this->createMock(PointFilter::class); + $second = $this->createMock(Pointcut::class); $second ->method('getKind') - ->willReturn(PointFilter::KIND_METHOD | PointFilter::KIND_FUNCTION); + ->willReturn(Pointcut::KIND_METHOD | Pointcut::KIND_FUNCTION); - $filter = new AndPointFilter($first, $second); - $this->assertEquals(PointFilter::KIND_METHOD, $filter->getKind()); + $filter = new AndPointcut(null, $first, $second); + $this->assertEquals(Pointcut::KIND_METHOD, $filter->getKind()); } #[\PHPUnit\Framework\Attributes\DataProvider('logicCases')] - public function testMatches(PointFilter $first, PointFilter $second, $expected): void + public function testMatches(Pointcut $first, Pointcut $second, bool $expected): void { - $filter = new AndPointFilter($first, $second); - $result = $filter->matches(new ReflectionClass(__CLASS__) /* anything */); + $filter = new AndPointcut(null, $first, $second); + $result = $filter->matches( + new ReflectionClass(self::class), + new \ReflectionMethod(self::class, __FUNCTION__), + /* anything */ + ); $this->assertSame($expected, $result); } public static function logicCases(): array { - $true = TruePointFilter::getInstance(); - $false = new NotPointFilter($true); + $true = new TruePointcut(); + $false = new NotPointcut($true); return [ [$false, $false, false], [$false, $true, false], diff --git a/tests/Go/Aop/Pointcut/AttributePointcutTest.php b/tests/Go/Aop/Pointcut/AttributePointcutTest.php new file mode 100644 index 00000000..5b7b6a91 --- /dev/null +++ b/tests/Go/Aop/Pointcut/AttributePointcutTest.php @@ -0,0 +1,136 @@ + + * + * This source file is subject to the license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Go\Aop\Pointcut; + +use Go\Aop\Pointcut; +use Go\Stubs\First; +use Go\Stubs\StubAttribute; +use PHPUnit\Framework\TestCase; +use ReflectionClass; +use ReflectionMethod; +use ReflectionProperty; + +class AttributePointcutTest extends TestCase +{ + public function testMatchesClassWithAttribute(): void + { + $pointcut = new AttributePointcut( + Pointcut::KIND_CLASS, + StubAttribute::class, + true + ); + + $matched = $pointcut->matches(new ReflectionClass(First::class)); + $this->assertTrue($matched, "Attribute pointcut should match class statically with attribute"); + + // When context matching is enabled, it should also match any methods based only on context matching, ignoring ref name. + $matched = $pointcut->matches( + new ReflectionClass(First::class), + new ReflectionMethod(First::class, 'publicMethod') + ); + $this->assertTrue($matched, "Pointcut should match this method because annotation is matched"); + } + + public function testDoesntMatchClassWithoutAttribute(): void + { + $pointcut = new AttributePointcut( + Pointcut::KIND_CLASS, + StubAttribute::class, + true + ); + + $matched = $pointcut->matches(new ReflectionClass(self::class)); + $this->assertFalse($matched, "Attribute pointcut should not match class statically without attribute"); + } + + public function testMatchesMethodWithAttribute(): void + { + $pointcut = new AttributePointcut( + Pointcut::KIND_METHOD, + StubAttribute::class, + ); + + // With one argument it should match statically with any given context + $matched = $pointcut->matches(new ReflectionClass(First::class)); + $this->assertTrue($matched, "Pointcut should match this class statically even without attribute"); + + $matched = $pointcut->matches(new ReflectionClass(self::class)); + $this->assertTrue($matched, "Pointcut should match this class statically even without attribute"); + + $matched = $pointcut->matches( + new ReflectionClass(First::class), + new ReflectionMethod(First::class, 'publicMethodWithAttribute') + ); + $this->assertTrue($matched, "Pointcut should match this method because annotation is matched"); + } + + public function testDoesntMatchMethodWithoutAttribute(): void + { + $pointcut = new AttributePointcut( + Pointcut::KIND_METHOD, + StubAttribute::class, + ); + + $matched = $pointcut->matches( + new ReflectionClass(First::class), + new ReflectionMethod(First::class, 'publicMethod') + ); + $this->assertFalse($matched, "Pointcut should not match this method because annotation is not matched"); + } + + public function testMatchesPropertyWithAttribute(): void + { + $pointcut = new AttributePointcut( + Pointcut::KIND_PROPERTY, + StubAttribute::class, + ); + + // With one argument it should match statically with any given context + $matched = $pointcut->matches(new ReflectionClass(First::class)); + $this->assertTrue($matched, "Pointcut should match this class statically even without reflector"); + + $matched = $pointcut->matches(new ReflectionClass(self::class)); + $this->assertTrue($matched, "Pointcut should match this class statically even without reflector"); + + $matched = $pointcut->matches( + new ReflectionClass(First::class), + new ReflectionProperty(First::class, 'publicWithAttribute') + ); + $this->assertTrue($matched, "Pointcut should match this property because annotation is matched"); + } + + public function testDoesntMatchPropertyWithoutAttribute(): void + { + $pointcut = new AttributePointcut( + Pointcut::KIND_PROPERTY, + StubAttribute::class, + ); + + $matched = $pointcut->matches( + new ReflectionClass(First::class), + new ReflectionProperty(First::class, 'public') + ); + $this->assertFalse($matched, "Pointcut should not match this property because annotation is not matched"); + } + + public function testGetKind(): void + { + $pointcut = new AttributePointcut( + Pointcut::KIND_CLASS, + StubAttribute::class, + true + ); + + $this->assertEquals(Pointcut::KIND_CLASS, $pointcut->getKind()); + } +} diff --git a/tests/Go/Aop/Pointcut/ClassInheritancePointcutTest.php b/tests/Go/Aop/Pointcut/ClassInheritancePointcutTest.php new file mode 100644 index 00000000..dda2881d --- /dev/null +++ b/tests/Go/Aop/Pointcut/ClassInheritancePointcutTest.php @@ -0,0 +1,57 @@ + + * + * This source file is subject to the license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Go\Aop\Pointcut; + +use Go\Aop\Pointcut; +use Go\ParserReflection\ReflectionFileNamespace; +use Go\Stubs\First; +use Go\Stubs\FirstStatic; +use PHPUnit\Framework\TestCase; +use ReflectionClass; + +/** + * Class ClassInheritancePointcutTest. + * + * Testing ClassInheritancePointcut functionality. + */ +class ClassInheritancePointcutTest extends TestCase +{ + public function testNonClassContextIsNotMatches(): void + { + $pointcut = new ClassInheritancePointcut(static::class); + + $this->assertFalse($pointcut->matches( + new ReflectionFileNamespace(__FILE__, __NAMESPACE__) + )); + } + + public function testInheritedClassContextMatches(): void + { + $pointcut = new ClassInheritancePointcut(First::class); + + $this->assertTrue($pointcut->matches(new ReflectionClass(FirstStatic::class))); + } + + public function testNonInheritedClassContextDoesntMatches(): void + { + $pointcut = new ClassInheritancePointcut(\stdClass::class); + + $this->assertFalse($pointcut->matches(new ReflectionClass(FirstStatic::class))); + } + + public function testGetKindReturnsCorrectValue(): void + { + $pointcut = new ClassInheritancePointcut(self::class); + $this->assertSame(Pointcut::KIND_CLASS, $pointcut->getKind()); + } +} \ No newline at end of file diff --git a/tests/Go/Aop/Pointcut/MagicMethodDynamicPointcutTest.php b/tests/Go/Aop/Pointcut/MagicMethodDynamicPointcutTest.php new file mode 100644 index 00000000..a2f14b09 --- /dev/null +++ b/tests/Go/Aop/Pointcut/MagicMethodDynamicPointcutTest.php @@ -0,0 +1,136 @@ + + * + * This source file is subject to the license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Go\Aop\Pointcut; + +use Go\Aop\Pointcut; +use Go\ParserReflection\ReflectionFileNamespace; +use Go\Stubs\ClassWithMagicMethods; +use PHPUnit\Framework\TestCase; +use ReflectionClass; +use ReflectionMethod; +use ReflectionProperty; + +class MagicMethodDynamicPointcutTest extends TestCase +{ + public function testMatchesExactDynamicMethodName(): void + { + $pointcut = new MagicMethodDynamicPointcut('test'); + + // Statically should match any class with magic methods inside + $matched = $pointcut->matches(new ReflectionClass(ClassWithMagicMethods::class)); + $this->assertTrue($matched, "MagicMethodDynamicPointcut should match classes with magic methods"); + + // Pointcut should statically match __call magic method in the class + $matched = $pointcut->matches( + new ReflectionClass(ClassWithMagicMethods::class), + new ReflectionMethod(ClassWithMagicMethods::class, '__call') + ); + $this->assertTrue($matched, "Pointcut should match __call method because it is magic"); + + // Pointcut should statically match __callStatic magic method in the class + $matched = $pointcut->matches( + new ReflectionClass(ClassWithMagicMethods::class), + new ReflectionMethod(ClassWithMagicMethods::class, '__callStatic') + ); + $this->assertTrue($matched, "Pointcut should match __callStatic method because it is magic"); + + // During dynamic matching, it should match arguments from corresponding magic calls + $matched = $pointcut->matches( + new ReflectionClass(ClassWithMagicMethods::class), + new ReflectionMethod(ClassWithMagicMethods::class, '__call'), + new ClassWithMagicMethods(), + ['test'] + ); + $this->assertTrue($matched, "Pointcut should dynamically match 'test' method because it matches"); + } + + public function testDoesntMatchExactDynamicMethodName(): void + { + $pointcut = new MagicMethodDynamicPointcut('another'); + + // During dynamic matching, it should not match dynamic method name + $matched = $pointcut->matches( + new ReflectionClass(ClassWithMagicMethods::class), + new ReflectionMethod(ClassWithMagicMethods::class, '__call'), + new ClassWithMagicMethods(), + ['test'] + ); + $this->assertFalse($matched, "Pointcut should not dynamically match 'test' method because we expect 'another'"); + } + + + public function testDoesntMatchWrongContextOrReflectorGiven(): void + { + $pointcut = new MagicMethodDynamicPointcut('test'); + + // Unsupported context (ReflectionFileNamespace) + $matched = $pointcut->matches(new ReflectionFileNamespace(__FILE__, __NAMESPACE__)); + $this->assertFalse($matched, "MagicMethodDynamicPointcut should not match ReflectionFileNamespace statically"); + + // Non-magic static method + $matched = $pointcut->matches( + new ReflectionClass(ClassWithMagicMethods::class), + new ReflectionMethod(ClassWithMagicMethods::class, 'notMagicMethod') + ); + $this->assertFalse($matched, "MagicMethodDynamicPointcut should not match non-magic method"); + + // Attempt to match property with magic name + $matched = $pointcut->matches( + new ReflectionClass(ClassWithMagicMethods::class), + new ReflectionProperty(ClassWithMagicMethods::class, '__call') + ); + $this->assertFalse($matched, "MagicMethodDynamicPointcut should not match property with magic name"); + + // Pointcut should not match statically for __callMe magic method in the class + $matched = $pointcut->matches( + new ReflectionClass(ClassWithMagicMethods::class), + new ReflectionMethod(ClassWithMagicMethods::class, '__callMe') + ); + $this->assertFalse($matched, "MagicMethodDynamicPointcut should not match __callMe method"); + + // During dynamic matching, attempt to match without arguments + $matched = $pointcut->matches( + new ReflectionClass(ClassWithMagicMethods::class), + new ReflectionMethod(ClassWithMagicMethods::class, '__call'), + new ClassWithMagicMethods(), + ); + $this->assertFalse($matched, "Pointcut should not dynamically match 'test' method without info about args"); + + // During dynamic matching, attempt to match with empty arguments + $matched = $pointcut->matches( + new ReflectionClass(ClassWithMagicMethods::class), + new ReflectionMethod(ClassWithMagicMethods::class, '__call'), + new ClassWithMagicMethods(), + [] + ); + $this->assertFalse($matched, "Pointcut should not dynamically match 'test' method without info about args"); + + // During dynamic matching, attempt to match arguments with wrong type + $matched = $pointcut->matches( + new ReflectionClass(ClassWithMagicMethods::class), + new ReflectionMethod(ClassWithMagicMethods::class, '__call'), + new ClassWithMagicMethods(), + [new \stdClass()] + ); + $this->assertFalse($matched, "Pointcut should not dynamically match 'test' method without info about args"); + + } + + public function testGetKind(): void + { + $pointcut = new MagicMethodDynamicPointcut('test'); + + $this->assertTrue(($pointcut->getKind() & Pointcut::KIND_DYNAMIC) > 0, 'Pointcut should be dynamic'); + $this->assertTrue(($pointcut->getKind() & Pointcut::KIND_METHOD) > 0, 'Pointcut should be for methods'); + } +} diff --git a/tests/Go/Aop/Pointcut/MatchInheritedPointcutTest.php b/tests/Go/Aop/Pointcut/MatchInheritedPointcutTest.php new file mode 100644 index 00000000..cca34f55 --- /dev/null +++ b/tests/Go/Aop/Pointcut/MatchInheritedPointcutTest.php @@ -0,0 +1,104 @@ + + * + * This source file is subject to the license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Go\Aop\Pointcut; + +use Go\Aop\Pointcut; +use Go\ParserReflection\ReflectionFileNamespace; +use Go\Stubs\ClassWithMagicMethods; +use Go\Stubs\FirstStatic; +use PHPUnit\Framework\TestCase; +use ReflectionClass; +use ReflectionFunction; +use ReflectionMethod; +use ReflectionProperty; + +class MatchInheritedPointcutTest extends TestCase +{ + public function testMatchesInheritedMethods(): void + { + $pointcut = new MatchInheritedPointcut(); + + // Statically should match any class + $matched = $pointcut->matches(new ReflectionClass(FirstStatic::class)); + $this->assertTrue($matched, "MatchInheritedPointcut should match any class statically"); + + // Pointcut should statically match dynamic parent method from the First::class + $matched = $pointcut->matches( + new ReflectionClass(FirstStatic::class), + new ReflectionMethod(FirstStatic::class, 'publicMethod') + ); + $this->assertTrue($matched, "MatchInheritedPointcut should match inherited `publicMethod` method"); + + // Pointcut should statically match static parent method from the First::class + $matched = $pointcut->matches( + new ReflectionClass(FirstStatic::class), + new ReflectionMethod(FirstStatic::class, 'staticSelfProtected') + ); + $this->assertTrue($matched, "MatchInheritedPointcut should match inherited `staticSelfProtected` method"); + } + + public function testMatchesInheritedProperties(): void + { + $pointcut = new MatchInheritedPointcut(); + + // Pointcut should statically match parent property from the First::class + $matched = $pointcut->matches( + new ReflectionClass(FirstStatic::class), + new ReflectionProperty(FirstStatic::class, 'public') + ); + $this->assertTrue($matched, "MatchInheritedPointcut should match inherited `public` property"); + + // Pointcut should statically match static protected parent property from the First::class + $matched = $pointcut->matches( + new ReflectionClass(FirstStatic::class), + new ReflectionProperty(FirstStatic::class, 'protected') + ); + $this->assertTrue($matched, "MatchInheritedPointcut should match inherited `protected` property"); + } + + public function testDoesntMatchNonInheritedMethods(): void + { + $pointcut = new MatchInheritedPointcut(); + + // Pointcut should not statically match method from the FirstStatic::class itself + $matched = $pointcut->matches( + new ReflectionClass(FirstStatic::class), + new ReflectionMethod(FirstStatic::class, 'init') + ); + $this->assertFalse($matched, "MatchInheritedPointcut should not match declared `init` method"); + } + + public function testDoesntMatchWrongContext(): void + { + $pointcut = new MatchInheritedPointcut(); + + // Unsupported context (ReflectionFileNamespace) + $matched = $pointcut->matches(new ReflectionFileNamespace(__FILE__, __NAMESPACE__)); + $this->assertFalse($matched, "MatchInheritedPointcut should not match ReflectionFileNamespace statically"); + + // Attempt to match function + $matched = $pointcut->matches( + new ReflectionClass(ClassWithMagicMethods::class), + new ReflectionFunction('var_dump') + ); + $this->assertFalse($matched, "MatchInheritedPointcut should not match function as reflector"); + } + + public function testGetKind(): void + { + $pointcut = new MatchInheritedPointcut(); + + $this->assertTrue(($pointcut->getKind() & Pointcut::KIND_PROPERTY) > 0, 'Pointcut should be for properties'); + $this->assertTrue(($pointcut->getKind() & Pointcut::KIND_METHOD) > 0, 'Pointcut should be for methods'); + } +} diff --git a/tests/Go/Aop/Pointcut/ModifierPointcutTest.php b/tests/Go/Aop/Pointcut/ModifierPointcutTest.php new file mode 100644 index 00000000..61954e58 --- /dev/null +++ b/tests/Go/Aop/Pointcut/ModifierPointcutTest.php @@ -0,0 +1,129 @@ + + * + * This source file is subject to the license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Go\Aop\Pointcut; + +use Go\Aop\Pointcut; +use Go\Stubs\FirstStatic; +use PHPUnit\Framework\TestCase; +use ReflectionClass; +use ReflectionFunction; +use ReflectionMethod; + +class ModifierPointcutTest extends TestCase +{ + private ModifierPointcut $pointcut; + + protected function setUp(): void + { + $this->pointcut = new ModifierPointcut(); + } + + /** + * @param ReflectionClass $context + */ + #[\PHPUnit\Framework\Attributes\DataProvider('reflectorProvider')] + public function testMatchesModifiers( + int $orMask, + int $andMask, + int $notMask, + ReflectionClass $context, + ReflectionMethod $reflector, + ): void { + if ($orMask > 0) { + $this->pointcut->orMatch($orMask); + } + if ($andMask > 0) { + $this->pointcut->andMatch($andMask); + } + if ($notMask > 0) { + $this->pointcut->notMatch($notMask); + } + + $modifiers = $reflector->getModifiers(); + + // If "not" isset and matches at least one modifier, this should never match at all + if ($notMask & $modifiers) { + $this->assertFalse($this->pointcut->matches($context, $reflector)); + } elseif ($orMask & $modifiers) { + // If "or" mask is set, it is enough to match with at least one modifier + $this->assertTrue($this->pointcut->matches($context, $reflector)); + } elseif ($andMask === ($andMask & $modifiers)) { + // Otherwise we have strict "AND" comparison that should match + $this->assertTrue($this->pointcut->matches($context, $reflector)); + } elseif ($andMask !== ($andMask & $modifiers)) { + // But if mask for "AND" is not equal itself, then we have strict comparison that should not match + $this->assertFalse($this->pointcut->matches($context, $reflector)); + } else { + $this->fail('Unknown logical combination of modifiers'); + } + } + + public static function reflectorProvider(): \Generator + { + $maskMatrix = [ + 0, + ReflectionMethod::IS_PUBLIC, + ReflectionMethod::IS_PROTECTED, + ReflectionMethod::IS_PRIVATE, + ReflectionMethod::IS_STATIC, + ReflectionMethod::IS_FINAL, + ]; + $reflectionClass = new ReflectionClass(FirstStatic::class); + + // We can store known modifiers to avoid extra loops for known modifiers + $knownModifiers = []; + foreach ($reflectionClass->getMethods() as $reflectionMethod) { + $modifierMask = $reflectionMethod->getModifiers(); + if (in_array($modifierMask, $knownModifiers, true)) { + // let's skip method if we have already tested another method with same modifier mask + continue; + } else { + $knownModifiers[] = $modifierMask; + } + foreach ($maskMatrix as $orMask) { + foreach ($maskMatrix as $andMask) { + foreach ($maskMatrix as $notMask) { + $orName = $orMask ? "(OR=" . join('', \Reflection::getModifierNames($orMask)) . ")" : ''; + $andName = $andMask ? "(AND=" . join('', \Reflection::getModifierNames($andMask)) . ")" : ''; + $notName = $notMask ? "(NOT=" . join('', \Reflection::getModifierNames($notMask)) . ")" : ''; + $name = $reflectionMethod->getDeclaringClass()->getName() . '::' . $reflectionMethod->getName(); + $key = $name . $orName . $andName . $notName; + yield $key => [$orMask, $andMask, $notMask, $reflectionClass, $reflectionMethod]; + } + } + } + } + } + + public function testAlwaysMatchesWithoutReflectorInstance(): void + { + $reflectionClass = new ReflectionClass(FirstStatic::class); + $this->assertTrue($this->pointcut->matches($reflectionClass)); + } + + public function testNeverMatchesForFunctionModifiers(): void + { + $reflectionClass = new ReflectionClass(FirstStatic::class); + $this->pointcut->andMatch(ReflectionMethod::IS_PUBLIC); + + $this->assertFalse($this->pointcut->matches( + $reflectionClass, + new ReflectionFunction('var_dump') + )); + } + + public function testGetKind(): void + { + $this->assertSame(Pointcut::KIND_ALL, $this->pointcut->getKind()); + } +} diff --git a/tests/Go/Aop/Pointcut/NamePointcutTest.php b/tests/Go/Aop/Pointcut/NamePointcutTest.php new file mode 100644 index 00000000..e4783b1d --- /dev/null +++ b/tests/Go/Aop/Pointcut/NamePointcutTest.php @@ -0,0 +1,141 @@ + + * + * This source file is subject to the license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Go\Aop\Pointcut; + +use Go\Aop\Pointcut; +use Go\Stubs\First; +use PHPUnit\Framework\TestCase; +use ReflectionClass; +use ReflectionMethod; +use ReflectionProperty; + +class NamePointcutTest extends TestCase +{ + /** + * Tests that method matched by name directly + */ + public function testDirectMethodMatchByName(): void + { + $pointcut = new NamePointcut( + Pointcut::KIND_METHOD, + 'publicMethod' + ); + + $matched = $pointcut->matches(new ReflectionClass(First::class), new ReflectionMethod(First::class, 'publicMethod')); + $this->assertTrue($matched, "Pointcut should match this method"); + } + + /** + * Tests that pointcut can match property + */ + public function testCanMatchProperty(): void + { + $pointcut = new NamePointcut( + Pointcut::KIND_PROPERTY, + 'public' + ); + + $matched = $pointcut->matches(new ReflectionClass(First::class), new ReflectionProperty(First::class, 'public')); + $this->assertTrue($matched, "Pointcut should match this property"); + } + + /** + * Tests that pattern is working correctly + */ + public function testRegularPattern(): void + { + $pointcut = new NamePointcut( + Pointcut::KIND_METHOD, + '*Method' + ); + + $matched = $pointcut->matches(new ReflectionClass(First::class), new ReflectionMethod(First::class, 'publicMethod')); + $this->assertTrue($matched, "Pointcut should match this method"); + + $matched = $pointcut->matches(new ReflectionClass(First::class), new ReflectionMethod(First::class, 'protectedMethod')); + $this->assertTrue($matched, "Pointcut should match this method"); + } + + /** + * Tests that multiple pattern is matching + */ + public function testMultipleRegularPattern(): void + { + $pointcut = new NamePointcut( + Pointcut::KIND_METHOD, + 'publicMethod|protectedMethod' + ); + + $matched = $pointcut->matches(new ReflectionClass(First::class), new ReflectionMethod(First::class, 'publicMethod')); + $this->assertTrue($matched, "Pointcut should match this method"); + + $matched = $pointcut->matches(new ReflectionClass(First::class), new ReflectionMethod(First::class, 'protectedMethod')); + $this->assertTrue($matched, "Pointcut should match this method"); + } + + /** + * Tests that multiple pattern is using strict matching + * + * @link https://github.com/goaop/framework/issues/115 + */ + public function testIssue115(): void + { + $pointcut = new NamePointcut( + Pointcut::KIND_METHOD, + 'public|Public' + ); + + $matched = $pointcut->matches(new ReflectionClass(First::class), new ReflectionMethod(First::class, 'publicMethod')); + $this->assertFalse($matched, "Pointcut should match strict"); + + $matched = $pointcut->matches(new ReflectionClass(First::class), new ReflectionMethod(First::class, 'staticLsbPublic')); + $this->assertFalse($matched, "Pointcut should match strict"); + } + + public function testMatchesAnyContextWithoutReflector(): void + { + $pointcut = new NamePointcut( + Pointcut::KIND_METHOD, + '*Method' + ); + + $matched = $pointcut->matches(new ReflectionClass(First::class)); + $this->assertTrue($matched, "Name pointcut should match statically without reflector"); + } + + public function testMatchesGivenContextWhenContextMatchingIsEnabled(): void + { + $pointcut = new NamePointcut( + Pointcut::KIND_METHOD, + First::class, + true + ); + + $matched = $pointcut->matches(new ReflectionClass(First::class)); + $this->assertTrue($matched, "Name pointcut should match statically given class"); + + // When context matching is enabled, it matches any methods based only on context matching, ignoring ref name. + $matched = $pointcut->matches( + new ReflectionClass(First::class), + new ReflectionMethod(First::class, 'publicMethod') + ); + $this->assertTrue($matched, "Pointcut should match this method"); + } + + + public function testGetKind(): void + { + $pointcut = new NamePointcut(Pointcut::KIND_METHOD, '*Method'); + $this->assertSame(Pointcut::KIND_METHOD, $pointcut->getKind()); + } +} diff --git a/tests/Go/Aop/Pointcut/NotPointcutTest.php b/tests/Go/Aop/Pointcut/NotPointcutTest.php index c4f2138c..5214814c 100644 --- a/tests/Go/Aop/Pointcut/NotPointcutTest.php +++ b/tests/Go/Aop/Pointcut/NotPointcutTest.php @@ -1,10 +1,10 @@ + * @copyright Copyright 2014, Lisachenko Alexander * * This source file is subject to the license that is bundled * with this source code in the file LICENSE. @@ -12,21 +12,40 @@ namespace Go\Aop\Pointcut; +use Go\Aop\Pointcut; +use Go\Stubs\FirstStatic; use PHPUnit\Framework\TestCase; use ReflectionClass; +use ReflectionMethod; class NotPointcutTest extends TestCase { - protected NotPointcut $pointcut; + #[\PHPUnit\Framework\Attributes\DataProvider('logicCases')] + public function testMatches(Pointcut $first, bool $expected): void + { + $filter = new NotPointcut($first); + $result = $filter->matches( + new ReflectionClass(self::class), + new ReflectionMethod(self::class, __FUNCTION__) + ); + $this->assertSame($expected, $result); + } - public function setUp(): void + public static function logicCases(): \Generator { - $this->pointcut = new NotPointcut(new TruePointcut()); + $true = new TruePointcut(); + $false = new NotPointcut($true); + + yield [$false, true]; + yield [$true, false]; } - public function testItNeverMatchesForTruePointcut() + public function testAlwaysMatchesWithoutReflectorInstance(): void { - $this->assertFalse($this->pointcut->matches(null)); - $this->assertFalse($this->pointcut->matches(new ReflectionClass(self::class))); + $truePointcut = new TruePointcut(); + $falsePointcut = new NotPointcut($truePointcut); + + $reflectionClass = new ReflectionClass(FirstStatic::class); + $this->assertTrue($falsePointcut->matches($reflectionClass)); } } diff --git a/tests/Go/Aop/Support/OrPointFilterTest.php b/tests/Go/Aop/Pointcut/OrPointcutTest.php similarity index 50% rename from tests/Go/Aop/Support/OrPointFilterTest.php rename to tests/Go/Aop/Pointcut/OrPointcutTest.php index c8d4e6b3..aa8da18f 100644 --- a/tests/Go/Aop/Support/OrPointFilterTest.php +++ b/tests/Go/Aop/Pointcut/OrPointcutTest.php @@ -10,45 +10,51 @@ * with this source code in the file LICENSE. */ -namespace Go\Aop\Support; +namespace Go\Aop\Pointcut; -use Go\Aop\PointFilter; +use Go\Aop\Pointcut; use PHPUnit\Framework\TestCase; +use ReflectionClass; +use ReflectionMethod; -class OrPointFilterTest extends TestCase +class OrPointcutTest extends TestCase { /** * Tests that filter combined different kinds of filters */ public function testKindIsCombined(): void { - $first = $this->createMock(PointFilter::class); + $first = $this->createMock(Pointcut::class); $first ->method('getKind') - ->willReturn(PointFilter::KIND_METHOD | PointFilter::KIND_PROPERTY); + ->willReturn(Pointcut::KIND_METHOD | Pointcut::KIND_PROPERTY); - $second = $this->createMock(PointFilter::class); + $second = $this->createMock(Pointcut::class); $second ->method('getKind') - ->willReturn(PointFilter::KIND_METHOD | PointFilter::KIND_FUNCTION); + ->willReturn(Pointcut::KIND_METHOD | Pointcut::KIND_FUNCTION); - $filter = new OrPointFilter($first, $second); - $expected = PointFilter::KIND_METHOD | PointFilter::KIND_FUNCTION | PointFilter::KIND_PROPERTY; + $filter = new OrPointcut($first, $second); + $expected = Pointcut::KIND_METHOD | Pointcut::KIND_FUNCTION | Pointcut::KIND_PROPERTY; $this->assertEquals($expected, $filter->getKind()); } #[\PHPUnit\Framework\Attributes\DataProvider('logicCases')] - public function testMatches(PointFilter $first, PointFilter $second, bool $expected): void + public function testMatches(Pointcut $first, Pointcut $second, bool $expected): void { - $filter = new OrPointFilter($first, $second); - $result = $filter->matches(new \ReflectionClass(__CLASS__) /* anything */); + $filter = new OrPointcut($first, $second); + $result = $filter->matches( + new ReflectionClass(self::class), + new ReflectionMethod(self::class, __FUNCTION__) + /* anything */ + ); $this->assertSame($expected, $result); } public static function logicCases(): array { - $true = TruePointFilter::getInstance(); - $false = new NotPointFilter($true); + $true = new TruePointcut(); + $false = new NotPointcut($true); return [ [$false, $false, false], [$false, $true, true], diff --git a/tests/Go/Aop/Pointcut/PointcutParserTest.php b/tests/Go/Aop/Pointcut/PointcutParserTest.php index 6886e443..f6ae9674 100644 --- a/tests/Go/Aop/Pointcut/PointcutParserTest.php +++ b/tests/Go/Aop/Pointcut/PointcutParserTest.php @@ -87,9 +87,6 @@ public static function validPointcutDefinitions(): array // Parenthesis ['within(DemoInterface+) && ( within(**) || within(*) )'], - // Control flow execution pointcuts - ['cflowbelow(execution(public Example->method(*)))'], - // Function pointcut ['execution(Demo\*\Test\**\*(*))'], ['execution(Demo\Namespace\array_*_er(*))'], diff --git a/tests/Go/Aop/Pointcut/ReturnTypePointcutTest.php b/tests/Go/Aop/Pointcut/ReturnTypePointcutTest.php new file mode 100644 index 00000000..867acc08 --- /dev/null +++ b/tests/Go/Aop/Pointcut/ReturnTypePointcutTest.php @@ -0,0 +1,83 @@ +matches($context, $reflector); + + self::assertSame($expectedMatch, $result); + } + + public static function returnTypeMatchesDataProvider(): array + { + return [ + 'Exact match (int)' => ['int', new ReflectionFunction('strlen'), true], + 'Star match (bool)' => ['b*l', new ReflectionMethod(ReturnTypePointcut::class, 'matches'), true], + 'Question match (int)' => ['?nt', new ReflectionMethod(ReturnTypePointcut::class, 'getKind'), true], + 'No match (int)' => ['array', new ReflectionFunction('strlen'), false], + ]; + } + + public function testAlwaysMatchesWithoutReflectorInstance(): void + { + $pointcut = new ReturnTypePointcut('void'); + + $reflectionClass = new ReflectionClass(self::class); + $this->assertTrue($pointcut->matches($reflectionClass)); + } + + public function testNeverMatchesForReflectionProperties(): void + { + $pointcut = new ReturnTypePointcut('int'); + $reflectionClass = new ReflectionClass(First::class); + + $this->assertFalse($pointcut->matches( + $reflectionClass, + $reflectionClass->getProperty('public') + )); + } + + public function testNeverMatchesWithoutReturnType(): void + { + $pointcut = new ReturnTypePointcut('int'); + $reflectionClass = new ReflectionClass(Joinpoint::class); + + $this->assertFalse($pointcut->matches( + $reflectionClass, + $reflectionClass->getMethod('proceed') + )); + } + + public function testThrowsInvalidArgumentExceptionForEmptyType(): void + { + $this->expectException(InvalidArgumentException::class); + + new ReturnTypePointcut(''); + } + + public function testGetKind(): void + { + $pointcut = new ReturnTypePointcut('test'); + + $this->assertTrue(($pointcut->getKind() & Pointcut::KIND_FUNCTION) > 0, 'Pointcut should be for functions'); + $this->assertTrue(($pointcut->getKind() & Pointcut::KIND_METHOD) > 0, 'Pointcut should be for methods'); + } +} \ No newline at end of file diff --git a/tests/Go/Aop/Pointcut/SignaturePointcutTest.php b/tests/Go/Aop/Pointcut/SignaturePointcutTest.php deleted file mode 100644 index 2010f449..00000000 --- a/tests/Go/Aop/Pointcut/SignaturePointcutTest.php +++ /dev/null @@ -1,122 +0,0 @@ - - * - * This source file is subject to the license that is bundled - * with this source code in the file LICENSE. - */ - -namespace Go\Aop\Pointcut; - -use Go\Aop\PointFilter; -use Go\Aop\Support\NotPointFilter; -use Go\Aop\Support\TruePointFilter; -use Go\Stubs\First; -use PHPUnit\Framework\TestCase; -use ReflectionMethod; -use ReflectionProperty; - -class SignaturePointcutTest extends TestCase -{ - /** - * Tests that method matched by name directly - */ - public function testDirectMethodMatchByName(): void - { - $pointcut = new SignaturePointcut( - PointFilter::KIND_METHOD, - 'publicMethod', - TruePointFilter::getInstance() - ); - - $matched = $pointcut->matches(new ReflectionMethod(First::class, 'publicMethod')); - $this->assertTrue($matched, "Pointcut should match this method"); - } - - /** - * Tests that pointcut can match property - */ - public function testCanMatchProperty(): void - { - $pointcut = new SignaturePointcut( - PointFilter::KIND_METHOD, - 'public', - TruePointFilter::getInstance() - ); - - $matched = $pointcut->matches(new ReflectionProperty(First::class, 'public')); - $this->assertTrue($matched, "Pointcut should match this property"); - } - - /** - * Tests that pointcut won't match if modifier filter is not match - */ - public function testWontMatchModifier(): void - { - $trueInstance = TruePointFilter::getInstance(); - $notInstance = new NotPointFilter($trueInstance); - $pointcut = new SignaturePointcut(PointFilter::KIND_METHOD, 'publicMethod', $notInstance); - $matched = $pointcut->matches(new ReflectionMethod(First::class, 'publicMethod')); - $this->assertFalse($matched, "Pointcut should not match modifier"); - } - - /** - * Tests that pattern is working correctly - */ - public function testRegularPattern(): void - { - $pointcut = new SignaturePointcut( - PointFilter::KIND_METHOD, - '*Method', - TruePointFilter::getInstance() - ); - - $matched = $pointcut->matches(new ReflectionMethod(First::class, 'publicMethod')); - $this->assertTrue($matched, "Pointcut should match this method"); - - $matched = $pointcut->matches(new ReflectionMethod(First::class, 'protectedMethod')); - $this->assertTrue($matched, "Pointcut should match this method"); - } - - /** - * Tests that multiple pattern is matching - */ - public function testMultipleRegularPattern(): void - { - $pointcut = new SignaturePointcut( - PointFilter::KIND_METHOD, - 'publicMethod|protectedMethod', - TruePointFilter::getInstance() - ); - - $matched = $pointcut->matches(new ReflectionMethod(First::class, 'publicMethod')); - $this->assertTrue($matched, "Pointcut should match this method"); - - $matched = $pointcut->matches(new ReflectionMethod(First::class, 'protectedMethod')); - $this->assertTrue($matched, "Pointcut should match this method"); - } - - /** - * Tests that multiple pattern is using strict matching - * - * @link https://github.com/lisachenko/go-aop-php/issues/115 - */ - public function testIssue115(): void - { - $pointcut = new SignaturePointcut( - PointFilter::KIND_METHOD, - 'public|Public', - TruePointFilter::getInstance() - ); - - $matched = $pointcut->matches(new ReflectionMethod(First::class, 'publicMethod')); - $this->assertFalse($matched, "Pointcut should match strict"); - - $matched = $pointcut->matches(new ReflectionMethod(First::class, 'staticLsbPublic')); - $this->assertFalse($matched, "Pointcut should match strict"); - } -} diff --git a/tests/Go/Aop/Pointcut/TruePointcutTest.php b/tests/Go/Aop/Pointcut/TruePointcutTest.php index 24eb0cce..864faa5f 100644 --- a/tests/Go/Aop/Pointcut/TruePointcutTest.php +++ b/tests/Go/Aop/Pointcut/TruePointcutTest.php @@ -12,9 +12,10 @@ namespace Go\Aop\Pointcut; -use Go\Aop\PointFilter; -use Go\Aop\Support\TruePointFilter; +use Go\Aop\Pointcut; use PHPUnit\Framework\TestCase; +use ReflectionClass; +use ReflectionMethod; class TruePointcutTest extends TestCase { @@ -25,32 +26,32 @@ public function setUp(): void $this->pointcut = new TruePointcut(); } - public function testItAlwaysMatchesForAnything() + public function testItAlwaysMatchesForAnything(): void { - $this->assertTrue($this->pointcut->matches(null)); - $this->assertTrue($this->pointcut->matches(new \ReflectionClass(self::class))); + $this->assertTrue($this->pointcut->matches(new ReflectionClass(self::class))); + $this->assertTrue( + $this->pointcut->matches( + new ReflectionClass(self::class), + new ReflectionMethod(self::class, __FUNCTION__) + ) + ); } - public function testItMatchesWithDefaultKinds() + public function testItMatchesWithDefaultKinds(): void { $kind = $this->pointcut->getKind(); - $this->assertTrue((bool)($kind & PointFilter::KIND_METHOD)); - $this->assertTrue((bool)($kind & PointFilter::KIND_PROPERTY)); - $this->assertTrue((bool)($kind & PointFilter::KIND_CLASS)); - $this->assertTrue((bool)($kind & PointFilter::KIND_TRAIT)); - $this->assertTrue((bool)($kind & PointFilter::KIND_FUNCTION)); - $this->assertTrue((bool)($kind & PointFilter::KIND_INIT)); - $this->assertTrue((bool)($kind & PointFilter::KIND_STATIC_INIT)); + $this->assertTrue((bool)($kind & Pointcut::KIND_METHOD)); + $this->assertTrue((bool)($kind & Pointcut::KIND_PROPERTY)); + $this->assertTrue((bool)($kind & Pointcut::KIND_CLASS)); + $this->assertTrue((bool)($kind & Pointcut::KIND_TRAIT)); + $this->assertTrue((bool)($kind & Pointcut::KIND_FUNCTION)); + $this->assertTrue((bool)($kind & Pointcut::KIND_INIT)); + $this->assertTrue((bool)($kind & Pointcut::KIND_STATIC_INIT)); } - public function testItDoesNotMatchWithDynamicKindByDefault() + public function testItDoesNotMatchWithDynamicKindByDefault(): void { $kind = $this->pointcut->getKind(); - $this->assertFalse((bool)($kind & PointFilter::KIND_DYNAMIC)); - } - - public function testItUsesTruePointFilterForClass() - { - $this->assertInstanceOf(TruePointFilter::class, $this->pointcut->getClassFilter()); + $this->assertFalse((bool)($kind & Pointcut::KIND_DYNAMIC)); } } diff --git a/tests/Go/Aop/Support/NotPointFilterTest.php b/tests/Go/Aop/Support/NotPointFilterTest.php deleted file mode 100644 index 3a4eb934..00000000 --- a/tests/Go/Aop/Support/NotPointFilterTest.php +++ /dev/null @@ -1,38 +0,0 @@ - - * - * This source file is subject to the license that is bundled - * with this source code in the file LICENSE. - */ - -namespace Go\Aop\Support; - -use Go\Aop\PointFilter; -use PHPUnit\Framework\TestCase; -use ReflectionClass; - -class NotPointFilterTest extends TestCase -{ - #[\PHPUnit\Framework\Attributes\DataProvider('logicCases')] - public function testMatches(PointFilter $first, bool $expected): void - { - $filter = new NotPointFilter($first); - $result = $filter->matches(new ReflectionClass(self::class)); - $this->assertSame($expected, $result); - } - - public static function logicCases(): array - { - $true = TruePointFilter::getInstance(); - $false = new NotPointFilter($true); - return [ - [$false, true], - [$true, false] - ]; - } -} diff --git a/tests/Go/Aop/Support/TruePointFilterTest.php b/tests/Go/Aop/Support/TruePointFilterTest.php deleted file mode 100644 index 5a03ba5b..00000000 --- a/tests/Go/Aop/Support/TruePointFilterTest.php +++ /dev/null @@ -1,43 +0,0 @@ - - * - * This source file is subject to the license that is bundled - * with this source code in the file LICENSE. - */ - -namespace Go\Aop\Support; - -use PHPUnit\Framework\TestCase; -use ReflectionClass; - -/** - * TruePointFilter test case - */ -class TruePointFilterTest extends TestCase -{ - protected TruePointFilter $filter; - - /** - * Sets up the fixture, for example, opens a network connection. - * This method is called before a test is executed. - */ - protected function setUp(): void - { - $this->filter = TruePointFilter::getInstance(); - } - - /** - * Test that true matcher always matches the class - */ - public function testMatches(): void - { - // Works correctly with ReflectionClass - $class = new ReflectionClass(self::class); - $this->assertTrue($this->filter->matches($class)); - } -} diff --git a/tests/Go/Core/AdviceMatcherTest.php b/tests/Go/Core/AdviceMatcherTest.php index 2c11c743..2765889e 100644 --- a/tests/Go/Core/AdviceMatcherTest.php +++ b/tests/Go/Core/AdviceMatcherTest.php @@ -14,13 +14,15 @@ use Go\Aop\Advice; use Go\Aop\Pointcut; -use Go\Aop\Support\DefaultPointcutAdvisor; -use Go\Aop\Support\TruePointFilter; +use Go\Aop\Pointcut\TruePointcut; +use Go\Aop\Support\GenericPointcutAdvisor; use Go\ParserReflection\Locator\ComposerLocator; use Go\ParserReflection\ReflectionEngine; use Go\ParserReflection\ReflectionFile; use PHPUnit\Framework\TestCase; use ReflectionClass; +use ReflectionMethod; +use ReflectionProperty; class AdviceMatcherTest extends TestCase { @@ -50,7 +52,7 @@ protected function setUp(): void /** * Verifies that empty result will be returned without aspects and advisors */ - public function testGetEmptyAdvicesForClass() + public function testGetEmptyAdvicesForClass(): void { // by reflection $advices = $this->adviceMatcher->getAdvicesForClass($this->reflectionClass, []); @@ -60,78 +62,64 @@ public function testGetEmptyAdvicesForClass() /** * Check that list of advices for method works correctly */ - public function testGetSingleMethodAdviceForClassFromAdvisor() + public function testGetSingleMethodAdviceForClassFromAdvisor(): void { - $funcName = __FUNCTION__; + $methodName = __FUNCTION__; $pointcut = $this->createMock(Pointcut::class); - $pointcut - ->expects($this->any()) - ->method('getClassFilter') - ->will($this->returnValue(TruePointFilter::getInstance())) - ; $pointcut ->expects($this->any()) ->method('matches') - ->will( - $this->returnCallback( - function ($point) use ($funcName) { - return $point->name === $funcName; - } - ) + ->willReturnCallback( + function (ReflectionClass $class, ReflectionMethod|null $method) use ($methodName): bool { + return !isset($method) || $method->name === $methodName; + } ) ; $pointcut ->expects($this->any()) ->method('getKind') - ->will($this->returnValue(Pointcut::KIND_METHOD)) + ->willReturn(Pointcut::KIND_METHOD) ; $advice = $this->createMock(Advice::class); - $advisor = new DefaultPointcutAdvisor($pointcut, $advice); + $advisor = new GenericPointcutAdvisor($pointcut, $advice); $advices = $this->adviceMatcher->getAdvicesForClass($this->reflectionClass, ['advisor' => $advisor]); $this->assertArrayHasKey(AspectContainer::METHOD_PREFIX, $advices); - $this->assertArrayHasKey($funcName, $advices[AspectContainer::METHOD_PREFIX]); + $this->assertArrayHasKey($methodName, $advices[AspectContainer::METHOD_PREFIX]); $this->assertCount(1, $advices[AspectContainer::METHOD_PREFIX]); } /** * Check that list of advices for fields works correctly */ - public function testGetSinglePropertyAdviceForClassFromAdvisor() + public function testGetSinglePropertyAdviceForClassFromAdvisor(): void { - $propName = 'adviceMatcher'; // $this->adviceMatcher; + $propertyName = 'adviceMatcher'; // $this->adviceMatcher; $pointcut = $this->createMock(Pointcut::class); - $pointcut - ->expects($this->any()) - ->method('getClassFilter') - ->will($this->returnValue(TruePointFilter::getInstance())) - ; $pointcut ->expects($this->any()) ->method('matches') - ->will( - $this->returnCallback( - function ($point) use ($propName) { - return $point->name === $propName; - } - ) + ->willReturnCallback( + function (ReflectionClass $class, ReflectionProperty|null $property) use ($propertyName): bool { + return !isset($property) || $property->name === $propertyName; + } ) ; $pointcut ->expects($this->any()) ->method('getKind') - ->will($this->returnValue(Pointcut::KIND_PROPERTY)) + ->willReturn(Pointcut::KIND_PROPERTY) ; $advice = $this->createMock(Advice::class); - $advisor = new DefaultPointcutAdvisor($pointcut, $advice); + $advisor = new GenericPointcutAdvisor($pointcut, $advice); $advices = $this->adviceMatcher->getAdvicesForClass($this->reflectionClass, ['advisor' => $advisor]); $this->assertArrayHasKey(AspectContainer::PROPERTY_PREFIX, $advices); - $this->assertArrayHasKey($propName, $advices[AspectContainer::PROPERTY_PREFIX]); + $this->assertArrayHasKey($propertyName, $advices[AspectContainer::PROPERTY_PREFIX]); $this->assertCount(1, $advices[AspectContainer::PROPERTY_PREFIX]); } } diff --git a/tests/Go/Stubs/ClassWithMagicMethods.php b/tests/Go/Stubs/ClassWithMagicMethods.php new file mode 100644 index 00000000..18f097fe --- /dev/null +++ b/tests/Go/Stubs/ClassWithMagicMethods.php @@ -0,0 +1,47 @@ + + * + * This source file is subject to the license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Go\Stubs; + +class ClassWithMagicMethods +{ + public string $__call = 'magic'; + + /** + * @param array $arguments + */ + public function __call(string $name, array $arguments): string + { + return $name; + } + + /** + * @param array $arguments + */ + public function __callMe(string $name, array $arguments): string + { + return $name; + } + + /** + * @param array $arguments + */ + public static function __callStatic(string $name, array $arguments): string + { + return $name; + } + + public function notMagicMethod(string $name): string + { + return $name; + } +} diff --git a/tests/Go/Stubs/First.php b/tests/Go/Stubs/First.php index 8c6e5953..c3443162 100644 --- a/tests/Go/Stubs/First.php +++ b/tests/Go/Stubs/First.php @@ -12,6 +12,7 @@ namespace Go\Stubs; +#[StubAttribute(First::class)] class First { @@ -19,6 +20,9 @@ class First protected int $protected = T_PROTECTED; public int $public = T_PUBLIC; + #[StubAttribute(First::class)] + public string $publicWithAttribute = 'attribute'; + private static int $staticPrivate = T_PRIVATE; protected static int $staticProtected = T_PROTECTED; protected static int $staticPublic = T_PUBLIC; @@ -39,6 +43,22 @@ public function publicMethod(): int return $this->public; } + public final function publicFinalMethod(): void + { + // nothing here + } + + protected final function protectedFinalMethod(): void + { + // nothing here + } + + #[StubAttribute(First::class)] + public function publicMethodWithAttribute(): string + { + return $this->publicWithAttribute; + } + // Static methods that access self:: properties private static function staticSelfPrivate(): int { diff --git a/tests/Go/Stubs/FirstStatic.php b/tests/Go/Stubs/FirstStatic.php index 5c750220..db02fb3d 100644 --- a/tests/Go/Stubs/FirstStatic.php +++ b/tests/Go/Stubs/FirstStatic.php @@ -27,4 +27,19 @@ public static function staticLsbRecursion(int $value, int $level = 0): int { return static::$invocation->__invoke(self::class, [$value, $level]); } + + private static function privateStaticNever(): never + { + throw new \RuntimeException('Not implemented yet'); + } + + public static final function publicStaticFinal(): void + { + // nothing here + } + + private function privateDynamicNever(): never + { + throw new \RuntimeException('Not implemented yet'); + } } diff --git a/tests/Go/Stubs/StubAttribute.php b/tests/Go/Stubs/StubAttribute.php new file mode 100644 index 00000000..192fd0ce --- /dev/null +++ b/tests/Go/Stubs/StubAttribute.php @@ -0,0 +1,21 @@ + + * + * This source file is subject to the license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Go\Stubs; + +use Attribute; + +#[Attribute(flags: Attribute::TARGET_ALL)] +readonly class StubAttribute +{ + public function __construct(public string $value) {} +}