Skip to content

Commit

Permalink
Merge pull request #236 from ngmy/support-readonly-class
Browse files Browse the repository at this point in the history
Add support for readonly class in PHP 8.2
  • Loading branch information
koriym authored Feb 14, 2025
2 parents 0eb646b + 105a980 commit 907717b
Show file tree
Hide file tree
Showing 13 changed files with 170 additions and 26 deletions.
50 changes: 50 additions & 0 deletions src-php8/Php82InterceptTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

declare(strict_types=1);

namespace Ray\Aop;

use Ray\Aop\ReflectiveMethodInvocation as Invocation;

use function call_user_func_array;

/**
* @psalm-import-type MethodBindings from Types
* @psalm-import-type Arguments from Types
*/
trait Php82InterceptTrait
{
private readonly InterceptTraitState $_state;

/**
* @param MethodBindings $bindings
*
* @see WeavedInterface::_initState()
*/
public function _initState(array $bindings): void
{
$this->_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;
}
}
5 changes: 4 additions & 1 deletion src/AopCode.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 */
Expand Down
7 changes: 5 additions & 2 deletions src/BindInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
/**
Expand All @@ -28,7 +31,7 @@ public function bindInterceptors(string $method, array $interceptors): self;
*
* [$methodNameA => [$interceptorA, ...][]
*
* @return array<string, array<MethodInterceptor|string>>
* @return MethodBindings
*/
public function getBindings();

Expand Down
5 changes: 2 additions & 3 deletions src/Compiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
27 changes: 17 additions & 10 deletions src/InterceptTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,26 @@

use function call_user_func_array;

/** @psalm-import-type MethodBindings from Types */
trait InterceptTrait
{
/**
* @var array<string, array<class-string<MethodInterceptor>>>
* @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<string, mixed> $args
Expand All @@ -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;
}
Expand Down
24 changes: 24 additions & 0 deletions src/InterceptTraitState.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

namespace Ray\Aop;

/** @psalm-import-type MethodBindings from Types */
final class InterceptTraitState
{
/**
* @var MethodBindings
* @readonly
*/
public $bindings;

/** @var bool */
public $isAspect = true;

/** @param MethodBindings $bindings */
public function __construct(array $bindings)
{
$this->bindings = $bindings;
}
}
7 changes: 7 additions & 0 deletions src/WeavedInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
4 changes: 2 additions & 2 deletions src/Weaver.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
19 changes: 11 additions & 8 deletions tests/CompilerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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);
}
Expand All @@ -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);
}
Expand All @@ -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);
}
Expand Down Expand Up @@ -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);
}
}
1 change: 1 addition & 0 deletions tests/Fake/FakeMockChild.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@

class FakeMockChild extends FakeMock
{
public $state;
}
7 changes: 7 additions & 0 deletions tests/Fake/FakePhp82ReadOnlyClass.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

declare(strict_types=1);

namespace Ray\Aop;

readonly class FakePhp82ReadOnlyClass {}
4 changes: 4 additions & 0 deletions tests/Fake/FakeWeavedClass.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,8 @@

class FakeWeavedClass extends FakeClass implements WeavedInterface
{
/**
* {@inheritDoc}
*/
public function _initState(array $bindings): void {}
}
36 changes: 36 additions & 0 deletions tests/tmp_unerase/Ray_Aop_FakeWeaverMock_523567342.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

declare(strict_types=1);

namespace Ray\Aop;

/** doc comment of FakeMock */
class FakeWeaverMock_523567342 extends FakeWeaverMock implements \Ray\Aop\WeavedInterface
{
use \Ray\Aop\Php82InterceptTrait;
/**
* doc comment of returnSame
*/
public function returnSame($a)
{
return $this->_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());
}
}

0 comments on commit 907717b

Please sign in to comment.