From 2a68d0f16ad6dbd9a58f2de4c06fd35ae7c4ff9f Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Tue, 30 Nov 2021 15:04:16 +0300 Subject: [PATCH] Finalize `StateResetter`, add more exceptions, add tests for 100% coverage (#282) --- composer.json | 3 +- src/StateResetter.php | 47 +++++++++++++++++++++--- tests/Unit/ContainerTest.php | 32 +++++++++++++++++ tests/Unit/StateResetterTest.php | 61 ++++++++++++++++++++++++++++++++ 4 files changed, 138 insertions(+), 5 deletions(-) create mode 100644 tests/Unit/StateResetterTest.php diff --git a/composer.json b/composer.json index 7494a0a0..595a8e5a 100644 --- a/composer.json +++ b/composer.json @@ -34,7 +34,8 @@ "roave/infection-static-analysis-plugin": "^1.11", "spatie/phpunit-watcher": "^1.23", "vimeo/psalm": "^4.13", - "yiisoft/injector": "^1.0" + "yiisoft/injector": "^1.0", + "yiisoft/test-support": "^1.3" }, "suggest": { "yiisoft/injector": "^1.0@dev", diff --git a/src/StateResetter.php b/src/StateResetter.php index bb38d4f8..310614ee 100644 --- a/src/StateResetter.php +++ b/src/StateResetter.php @@ -4,17 +4,25 @@ namespace Yiisoft\Di; +use Closure; +use InvalidArgumentException; use Psr\Container\ContainerInterface; + use function get_class; +use function gettype; use function is_int; +use function is_object; /** * State resetter allows resetting state of the services that are currently stored in the container and have "reset" * callback defined. The reset should be triggered after each request-response cycle in case you build long-running * applications with tools like [Swoole](https://www.swoole.co.uk/) or [RoadRunner](https://roadrunner.dev/). */ -class StateResetter +final class StateResetter { + /** + * @var Closure[]|self[] + */ private array $resetters = []; private ContainerInterface $container; @@ -41,20 +49,51 @@ public function reset(): void } /** - * @param array $resetters Array of reset callbacks. Each callback has access to the private and protected - * properties of the service instance, so you can set initial state of the service efficiently without creating - * a new instance. + * @param Closure[]|self[] $resetters Array of reset callbacks. Each callback has access to the private and + * protected properties of the service instance, so you can set initial state of the service efficiently + * without creating a new instance. */ public function setResetters(array $resetters): void { $this->resetters = []; foreach ($resetters as $serviceId => $callback) { if (is_int($serviceId)) { + if (!$callback instanceof self) { + throw new InvalidArgumentException(sprintf( + 'State resetter object should be instance of "%s", "%s" given.', + self::class, + $this->getType($callback) + )); + } $this->resetters[] = $callback; continue; } + + if (!$callback instanceof Closure) { + throw new InvalidArgumentException( + 'Callback for state resetter should be closure in format ' . + '`function (ContainerInterface $container): void`. ' . + 'Got "' . $this->getType($callback) . '".' + ); + } + + /** @var mixed $instance */ $instance = $this->container->get($serviceId); + if (!is_object($instance)) { + throw new InvalidArgumentException( + 'State resetter supports resetting objects only. Container returned ' . gettype($instance) . '.' + ); + } + $this->resetters[] = $callback->bindTo($instance, get_class($instance)); } } + + /** + * @param mixed $variable + */ + private function getType($variable): string + { + return is_object($variable) ? get_class($variable) : gettype($variable); + } } diff --git a/tests/Unit/ContainerTest.php b/tests/Unit/ContainerTest.php index 574a1e78..830dce1e 100644 --- a/tests/Unit/ContainerTest.php +++ b/tests/Unit/ContainerTest.php @@ -1151,6 +1151,38 @@ public function testResetter(): void $this->assertSame(42, $engine->getNumber()); } + public function testResetterInDelegates(): void + { + $config = ContainerConfig::create() + ->withDelegates([ + static function (ContainerInterface $container) { + $config = ContainerConfig::create() + ->withDefinitions([ + EngineInterface::class => [ + 'class' => EngineMarkOne::class, + 'setNumber()' => [42], + 'reset' => function () { + $this->number = 42; + }, + ], + ]); + return new Container($config); + }, + ]); + $container = new Container($config); + + $engine = $container->get(EngineInterface::class); + $this->assertSame(42, $container->get(EngineInterface::class)->getNumber()); + + $engine->setNumber(45); + $this->assertSame(45, $container->get(EngineInterface::class)->getNumber()); + + $container->get(StateResetter::class)->reset(); + + $this->assertSame($engine, $container->get(EngineInterface::class)); + $this->assertSame(42, $engine->getNumber()); + } + public function testWrongResetter(): void { $this->expectException(TypeError::class); diff --git a/tests/Unit/StateResetterTest.php b/tests/Unit/StateResetterTest.php new file mode 100644 index 00000000..0447899b --- /dev/null +++ b/tests/Unit/StateResetterTest.php @@ -0,0 +1,61 @@ +expectException(InvalidArgumentException::class); + $this->expectExceptionMessage( + 'State resetter object should be instance of "' . StateResetter::class . '", "stdClass" given.' + ); + $resetter->setResetters([ + new stdClass(), + ]); + } + + public function testStateResetterObjectForService(): void + { + $resetter = new StateResetter(new SimpleContainer()); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage( + 'Callback for state resetter should be closure in format ' . + '`function (ContainerInterface $container): void`. ' . + 'Got "' . StateResetter::class . '".' + ); + $resetter->setResetters([ + Car::class => $resetter, + ]); + } + + public function testResetNonObject(): void + { + $resetter = new StateResetter( + new SimpleContainer([ + 'value' => 42, + ]) + ); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage( + 'State resetter supports resetting objects only. Container returned integer.' + ); + $resetter->setResetters([ + 'value' => static function () { + }, + ]); + } +}