.
This commit is contained in:
commit
a5ce423afe
30 changed files with 1807 additions and 0 deletions
58
tests/AspectExceptionTest.php
Normal file
58
tests/AspectExceptionTest.php
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
<?php
|
||||
|
||||
use IceFox\Aspect\AspectException;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Tests\Aspects\ThrowingAspect;
|
||||
use Tests\Classes\ThrowingClass;
|
||||
|
||||
final class AspectExceptionTest extends TestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
ThrowingAspect::reset();
|
||||
}
|
||||
|
||||
public function testExceptionInBeforeIsWrappedInAspectException(): void
|
||||
{
|
||||
ThrowingAspect::$throwInBefore = true;
|
||||
|
||||
$instance = new ThrowingClass();
|
||||
|
||||
try {
|
||||
$instance->methodWithAspect();
|
||||
$this->fail('Expected AspectException to be thrown');
|
||||
} catch (AspectException $e) {
|
||||
$this->assertEquals('Exception in before() method of aspect', $e->getMessage());
|
||||
$this->assertEquals(ThrowingAspect::class, $e->aspectClass);
|
||||
$this->assertEquals('before', $e->method);
|
||||
$this->assertInstanceOf(\RuntimeException::class, $e->getPrevious());
|
||||
$this->assertEquals('Exception thrown in before()', $e->getPrevious()->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function testExceptionInAfterIsWrappedInAspectException(): void
|
||||
{
|
||||
ThrowingAspect::$throwInAfter = true;
|
||||
|
||||
$instance = new ThrowingClass();
|
||||
|
||||
try {
|
||||
$instance->methodWithAspect();
|
||||
$this->fail('Expected AspectException to be thrown');
|
||||
} catch (AspectException $e) {
|
||||
$this->assertEquals('Exception in after() method of aspect', $e->getMessage());
|
||||
$this->assertEquals(ThrowingAspect::class, $e->aspectClass);
|
||||
$this->assertEquals('after', $e->method);
|
||||
$this->assertInstanceOf(\RuntimeException::class, $e->getPrevious());
|
||||
$this->assertEquals('Exception thrown in after()', $e->getPrevious()->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function testMethodExecutesSuccessfullyWhenNoExceptions(): void
|
||||
{
|
||||
$instance = new ThrowingClass();
|
||||
$result = $instance->methodWithAspect();
|
||||
|
||||
$this->assertEquals('success', $result);
|
||||
}
|
||||
}
|
||||
67
tests/AspectStackingTest.php
Normal file
67
tests/AspectStackingTest.php
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
<?php
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Tests\Classes\StackedAspectsClass;
|
||||
use Tests\Aspects\LoggingAspect;
|
||||
|
||||
final class AspectStackingTest extends TestCase
|
||||
{
|
||||
private StackedAspectsClass $instance;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->instance = new StackedAspectsClass();
|
||||
LoggingAspect::clearLogs();
|
||||
}
|
||||
|
||||
public function testMultipleAspectsExecuteInOrder(): void
|
||||
{
|
||||
$tracker = (object) ['before' => false, 'after' => false];
|
||||
$result = $this->instance->multipleAspects(10, $tracker);
|
||||
|
||||
$this->assertEquals(20, $result);
|
||||
$this->assertTrue($tracker->before);
|
||||
$this->assertTrue($tracker->after);
|
||||
|
||||
$this->assertCount(2, LoggingAspect::$logs);
|
||||
$this->assertEquals('logging_before', LoggingAspect::$logs[0]['type']);
|
||||
$this->assertEquals('logging_after', LoggingAspect::$logs[1]['type']);
|
||||
$this->assertEquals(20, LoggingAspect::$logs[1]['result']);
|
||||
}
|
||||
|
||||
public function testSingleBasicAspect(): void
|
||||
{
|
||||
$tracker = (object) ['before' => false, 'after' => false];
|
||||
$result = $this->instance->onlyBasic(5, $tracker);
|
||||
|
||||
$this->assertEquals(6, $result);
|
||||
$this->assertTrue($tracker->before);
|
||||
$this->assertTrue($tracker->after);
|
||||
$this->assertEmpty(LoggingAspect::$logs);
|
||||
}
|
||||
|
||||
public function testSingleLoggingAspect(): void
|
||||
{
|
||||
$result = $this->instance->onlyLogging('hello');
|
||||
|
||||
$this->assertEquals('HELLO', $result);
|
||||
$this->assertCount(2, LoggingAspect::$logs);
|
||||
$this->assertEquals('logging_before', LoggingAspect::$logs[0]['type']);
|
||||
$this->assertEquals('logging_after', LoggingAspect::$logs[1]['type']);
|
||||
}
|
||||
|
||||
public function testNoAspects(): void
|
||||
{
|
||||
$result = $this->instance->noAspects();
|
||||
$this->assertEquals('plain', $result);
|
||||
$this->assertEmpty(LoggingAspect::$logs);
|
||||
}
|
||||
|
||||
public function testAspectsReceiveCorrectArguments(): void
|
||||
{
|
||||
$tracker = (object) ['before' => false, 'after' => false];
|
||||
$this->instance->multipleAspects(15, $tracker);
|
||||
|
||||
$this->assertEquals([15, $tracker], LoggingAspect::$logs[0]['args']);
|
||||
}
|
||||
}
|
||||
23
tests/Aspects/BasicAspect.php
Normal file
23
tests/Aspects/BasicAspect.php
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Aspects;
|
||||
|
||||
use Attribute;
|
||||
|
||||
#[Attribute(Attribute::TARGET_METHOD)]
|
||||
class BasicAspect
|
||||
{
|
||||
public ?object $object = null;
|
||||
|
||||
public function before(object|string $target, mixed ...$args): void
|
||||
{
|
||||
$this->object = end($args);
|
||||
$this->object->before = true;
|
||||
}
|
||||
|
||||
public function after(object|string $target, mixed $return): mixed
|
||||
{
|
||||
$this->object->after = true;
|
||||
return $return;
|
||||
}
|
||||
}
|
||||
60
tests/Aspects/ConfigurableAspect.php
Normal file
60
tests/Aspects/ConfigurableAspect.php
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Aspects;
|
||||
|
||||
use Attribute;
|
||||
|
||||
#[Attribute(Attribute::TARGET_METHOD)]
|
||||
class ConfigurableAspect
|
||||
{
|
||||
public static array $executionLog = [];
|
||||
|
||||
public function __construct(
|
||||
public readonly string $prefix = 'default',
|
||||
public readonly int $multiplier = 1,
|
||||
public readonly bool $enabled = true,
|
||||
) {
|
||||
}
|
||||
|
||||
public function before(object|string $target, mixed ...$args): void
|
||||
{
|
||||
if ($this->enabled) {
|
||||
self::$executionLog[] = [
|
||||
'event' => 'before',
|
||||
'prefix' => $this->prefix,
|
||||
'multiplier' => $this->multiplier,
|
||||
'args' => $args,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
public function after(object|string $target, mixed $result): mixed
|
||||
{
|
||||
if (!$this->enabled) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
self::$executionLog[] = [
|
||||
'event' => 'after',
|
||||
'prefix' => $this->prefix,
|
||||
'multiplier' => $this->multiplier,
|
||||
'result' => $result,
|
||||
];
|
||||
|
||||
// Modify result based on constructor parameters
|
||||
if (is_int($result)) {
|
||||
return $result * $this->multiplier;
|
||||
}
|
||||
|
||||
if (is_string($result)) {
|
||||
return $this->prefix . $result;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public static function reset(): void
|
||||
{
|
||||
self::$executionLog = [];
|
||||
}
|
||||
}
|
||||
27
tests/Aspects/LoggingAspect.php
Normal file
27
tests/Aspects/LoggingAspect.php
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Aspects;
|
||||
|
||||
use Attribute;
|
||||
|
||||
#[Attribute(Attribute::TARGET_METHOD)]
|
||||
class LoggingAspect
|
||||
{
|
||||
public static array $logs = [];
|
||||
|
||||
public function before(object|string $target, mixed ...$args): void
|
||||
{
|
||||
self::$logs[] = ['type' => 'logging_before', 'args' => $args];
|
||||
}
|
||||
|
||||
public function after(object|string $target, mixed $result): mixed
|
||||
{
|
||||
self::$logs[] = ['type' => 'logging_after', 'result' => $result];
|
||||
return $result;
|
||||
}
|
||||
|
||||
public static function clearLogs(): void
|
||||
{
|
||||
self::$logs = [];
|
||||
}
|
||||
}
|
||||
24
tests/Aspects/ModifyingAspect.php
Normal file
24
tests/Aspects/ModifyingAspect.php
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Aspects;
|
||||
|
||||
use Attribute;
|
||||
|
||||
#[Attribute(Attribute::TARGET_METHOD)]
|
||||
class ModifyingAspect
|
||||
{
|
||||
public static mixed $modifier = null;
|
||||
|
||||
public function after(object|string $target, mixed $result): mixed
|
||||
{
|
||||
if (self::$modifier !== null) {
|
||||
return (self::$modifier)($result);
|
||||
}
|
||||
return $result; // Return original value when not modifying
|
||||
}
|
||||
|
||||
public static function reset(): void
|
||||
{
|
||||
self::$modifier = null;
|
||||
}
|
||||
}
|
||||
34
tests/Aspects/ThrowingAspect.php
Normal file
34
tests/Aspects/ThrowingAspect.php
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Aspects;
|
||||
|
||||
use Attribute;
|
||||
use RuntimeException;
|
||||
|
||||
#[Attribute(Attribute::TARGET_METHOD)]
|
||||
class ThrowingAspect
|
||||
{
|
||||
public static bool $throwInBefore = false;
|
||||
public static bool $throwInAfter = false;
|
||||
|
||||
public function before(object|string $target, mixed ...$args): void
|
||||
{
|
||||
if (self::$throwInBefore) {
|
||||
throw new RuntimeException('Exception thrown in before()');
|
||||
}
|
||||
}
|
||||
|
||||
public function after(object|string $target, mixed $result): mixed
|
||||
{
|
||||
if (self::$throwInAfter) {
|
||||
throw new RuntimeException('Exception thrown in after()');
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
public static function reset(): void
|
||||
{
|
||||
self::$throwInBefore = false;
|
||||
self::$throwInAfter = false;
|
||||
}
|
||||
}
|
||||
47
tests/Aspects/TrackingAspect.php
Normal file
47
tests/Aspects/TrackingAspect.php
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Aspects;
|
||||
|
||||
use Attribute;
|
||||
|
||||
#[Attribute(Attribute::TARGET_METHOD)]
|
||||
class TrackingAspect
|
||||
{
|
||||
public static array $calls = [];
|
||||
|
||||
public function before(object|string $target, mixed ...$args): void
|
||||
{
|
||||
self::$calls[] = ['event' => 'before', 'args' => $args];
|
||||
}
|
||||
|
||||
public function after(object|string $target, mixed $result): mixed
|
||||
{
|
||||
self::$calls[] = ['event' => 'after', 'result' => $result];
|
||||
return $result;
|
||||
}
|
||||
|
||||
public static function clearCalls(): void
|
||||
{
|
||||
self::$calls = [];
|
||||
}
|
||||
|
||||
public static function getLastBefore(): ?array
|
||||
{
|
||||
foreach (array_reverse(self::$calls) as $call) {
|
||||
if ($call['event'] === 'before') {
|
||||
return $call;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static function getLastAfter(): ?array
|
||||
{
|
||||
foreach (array_reverse(self::$calls) as $call) {
|
||||
if ($call['event'] === 'after') {
|
||||
return $call;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
163
tests/AttributeArgumentsTest.php
Normal file
163
tests/AttributeArgumentsTest.php
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
<?php
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Tests\Aspects\ConfigurableAspect;
|
||||
use Tests\Classes\ConfigurableClass;
|
||||
|
||||
final class AttributeArgumentsTest extends TestCase
|
||||
{
|
||||
private ConfigurableClass $instance;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->instance = new ConfigurableClass();
|
||||
ConfigurableAspect::reset();
|
||||
}
|
||||
|
||||
public function testAttributeArgumentsArePassedToConstructor(): void
|
||||
{
|
||||
$result = $this->instance->customConfigMethod();
|
||||
|
||||
// Verify the result was modified using the constructor arguments
|
||||
$this->assertEquals('PREFIX:value', $result);
|
||||
|
||||
// Verify the execution log contains the constructor parameters
|
||||
$this->assertCount(2, ConfigurableAspect::$executionLog);
|
||||
|
||||
$beforeLog = ConfigurableAspect::$executionLog[0];
|
||||
$this->assertEquals('before', $beforeLog['event']);
|
||||
$this->assertEquals('PREFIX:', $beforeLog['prefix']);
|
||||
$this->assertEquals(10, $beforeLog['multiplier']);
|
||||
|
||||
$afterLog = ConfigurableAspect::$executionLog[1];
|
||||
$this->assertEquals('after', $afterLog['event']);
|
||||
$this->assertEquals('PREFIX:', $afterLog['prefix']);
|
||||
$this->assertEquals(10, $afterLog['multiplier']);
|
||||
}
|
||||
|
||||
public function testDifferentMethodsCanHaveDifferentConfigurations(): void
|
||||
{
|
||||
$result1 = $this->instance->customConfigMethod();
|
||||
ConfigurableAspect::reset();
|
||||
|
||||
$result2 = $this->instance->anotherMethod();
|
||||
|
||||
// First method uses prefix: 'PREFIX:', multiplier: 10
|
||||
$this->assertEquals('PREFIX:value', $result1);
|
||||
|
||||
// Second method uses prefix: 'BEFORE_', multiplier: 5
|
||||
$this->assertEquals(35, $result2); // 7 * 5
|
||||
|
||||
// Verify execution log for second method
|
||||
$afterLog = ConfigurableAspect::$executionLog[1];
|
||||
$this->assertEquals('BEFORE_', $afterLog['prefix']);
|
||||
$this->assertEquals(5, $afterLog['multiplier']);
|
||||
}
|
||||
|
||||
public function testDisabledAspectDoesNotModifyResult(): void
|
||||
{
|
||||
$result = $this->instance->disabledMethod();
|
||||
|
||||
// enabled: false should prevent modification
|
||||
$this->assertEquals('should not be modified', $result);
|
||||
|
||||
// No logs should be created when disabled
|
||||
$this->assertEmpty(ConfigurableAspect::$executionLog);
|
||||
}
|
||||
|
||||
public function testDefaultConstructorArgumentsAreUsed(): void
|
||||
{
|
||||
$result = $this->instance->defaultConfigMethod();
|
||||
|
||||
// Default multiplier is 1, so 42 * 1 = 42
|
||||
$this->assertEquals(42, $result);
|
||||
|
||||
// Verify default values in execution log
|
||||
$beforeLog = ConfigurableAspect::$executionLog[0];
|
||||
$this->assertEquals('default', $beforeLog['prefix']);
|
||||
$this->assertEquals(1, $beforeLog['multiplier']);
|
||||
}
|
||||
|
||||
public function testConstructorArgumentsAffectBehavior(): void
|
||||
{
|
||||
// Test with multiplier
|
||||
$result = $this->instance->anotherMethod();
|
||||
$this->assertEquals(35, $result); // 7 * 5 = 35
|
||||
|
||||
ConfigurableAspect::reset();
|
||||
|
||||
// Test with prefix
|
||||
$result = $this->instance->customConfigMethod();
|
||||
$this->assertEquals('PREFIX:value', $result);
|
||||
}
|
||||
|
||||
public function testEachConstructorArgumentIsActuallyUsed(): void
|
||||
{
|
||||
// Test 1: Verify 'prefix' argument is used
|
||||
ConfigurableAspect::reset();
|
||||
$result = $this->instance->customConfigMethod();
|
||||
$this->assertEquals('PREFIX:value', $result, 'prefix argument should prepend to string result');
|
||||
$afterLog = ConfigurableAspect::$executionLog[1];
|
||||
$this->assertEquals('PREFIX:', $afterLog['prefix'], 'prefix constructor argument should be stored and used');
|
||||
|
||||
// Test 2: Verify 'multiplier' argument is used
|
||||
ConfigurableAspect::reset();
|
||||
$result = $this->instance->anotherMethod();
|
||||
$this->assertEquals(35, $result, 'multiplier argument should multiply integer result (7 * 5 = 35)');
|
||||
$afterLog = ConfigurableAspect::$executionLog[1];
|
||||
$this->assertEquals(5, $afterLog['multiplier'], 'multiplier constructor argument should be stored and used');
|
||||
|
||||
// Test 3: Verify 'enabled' argument is used
|
||||
ConfigurableAspect::reset();
|
||||
$result = $this->instance->disabledMethod();
|
||||
$this->assertEquals('should not be modified', $result, 'enabled=false should prevent result modification');
|
||||
$this->assertEmpty(ConfigurableAspect::$executionLog, 'enabled=false should prevent logging');
|
||||
}
|
||||
|
||||
public function testPartialConstructorArgumentsWithDefaults(): void
|
||||
{
|
||||
// disabledMethod uses only 'enabled: false', should use defaults for prefix and multiplier
|
||||
ConfigurableAspect::reset();
|
||||
$this->instance->disabledMethod();
|
||||
|
||||
// Since enabled=false, no logs - but we can test with default values
|
||||
ConfigurableAspect::reset();
|
||||
$result = $this->instance->defaultConfigMethod();
|
||||
|
||||
// Should use all defaults: prefix='default', multiplier=1, enabled=true
|
||||
$this->assertEquals(42, $result, 'default multiplier (1) should not change result');
|
||||
|
||||
$beforeLog = ConfigurableAspect::$executionLog[0];
|
||||
$this->assertEquals('default', $beforeLog['prefix'], 'default prefix should be "default"');
|
||||
$this->assertEquals(1, $beforeLog['multiplier'], 'default multiplier should be 1');
|
||||
|
||||
$afterLog = ConfigurableAspect::$executionLog[1];
|
||||
$this->assertEquals('default', $afterLog['prefix'], 'default prefix should persist in after hook');
|
||||
$this->assertEquals(1, $afterLog['multiplier'], 'default multiplier should persist in after hook');
|
||||
}
|
||||
|
||||
public function testAllThreeConstructorArgumentsSimultaneously(): void
|
||||
{
|
||||
ConfigurableAspect::reset();
|
||||
|
||||
// customConfigMethod uses: prefix='PREFIX:', multiplier=10, enabled=true (default)
|
||||
$result = $this->instance->customConfigMethod();
|
||||
|
||||
// Verify all three arguments are working together
|
||||
$this->assertEquals('PREFIX:value', $result);
|
||||
|
||||
$beforeLog = ConfigurableAspect::$executionLog[0];
|
||||
$this->assertEquals('before', $beforeLog['event']);
|
||||
$this->assertEquals('PREFIX:', $beforeLog['prefix'], 'First argument (prefix) should be used');
|
||||
$this->assertEquals(10, $beforeLog['multiplier'], 'Second argument (multiplier) should be used');
|
||||
// Third argument (enabled=true) is verified by the fact that logs exist
|
||||
|
||||
$afterLog = ConfigurableAspect::$executionLog[1];
|
||||
$this->assertEquals('after', $afterLog['event']);
|
||||
$this->assertEquals('PREFIX:', $afterLog['prefix'], 'First argument should persist in after hook');
|
||||
$this->assertEquals(10, $afterLog['multiplier'], 'Second argument should persist in after hook');
|
||||
|
||||
// Verify the log exists (proving enabled=true was used)
|
||||
$this->assertCount(2, ConfigurableAspect::$executionLog, 'Third argument (enabled=true) should allow logging');
|
||||
}
|
||||
}
|
||||
17
tests/BasicAspectTest.php
Normal file
17
tests/BasicAspectTest.php
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Tests\Classes\WrappedClass;
|
||||
|
||||
final class BasicAspectTest extends TestCase
|
||||
{
|
||||
public function testAspectMethodIsCalled(): void
|
||||
{
|
||||
$sideEffect = (object) ['before' => false, 'after' => false];
|
||||
$c = new WrappedClass();
|
||||
$r = $c->wrappedMethod(1, 3, $sideEffect);
|
||||
$this->assertEquals(4, $r);
|
||||
$this->assertTrue($sideEffect->before);
|
||||
$this->assertTrue($sideEffect->after);
|
||||
}
|
||||
}
|
||||
32
tests/Classes/ConfigurableClass.php
Normal file
32
tests/Classes/ConfigurableClass.php
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Classes;
|
||||
|
||||
use Tests\Aspects\ConfigurableAspect;
|
||||
|
||||
class ConfigurableClass
|
||||
{
|
||||
#[ConfigurableAspect(prefix: 'PREFIX:', multiplier: 10)]
|
||||
public function customConfigMethod(): string
|
||||
{
|
||||
return 'value';
|
||||
}
|
||||
|
||||
#[ConfigurableAspect(prefix: 'BEFORE_', multiplier: 5)]
|
||||
public function anotherMethod(): int
|
||||
{
|
||||
return 7;
|
||||
}
|
||||
|
||||
#[ConfigurableAspect(enabled: false)]
|
||||
public function disabledMethod(): string
|
||||
{
|
||||
return 'should not be modified';
|
||||
}
|
||||
|
||||
#[ConfigurableAspect] // Uses default values
|
||||
public function defaultConfigMethod(): int
|
||||
{
|
||||
return 42;
|
||||
}
|
||||
}
|
||||
26
tests/Classes/ModifyingClass.php
Normal file
26
tests/Classes/ModifyingClass.php
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Classes;
|
||||
|
||||
use Tests\Aspects\ModifyingAspect;
|
||||
|
||||
class ModifyingClass
|
||||
{
|
||||
#[ModifyingAspect]
|
||||
public function getValue(): int
|
||||
{
|
||||
return 42;
|
||||
}
|
||||
|
||||
#[ModifyingAspect]
|
||||
public function getString(): string
|
||||
{
|
||||
return 'original';
|
||||
}
|
||||
|
||||
#[ModifyingAspect]
|
||||
public function getArray(): array
|
||||
{
|
||||
return ['key' => 'value'];
|
||||
}
|
||||
}
|
||||
66
tests/Classes/ParameterTypesClass.php
Normal file
66
tests/Classes/ParameterTypesClass.php
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Classes;
|
||||
|
||||
use Tests\Aspects\TrackingAspect;
|
||||
|
||||
class ParameterTypesClass
|
||||
{
|
||||
#[TrackingAspect]
|
||||
public function withNullable(?string $value): ?string
|
||||
{
|
||||
return $value;
|
||||
}
|
||||
|
||||
#[TrackingAspect]
|
||||
public function withVariadic(string ...$items): array
|
||||
{
|
||||
return $items;
|
||||
}
|
||||
|
||||
#[TrackingAspect]
|
||||
public function withReference(int &$counter): void
|
||||
{
|
||||
$counter++;
|
||||
}
|
||||
|
||||
#[TrackingAspect]
|
||||
public function withUnionType(int|string $value): int|string
|
||||
{
|
||||
return $value;
|
||||
}
|
||||
|
||||
#[TrackingAspect]
|
||||
public function withMixed(mixed $data): mixed
|
||||
{
|
||||
return $data;
|
||||
}
|
||||
|
||||
#[TrackingAspect]
|
||||
public function withArray(array $data): array
|
||||
{
|
||||
return array_merge($data, ['processed' => true]);
|
||||
}
|
||||
|
||||
#[TrackingAspect]
|
||||
public function withDefaultValues(string $name = 'default', int $age = 0): string
|
||||
{
|
||||
return "$name:$age";
|
||||
}
|
||||
|
||||
#[TrackingAspect]
|
||||
public function voidReturn(): void
|
||||
{
|
||||
}
|
||||
|
||||
#[TrackingAspect]
|
||||
protected function protectedMethod(string $value): string
|
||||
{
|
||||
return strtoupper($value);
|
||||
}
|
||||
|
||||
public function callProtected(string $value): string
|
||||
{
|
||||
return $this->protectedMethod($value);
|
||||
}
|
||||
}
|
||||
33
tests/Classes/StackedAspectsClass.php
Normal file
33
tests/Classes/StackedAspectsClass.php
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Classes;
|
||||
|
||||
use Tests\Aspects\BasicAspect;
|
||||
use Tests\Aspects\LoggingAspect;
|
||||
|
||||
class StackedAspectsClass
|
||||
{
|
||||
#[BasicAspect]
|
||||
#[LoggingAspect]
|
||||
public function multipleAspects(int $value, object $tracker): int
|
||||
{
|
||||
return $value * 2;
|
||||
}
|
||||
|
||||
#[BasicAspect]
|
||||
public function onlyBasic(int $value, object $tracker): int
|
||||
{
|
||||
return $value + 1;
|
||||
}
|
||||
|
||||
#[LoggingAspect]
|
||||
public function onlyLogging(string $message): string
|
||||
{
|
||||
return strtoupper($message);
|
||||
}
|
||||
|
||||
public function noAspects(): string
|
||||
{
|
||||
return 'plain';
|
||||
}
|
||||
}
|
||||
14
tests/Classes/ThrowingClass.php
Normal file
14
tests/Classes/ThrowingClass.php
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Classes;
|
||||
|
||||
use Tests\Aspects\ThrowingAspect;
|
||||
|
||||
class ThrowingClass
|
||||
{
|
||||
#[ThrowingAspect]
|
||||
public function methodWithAspect(): string
|
||||
{
|
||||
return 'success';
|
||||
}
|
||||
}
|
||||
14
tests/Classes/WrappedClass.php
Normal file
14
tests/Classes/WrappedClass.php
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Classes;
|
||||
|
||||
use Tests\Aspects\BasicAspect;
|
||||
|
||||
class WrappedClass
|
||||
{
|
||||
#[BasicAspect]
|
||||
public function wrappedMethod(int $a, int $b, object $sideEffect): int
|
||||
{
|
||||
return $a + $b;
|
||||
}
|
||||
}
|
||||
98
tests/ParameterTypesTest.php
Normal file
98
tests/ParameterTypesTest.php
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
<?php
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Tests\Classes\ParameterTypesClass;
|
||||
use Tests\Aspects\TrackingAspect;
|
||||
|
||||
final class ParameterTypesTest extends TestCase
|
||||
{
|
||||
private ParameterTypesClass $instance;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->instance = new ParameterTypesClass();
|
||||
TrackingAspect::clearCalls();
|
||||
}
|
||||
|
||||
public function testNullableParameter(): void
|
||||
{
|
||||
$result = $this->instance->withNullable('test');
|
||||
$this->assertEquals('test', $result);
|
||||
$this->assertCount(2, TrackingAspect::$calls);
|
||||
|
||||
TrackingAspect::clearCalls();
|
||||
$result = $this->instance->withNullable(null);
|
||||
$this->assertNull($result);
|
||||
$this->assertCount(2, TrackingAspect::$calls);
|
||||
}
|
||||
|
||||
public function testVariadicParameters(): void
|
||||
{
|
||||
$result = $this->instance->withVariadic('a', 'b', 'c');
|
||||
$this->assertEquals(['a', 'b', 'c'], $result);
|
||||
|
||||
$result = $this->instance->withVariadic();
|
||||
$this->assertEquals([], $result);
|
||||
}
|
||||
|
||||
public function testReferenceParameter(): void
|
||||
{
|
||||
$counter = 5;
|
||||
$this->instance->withReference($counter);
|
||||
$this->assertEquals(6, $counter);
|
||||
}
|
||||
|
||||
public function testUnionType(): void
|
||||
{
|
||||
$result = $this->instance->withUnionType(42);
|
||||
$this->assertEquals(42, $result);
|
||||
|
||||
$result = $this->instance->withUnionType('hello');
|
||||
$this->assertEquals('hello', $result);
|
||||
}
|
||||
|
||||
public function testMixedType(): void
|
||||
{
|
||||
$result = $this->instance->withMixed(['key' => 'value']);
|
||||
$this->assertEquals(['key' => 'value'], $result);
|
||||
|
||||
$result = $this->instance->withMixed('string');
|
||||
$this->assertEquals('string', $result);
|
||||
|
||||
$result = $this->instance->withMixed(null);
|
||||
$this->assertNull($result);
|
||||
}
|
||||
|
||||
public function testArrayParameter(): void
|
||||
{
|
||||
$result = $this->instance->withArray(['original' => true]);
|
||||
$this->assertEquals(['original' => true, 'processed' => true], $result);
|
||||
}
|
||||
|
||||
public function testDefaultValues(): void
|
||||
{
|
||||
$result = $this->instance->withDefaultValues();
|
||||
$this->assertEquals('default:0', $result);
|
||||
|
||||
$result = $this->instance->withDefaultValues('John', 25);
|
||||
$this->assertEquals('John:25', $result);
|
||||
|
||||
$result = $this->instance->withDefaultValues('Jane');
|
||||
$this->assertEquals('Jane:0', $result);
|
||||
}
|
||||
|
||||
public function testVoidReturnType(): void
|
||||
{
|
||||
$this->instance->voidReturn();
|
||||
$this->assertCount(2, TrackingAspect::$calls);
|
||||
$this->assertEquals('before', TrackingAspect::$calls[0]['event']);
|
||||
$this->assertEquals('after', TrackingAspect::$calls[1]['event']);
|
||||
$this->assertNull(TrackingAspect::$calls[1]['result']);
|
||||
}
|
||||
|
||||
public function testProtectedMethod(): void
|
||||
{
|
||||
$result = $this->instance->callProtected('hello');
|
||||
$this->assertEquals('HELLO', $result);
|
||||
}
|
||||
}
|
||||
91
tests/ReturnModificationTest.php
Normal file
91
tests/ReturnModificationTest.php
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
<?php
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Tests\Aspects\ModifyingAspect;
|
||||
use Tests\Classes\ModifyingClass;
|
||||
|
||||
final class ReturnModificationTest extends TestCase
|
||||
{
|
||||
private ModifyingClass $instance;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->instance = new ModifyingClass();
|
||||
ModifyingAspect::reset();
|
||||
}
|
||||
|
||||
public function testAfterReturningOriginalValueDoesNotModifyResult(): void
|
||||
{
|
||||
ModifyingAspect::$modifier = fn($result) => $result;
|
||||
|
||||
$result = $this->instance->getValue();
|
||||
|
||||
$this->assertEquals(42, $result);
|
||||
}
|
||||
|
||||
public function testAfterCanModifyIntegerResult(): void
|
||||
{
|
||||
ModifyingAspect::$modifier = fn($result) => $result * 2;
|
||||
|
||||
$result = $this->instance->getValue();
|
||||
|
||||
$this->assertEquals(84, $result);
|
||||
}
|
||||
|
||||
public function testAfterCanModifyStringResult(): void
|
||||
{
|
||||
ModifyingAspect::$modifier = fn($result) => strtoupper($result);
|
||||
|
||||
$result = $this->instance->getString();
|
||||
|
||||
$this->assertEquals('ORIGINAL', $result);
|
||||
}
|
||||
|
||||
public function testAfterCanIncrementIntegerResult(): void
|
||||
{
|
||||
ModifyingAspect::$modifier = fn($result) => $result + 100;
|
||||
|
||||
$result = $this->instance->getValue();
|
||||
|
||||
$this->assertEquals(142, $result);
|
||||
}
|
||||
|
||||
public function testAfterCanModifyArrayResult(): void
|
||||
{
|
||||
ModifyingAspect::$modifier = fn($result) => array_merge($result, ['added' => 'new']);
|
||||
|
||||
$result = $this->instance->getArray();
|
||||
|
||||
$this->assertEquals(['key' => 'value', 'added' => 'new'], $result);
|
||||
}
|
||||
|
||||
public function testAfterCanFilterArrayResult(): void
|
||||
{
|
||||
ModifyingAspect::$modifier = fn($result) => array_filter($result, fn($v) => $v !== 'value');
|
||||
|
||||
$result = $this->instance->getArray();
|
||||
|
||||
$this->assertEquals([], $result);
|
||||
}
|
||||
|
||||
public function testMultipleCallsWithDifferentModifiers(): void
|
||||
{
|
||||
ModifyingAspect::$modifier = fn($result) => $result * 2;
|
||||
$result1 = $this->instance->getValue();
|
||||
|
||||
ModifyingAspect::$modifier = fn($result) => $result + 10;
|
||||
$result2 = $this->instance->getValue();
|
||||
|
||||
$this->assertEquals(84, $result1);
|
||||
$this->assertEquals(52, $result2);
|
||||
}
|
||||
|
||||
public function testNoModifierReturnsOriginalValue(): void
|
||||
{
|
||||
// ModifyingAspect::$modifier is null, so after() returns original value
|
||||
|
||||
$result = $this->instance->getValue();
|
||||
|
||||
$this->assertEquals(42, $result);
|
||||
}
|
||||
}
|
||||
39
tests/bootstrap.php
Normal file
39
tests/bootstrap.php
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
use IceFox\Aspect\AspectBuilder;
|
||||
use IceFox\Aspect\AspectWeaver;
|
||||
use Psr\Log\NullLogger;
|
||||
use Tests\Aspects\BasicAspect;
|
||||
use Tests\Aspects\LoggingAspect;
|
||||
use Tests\Aspects\TrackingAspect;
|
||||
use Tests\Aspects\ThrowingAspect;
|
||||
use Tests\Aspects\ModifyingAspect;
|
||||
use Tests\Aspects\ConfigurableAspect;
|
||||
use Tests\Classes\WrappedClass;
|
||||
use Tests\Classes\ParameterTypesClass;
|
||||
use Tests\Classes\StackedAspectsClass;
|
||||
use Tests\Classes\ThrowingClass;
|
||||
use Tests\Classes\ModifyingClass;
|
||||
use Tests\Classes\ConfigurableClass;
|
||||
|
||||
$cacheDir = sys_get_temp_dir() . '/cache/php-aop-cache';
|
||||
$useCache = false;
|
||||
|
||||
$weaver = new AspectWeaver(
|
||||
[BasicAspect::class, LoggingAspect::class, TrackingAspect::class, ThrowingAspect::class, ModifyingAspect::class, ConfigurableAspect::class],
|
||||
$cacheDir,
|
||||
$useCache,
|
||||
new NullLogger(),
|
||||
);
|
||||
|
||||
$loader = AspectBuilder::begin()
|
||||
->withClasses([
|
||||
WrappedClass::class,
|
||||
ParameterTypesClass::class,
|
||||
StackedAspectsClass::class,
|
||||
ThrowingClass::class,
|
||||
ModifyingClass::class,
|
||||
ConfigurableClass::class,
|
||||
])
|
||||
->build($weaver)
|
||||
->register();
|
||||
Loading…
Add table
Add a link
Reference in a new issue