From 0bc1fc5e061f5a027d4cd45852c12ec648f76423 Mon Sep 17 00:00:00 2001 From: WalterWoshid Date: Sat, 5 Sep 2026 20:21:32 +0200 Subject: [PATCH] Support explicit regex patterns in advice attributes --- README.md | 25 ++++++ .../Attributes/AdviceType/MethodAdvice.php | 10 +-- src/Core/Attributes/Base/BaseAdvice.php | 28 ++++++- .../AspectMatching/RegexPatterns/Kernel.php | 14 ++++ .../RegexPatterns/PatternAspect.php | 18 +++++ .../RegexPatterns/RegexWeavingTest.php | 28 +++++++ .../RegexPatterns/Target/Other.php | 11 +++ .../RegexPatterns/Target/Selected.php | 18 +++++ .../RegexPatterns/AttributeFixture.php | 12 +++ .../RegexPatterns/RegexPatternsTest.php | 81 +++++++++++++++++++ 10 files changed, 237 insertions(+), 8 deletions(-) create mode 100644 tests/Functional/AspectMatching/RegexPatterns/Kernel.php create mode 100644 tests/Functional/AspectMatching/RegexPatterns/PatternAspect.php create mode 100644 tests/Functional/AspectMatching/RegexPatterns/RegexWeavingTest.php create mode 100644 tests/Functional/AspectMatching/RegexPatterns/Target/Other.php create mode 100644 tests/Functional/AspectMatching/RegexPatterns/Target/Selected.php create mode 100644 tests/Integration/RegexPatterns/AttributeFixture.php create mode 100644 tests/Integration/RegexPatterns/RegexPatternsTest.php diff --git a/README.md b/README.md index e216841..dff2bed 100644 --- a/README.md +++ b/README.md @@ -307,6 +307,31 @@ class PaymentProcessorAspect ``` +### Regular-expression patterns + +`class` and `method` accept either wildcard strings or explicit +`Okapi\Wildcards\Regex` objects. Each argument can use a different pattern type: + +```php +use Okapi\Aop\Attributes\After; +use Okapi\Wildcards\Regex; + +#[After( + class: 'App\\Http\\Controllers\\*', + method: new Regex('/^[a-z][a-z0-9_]*$/i'), +)] +``` + +This matches controller methods whose names start with a letter, excluding +constructors and other magic methods. Regex objects can also be used for `class`. +The same arguments are supported by `Before`, `Around`, and `After`. + +Pass a complete PHP regular expression, including delimiters and any modifiers. +Regex patterns are used as written: add `^` and `$` when you want to match the +whole name. Plain strings always retain wildcard semantics, even if they look +like `/regex/`. Invalid explicit regex patterns throw `InvalidArgumentException` +when the advice attribute is instantiated, identifying the affected argument. + ### Target Classes ```php diff --git a/src/Core/Attributes/AdviceType/MethodAdvice.php b/src/Core/Attributes/AdviceType/MethodAdvice.php index 1452be0..40ed998 100644 --- a/src/Core/Attributes/AdviceType/MethodAdvice.php +++ b/src/Core/Attributes/AdviceType/MethodAdvice.php @@ -25,8 +25,8 @@ abstract class MethodAdvice extends BaseAdvice /** * MethodAdvice constructor. * - * @param string|null $class Wildcard pattern for the class name. - * @param string|null $method Wildcard pattern for the method name. + * @param string|Regex|null $class Wildcard string or explicit regular expression for the class name. + * @param string|Regex|null $method Wildcard string or explicit regular expression for the method name. * @param int $order The order of the advice. * @param bool $interceptTraitMethods If {@see true}, trait methods will be intercepted. * [Default: {@see true}] @@ -34,13 +34,13 @@ abstract class MethodAdvice extends BaseAdvice * [Default: {@see false}] */ public function __construct( - ?string $class = null, - ?string $method = null, + string|Regex|null $class = null, + string|Regex|null $method = null, int $order = 0, public bool $interceptTraitMethods = true, public bool $onlyPublicMethods = false, ) { parent::__construct($class, $order); - $this->method = $method ? Regex::fromWildcard($method) : null; + $this->method = self::resolvePattern($method, 'method'); } } diff --git a/src/Core/Attributes/Base/BaseAdvice.php b/src/Core/Attributes/Base/BaseAdvice.php index ff85923..9097602 100644 --- a/src/Core/Attributes/Base/BaseAdvice.php +++ b/src/Core/Attributes/Base/BaseAdvice.php @@ -2,7 +2,9 @@ namespace Okapi\Aop\Core\Attributes\Base; +use InvalidArgumentException; use Okapi\Aop\Core\Attributes\AdviceType\MethodAdvice; +use Okapi\Wildcards\Exceptions\WildcardException; use Okapi\Wildcards\Regex; /** @@ -20,13 +22,33 @@ abstract class BaseAdvice extends BaseAttribute /** * Base advice constructor. * - * @param string|null $class Wildcard pattern for the class name. + * @param string|Regex|null $class Wildcard string or explicit regular expression for the class name. * @param int $order The order of the advice. */ public function __construct( - ?string $class = null, + string|Regex|null $class = null, public int $order = 0, ) { - $this->class = $class ? Regex::fromWildcard($class) : null; + $this->class = self::resolvePattern($class, 'class'); + } + + /** @throws InvalidArgumentException If an explicit regular expression is invalid. */ + protected static function resolvePattern(string|Regex|null $pattern, string $parameter): ?Regex + { + if (!$pattern instanceof Regex) { + return $pattern ? Regex::fromWildcard($pattern) : null; + } + + try { + // Compile the explicit expression now, before any class or method is matched. + $pattern->matches(''); + } catch (WildcardException $exception) { + throw new InvalidArgumentException( + sprintf('Invalid %s regex "%s": %s', $parameter, $pattern->getRegex(), $exception->getMessage()), + previous: $exception, + ); + } + + return $pattern; } } diff --git a/tests/Functional/AspectMatching/RegexPatterns/Kernel.php b/tests/Functional/AspectMatching/RegexPatterns/Kernel.php new file mode 100644 index 0000000..b868345 --- /dev/null +++ b/tests/Functional/AspectMatching/RegexPatterns/Kernel.php @@ -0,0 +1,14 @@ + */ + protected array $aspects = [PatternAspect::class]; +} diff --git a/tests/Functional/AspectMatching/RegexPatterns/PatternAspect.php b/tests/Functional/AspectMatching/RegexPatterns/PatternAspect.php new file mode 100644 index 0000000..5c97b3e --- /dev/null +++ b/tests/Functional/AspectMatching/RegexPatterns/PatternAspect.php @@ -0,0 +1,18 @@ +addTrace('matched'); + } +} diff --git a/tests/Functional/AspectMatching/RegexPatterns/RegexWeavingTest.php b/tests/Functional/AspectMatching/RegexPatterns/RegexWeavingTest.php new file mode 100644 index 0000000..44e0853 --- /dev/null +++ b/tests/Functional/AspectMatching/RegexPatterns/RegexWeavingTest.php @@ -0,0 +1,28 @@ +getStackTrace()); + static::assertSame('saved', $selected->save()); + static::assertSame(['matched'], StackTrace::getInstance()->getStackTrace()); + static::assertSame('skipped', $selected->skip()); + static::assertSame('other', (new Other())->save()); + static::assertSame(['matched'], StackTrace::getInstance()->getStackTrace()); + } +} diff --git a/tests/Functional/AspectMatching/RegexPatterns/Target/Other.php b/tests/Functional/AspectMatching/RegexPatterns/Target/Other.php new file mode 100644 index 0000000..c9a6f41 --- /dev/null +++ b/tests/Functional/AspectMatching/RegexPatterns/Target/Other.php @@ -0,0 +1,11 @@ +class); + static::assertSame($method, $advice->method); + static::assertTrue($class->matches('App\\Controller')); + static::assertTrue($method->matches('SAVE')); + static::assertFalse($method->matches('__construct')); + } + } + + public function testRegexAndWildcardPatternsCanBeMixed(): void + { + $wildcardClass = new After(class: 'App\\*', method: new Regex('~^save$~')); + static::assertNotNull($wildcardClass->class); + static::assertTrue($wildcardClass->class->matches('App\\Controller')); + $wildcardMethod = new After(class: new Regex('~Controller$~'), method: 'save*'); + static::assertNotNull($wildcardMethod->method); + static::assertTrue($wildcardMethod->method->matches('saveAll')); + static::assertNotNull($wildcardMethod->class); + static::assertTrue($wildcardMethod->class->matches('App\\Controller')); + } + + public function testRegexLookingStringsRemainWildcards(): void + { + $advice = new After(class: 'App\\*', method: '/^save$/'); + static::assertNotNull($advice->method); + static::assertTrue($advice->method->matches('/^save$/')); + static::assertFalse($advice->method->matches('save')); + } + + public function testAbsentPatternsRemainAbsent(): void + { + foreach ([new After(), new After('', ''), new After('0', '0')] as $advice) { + static::assertNull($advice->class); + static::assertNull($advice->method); + } + } + + public function testInvalidClassRegexFailsDuringConstruction(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid class regex'); + new After(class: new Regex('/[/'), method: '*'); + } + + public function testInvalidMethodRegexFailsDuringConstruction(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid method regex'); + new After(class: '*', method: new Regex('missing delimiters')); + } + + public function testRegexObjectsCanBeUsedInPhpAttributes(): void + { + $reflection = new ReflectionMethod(AttributeFixture::class, 'advice'); + $attribute = $reflection->getAttributes(After::class)[0]->newInstance(); + static::assertInstanceOf(After::class, $attribute); + static::assertNotNull($attribute->class); + static::assertNotNull($attribute->method); + static::assertTrue($attribute->class->matches('App\\Controller')); + static::assertTrue($attribute->method->matches('save')); + static::assertFalse($attribute->method->matches('__construct')); + } +}