diff --git a/src-php8/Php82InterceptTrait.php b/src-php8/Php82InterceptTrait.php new file mode 100644 index 00000000..44433319 --- /dev/null +++ b/src-php8/Php82InterceptTrait.php @@ -0,0 +1,50 @@ +_state = new InterceptTraitState($bindings); + } + + /** + * @param Arguments $args + * + * @return mixed + * + * @SuppressWarnings(PHPMD.CamelCaseMethodName) + */ + private function _intercept(string $func, array $args) // phpcs:ignore + { + if (! $this->_state->isAspect) { + $this->_state->isAspect = true; + + return call_user_func_array([parent::class, $func], $args); + } + + $this->_state->isAspect = false; + $result = (new Invocation($this, $func, $args, $this->_state->bindings[$func]))->proceed(); + $this->_state->isAspect = true; + + return $result; + } +} diff --git a/src/AopCode.php b/src/AopCode.php index fdfea200..f461f4af 100644 --- a/src/AopCode.php +++ b/src/AopCode.php @@ -19,6 +19,7 @@ use function sprintf; use function token_get_all; +use const PHP_VERSION_ID; use const T_CLASS; use const T_EXTENDS; use const T_STRING; @@ -202,7 +203,9 @@ private function addMethods(ReflectionClass $class, BindInterface $bind): void /** @psalm-external-mutation-free */ private function addIntercepterTrait(): void { - $this->add(sprintf("{\n use \%s;\n}\n", InterceptTrait::class)); + PHP_VERSION_ID >= 80200 + ? $this->add(sprintf("{\n use \%s;\n}\n", Php82InterceptTrait::class)) + : $this->add(sprintf("{\n use \%s;\n}\n", InterceptTrait::class)); } /** @psalm-external-mutation-free */ diff --git a/src/BindInterface.php b/src/BindInterface.php index 3d379337..1e07eedb 100644 --- a/src/BindInterface.php +++ b/src/BindInterface.php @@ -4,7 +4,10 @@ namespace Ray\Aop; -/** @psalm-import-type MethodName from Types */ +/** + * @psalm-import-type MethodName from Types + * @psalm-import-type MethodBindings from Types + */ interface BindInterface { /** @@ -28,7 +31,7 @@ public function bindInterceptors(string $method, array $interceptors): self; * * [$methodNameA => [$interceptorA, ...][] * - * @return array> + * @return MethodBindings */ public function getBindings(); diff --git a/src/Compiler.php b/src/Compiler.php index 0f009cf3..ea4d395a 100644 --- a/src/Compiler.php +++ b/src/Compiler.php @@ -64,9 +64,8 @@ public function newInstance(string $class, array $args, BindInterface $bind): ob $compiledClass = $this->compile($class, $bind); assert(class_exists($compiledClass)); $instance = (new ReflectionClass($compiledClass))->newInstanceArgs($args); - assert($instance instanceof $class); - if (isset($instance->bindings)) { - $instance->bindings = $bind->getBindings(); + if ($instance instanceof WeavedInterface) { + $instance->_initState($bind->getBindings()); } assert($instance instanceof $class); diff --git a/src/InterceptTrait.php b/src/InterceptTrait.php index 8bd207de..2eaa9dfe 100644 --- a/src/InterceptTrait.php +++ b/src/InterceptTrait.php @@ -8,19 +8,26 @@ use function call_user_func_array; +/** @psalm-import-type MethodBindings from Types */ trait InterceptTrait { /** - * @var array>> + * @var InterceptTraitState * @readonly + * @psalm-suppress MissingConstructor */ - public $bindings = []; + private $_state; /** - * @var bool - * @readonly + * @param MethodBindings $bindings + * + * @see WeavedInterface::_initState() + * @SuppressWarnings(PHPMD.CamelCaseMethodName) */ - private $isAspect = true; + public function _initState(array $bindings): void // phpcs:ignore + { + $this->_state = new InterceptTraitState($bindings); + } /** * @param array $args @@ -31,15 +38,15 @@ trait InterceptTrait */ private function _intercept(string $func, array $args) // phpcs:ignore { - if (! $this->isAspect) { - $this->isAspect = true; + if (! $this->_state->isAspect) { + $this->_state->isAspect = true; return call_user_func_array([parent::class, $func], $args); } - $this->isAspect = false; - $result = (new Invocation($this, $func, $args, $this->bindings[$func]))->proceed(); - $this->isAspect = true; + $this->_state->isAspect = false; + $result = (new Invocation($this, $func, $args, $this->_state->bindings[$func]))->proceed(); + $this->_state->isAspect = true; return $result; } diff --git a/src/InterceptTraitState.php b/src/InterceptTraitState.php new file mode 100644 index 00000000..38aa63fb --- /dev/null +++ b/src/InterceptTraitState.php @@ -0,0 +1,24 @@ +bindings = $bindings; + } +} diff --git a/src/WeavedInterface.php b/src/WeavedInterface.php index 9e1d0c64..17d73028 100644 --- a/src/WeavedInterface.php +++ b/src/WeavedInterface.php @@ -4,6 +4,13 @@ namespace Ray\Aop; +/** @psalm-import-type MethodBindings from Types */ interface WeavedInterface { + /** + * @param MethodBindings $bindings + * + * @SuppressWarnings(PHPMD.CamelCaseMethodName) + */ + public function _initState(array $bindings): void; // phpcs:ignore } diff --git a/src/Weaver.php b/src/Weaver.php index 0d803fa6..2ee361c9 100644 --- a/src/Weaver.php +++ b/src/Weaver.php @@ -57,12 +57,12 @@ public function newInstance(string $class, array $args): object { $aopClass = $this->weave($class); $instance = (new ReflectionClass($aopClass))->newInstanceArgs($args); - if (! isset($instance->bindings)) { + if (! $instance instanceof WeavedInterface) { /** @var T $instance */ return $instance; } - $instance->bindings = $this->bind->getBindings(); + $instance->_initState($this->bind->getBindings()); assert($instance instanceof $class); return $instance; diff --git a/tests/CompilerTest.php b/tests/CompilerTest.php index 25e391e1..a2bc6db7 100644 --- a/tests/CompilerTest.php +++ b/tests/CompilerTest.php @@ -78,8 +78,6 @@ public function testParentClassName(object $class): void /** @depends testNewInstance */ public function testBuildClassWeaved(FakeMock $weaved): void { - assert(isset($weaved->bindings)); - $weaved->bindings = $this->bind->getBindings(); $result = $weaved->returnSame(1); $this->assertSame(2, $result); } @@ -117,8 +115,7 @@ public function testParentMethodIntercept(): void { $mock = $this->compiler->newInstance(FakeMockGrandChild::class, [], $this->bind); assert($mock instanceof FakeMockGrandChild); - assert(property_exists($mock, 'bindings')); - $mock->bindings = $this->bind->getBindings(); + assert(property_exists($mock, '_state')); $result = $mock->returnSame(1); $this->assertSame(2, $result); } @@ -128,8 +125,7 @@ public function testTypedParentMethodIntercept(): void $bind = (new Bind())->bindInterceptors('passIterator', [new NullInterceptor()]); $mock = $this->compiler->newInstance(FakeTypedMockGrandChild::class, [], $bind); assert($mock instanceof FakeTypedMockGrandChild); - assert(property_exists($mock, 'bindings')); - $mock->bindings = $bind->getBindings(); + assert(property_exists($mock, '_state')); $result = $mock->passIterator(new ArrayIterator()); $this->assertInstanceOf(ArrayIterator::class, $result); } @@ -138,8 +134,7 @@ public function testParentOfParentMethodIntercept(): void { $mock = $this->compiler->newInstance(FakeMockChildChild::class, [], $this->bind); assert($mock instanceof FakeMockChild); - assert(property_exists($mock, 'bindings')); - $mock->bindings = $this->bind->getBindings(); + assert(property_exists($mock, '_state')); $result = $mock->returnSame(1); $this->assertSame(2, $result); } @@ -350,4 +345,12 @@ public function testMethodWithMixedArgument(): void $this->assertInstanceOf(FakeMixedParamClass::class, $mock); $this->assertInstanceOf(WeavedInterface::class, $mock); } + + /** @requires PHP 8.2 */ + public function testNewInstanceWithPhp82ReadOnlyClass(): void + { + $mock = $this->compiler->newInstance(FakePhp82ReadOnlyClass::class, [], $this->bind); + $this->assertInstanceOf(FakePhp82ReadOnlyClass::class, $mock); + $this->assertInstanceOf(WeavedInterface::class, $mock); + } } diff --git a/tests/Fake/FakeMockChild.php b/tests/Fake/FakeMockChild.php index 80284b03..2e6d00fa 100644 --- a/tests/Fake/FakeMockChild.php +++ b/tests/Fake/FakeMockChild.php @@ -6,4 +6,5 @@ class FakeMockChild extends FakeMock { + public $state; } diff --git a/tests/Fake/FakePhp82ReadOnlyClass.php b/tests/Fake/FakePhp82ReadOnlyClass.php new file mode 100644 index 00000000..552239f9 --- /dev/null +++ b/tests/Fake/FakePhp82ReadOnlyClass.php @@ -0,0 +1,7 @@ +_intercept(__FUNCTION__, func_get_args()); + } + + /** + * doc comment of getSub + */ + public function getSub($a, $b) + { + return $this->_intercept(__FUNCTION__, func_get_args()); + } + + public function returnValue(null|\Ray\Aop\FakeNum $num = NULL) + { + return $this->_intercept(__FUNCTION__, func_get_args()); + } + + public function getPrivateVal() + { + return $this->_intercept(__FUNCTION__, func_get_args()); + } +} \ No newline at end of file