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 7e1633df..e18afbbd 100644 --- a/src/Aop/Pointcut/ClassMemberReference.php +++ b/src/Aop/Pointcut/ClassMemberReference.php @@ -12,83 +12,25 @@ namespace Go\Aop\Pointcut; -use Go\Aop\PointFilter; -use Go\Aop\Support\ModifierMatcherFilter; +use Go\Aop\Pointcut; /** - * 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 +final 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 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( - 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 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 5499b20d..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( - $reference->getVisibilityFilter(), - $reference->getAccessTypeFilter() + $pointcut = new AndPointcut( + Pointcut::KIND_METHOD | Pointcut::KIND_DYNAMIC, + $reference->classFilter, + $reference->visibilityFilter, + $reference->accessTypeFilter, + new MagicMethodDynamicPointcut($reference->memberNamePattern) ); - $pointcut = new MagicMethodPointcut($reference->getMemberNamePattern(), $memberFilter); - $pointcut->setClassFilter($reference->getClassFilter()); - return $pointcut; } ) @@ -202,20 +180,13 @@ function ($_0, $_1, ClassMemberReference $reference) { ->is('memberReference') ->call( function (ClassMemberReference $reference) { - $memberFilter = new AndPointFilter( - $reference->getVisibilityFilter(), - $reference->getAccessTypeFilter() + return new AndPointcut( + Pointcut::KIND_PROPERTY, + $reference->classFilter, + $reference->visibilityFilter, + $reference->accessTypeFilter, + new NamePointcut(Pointcut::KIND_PROPERTY, $reference->memberNamePattern) ); - - $pointcut = new SignaturePointcut( - PointFilter::KIND_PROPERTY, - $reference->getMemberNamePattern(), - $memberFilter - ); - - $pointcut->setClassFilter($reference->getClassFilter()); - - return $pointcut; } ) ; @@ -224,40 +195,26 @@ function (ClassMemberReference $reference) { ->is('memberReference', '(', 'argumentList', ')') ->call( function (ClassMemberReference $reference) { - $memberFilter = new AndPointFilter( - $reference->getVisibilityFilter(), - $reference->getAccessTypeFilter() + return new AndPointcut( + Pointcut::KIND_METHOD, + $reference->classFilter, + $reference->visibilityFilter, + $reference->accessTypeFilter, + new NamePointcut(Pointcut::KIND_METHOD, $reference->memberNamePattern) ); - - $pointcut = new SignaturePointcut( - PointFilter::KIND_METHOD, - $reference->getMemberNamePattern(), - $memberFilter - ); - - $pointcut->setClassFilter($reference->getClassFilter()); - - return $pointcut; } ) ->is('memberReference', '(', 'argumentList', ')', ':', 'namespaceName') ->call( function (ClassMemberReference $reference, $_0, $_1, $_2, $_3, $returnType) { - $memberFilter = new AndPointFilter( - $reference->getVisibilityFilter(), - $reference->getAccessTypeFilter(), - new ReturnTypeFilter($returnType) + return new AndPointcut( + Pointcut::KIND_METHOD, + $reference->classFilter, + $reference->visibilityFilter, + $reference->accessTypeFilter, + new NamePointcut(Pointcut::KIND_METHOD, $reference->memberNamePattern), + new ReturnTypePointcut($returnType), ); - - $pointcut = new SignaturePointcut( - PointFilter::KIND_METHOD, - $reference->getMemberNamePattern(), - $memberFilter - ); - - $pointcut->setClassFilter($reference->getClassFilter()); - - 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) {} +}