Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 5 additions & 5 deletions src/Core/Attributes/AdviceType/MethodAdvice.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,22 +25,22 @@ 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}]
* @param bool $onlyPublicMethods If {@see true}, only public methods will be intercepted.
* [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');
}
}
28 changes: 25 additions & 3 deletions src/Core/Attributes/Base/BaseAdvice.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -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;
}
}
14 changes: 14 additions & 0 deletions tests/Functional/AspectMatching/RegexPatterns/Kernel.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

namespace Okapi\Aop\Tests\Functional\AspectMatching\RegexPatterns;

use Okapi\Aop\AopKernel;
use Okapi\Aop\Tests\Util;

class Kernel extends AopKernel
{
protected ?string $cacheDir = Util::CACHE_DIR;

/** @var array<array-key, class-string> */
protected array $aspects = [PatternAspect::class];
}
18 changes: 18 additions & 0 deletions tests/Functional/AspectMatching/RegexPatterns/PatternAspect.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

namespace Okapi\Aop\Tests\Functional\AspectMatching\RegexPatterns;

use Okapi\Aop\Attributes\Aspect;
use Okapi\Aop\Attributes\Before;
use Okapi\Aop\Tests\Stubs\Etc\StackTrace;
use Okapi\Wildcards\Regex;

#[Aspect]
class PatternAspect
{
#[Before(class: new Regex('~\\\\RegexPatterns\\\\Target\\\\Selected$~'), method: new Regex('~^SAVE$~i'))]
public function record(): void
{
StackTrace::getInstance()->addTrace('matched');
}
}
28 changes: 28 additions & 0 deletions tests/Functional/AspectMatching/RegexPatterns/RegexWeavingTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

namespace Okapi\Aop\Tests\Functional\AspectMatching\RegexPatterns;

use Okapi\Aop\Tests\Functional\AspectMatching\RegexPatterns\Target\Other;
use Okapi\Aop\Tests\Functional\AspectMatching\RegexPatterns\Target\Selected;
use Okapi\Aop\Tests\Stubs\Etc\StackTrace;
use Okapi\Aop\Tests\Util;
use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses;
use PHPUnit\Framework\TestCase;

#[RunTestsInSeparateProcesses]
class RegexWeavingTest extends TestCase
{
public function testRegexSelectsClassesAndMethodsDuringWeaving(): void
{
Util::clearCache();
Kernel::init();

$selected = new Selected();
static::assertSame([], StackTrace::getInstance()->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());
}
}
11 changes: 11 additions & 0 deletions tests/Functional/AspectMatching/RegexPatterns/Target/Other.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace Okapi\Aop\Tests\Functional\AspectMatching\RegexPatterns\Target;

class Other
{
public function save(): string
{
return 'other';
}
}
18 changes: 18 additions & 0 deletions tests/Functional/AspectMatching/RegexPatterns/Target/Selected.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

namespace Okapi\Aop\Tests\Functional\AspectMatching\RegexPatterns\Target;

class Selected
{
public function __construct() {}

public function save(): string
{
return 'saved';
}

public function skip(): string
{
return 'skipped';
}
}
12 changes: 12 additions & 0 deletions tests/Integration/RegexPatterns/AttributeFixture.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

namespace Okapi\Aop\Tests\Integration\RegexPatterns;

use Okapi\Aop\Attributes\After;
use Okapi\Wildcards\Regex;

class AttributeFixture
{
#[After(class: new Regex('~^App\\\\Controller$~'), method: new Regex('~^[a-z]+$~'))]
public function advice(): void {}
}
81 changes: 81 additions & 0 deletions tests/Integration/RegexPatterns/RegexPatternsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php

namespace Okapi\Aop\Tests\Integration\RegexPatterns;

use InvalidArgumentException;
use Okapi\Aop\Attributes\After;
use Okapi\Aop\Attributes\Around;
use Okapi\Aop\Attributes\Before;
use Okapi\Wildcards\Regex;
use PHPUnit\Framework\TestCase;
use ReflectionMethod;

class RegexPatternsTest extends TestCase
{
public function testAllAdviceTypesAcceptRegexObjects(): void
{
$class = new Regex('~^App\\\\Controller$~');
$method = new Regex('~^[a-z][a-z0-9_]*$~i');
foreach ([new Before($class, $method), new Around($class, $method), new After($class, $method)] as $advice) {
static::assertSame($class, $advice->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'));
}
}
Loading