From b1a6fc3d0e9e3c25df1b64c177f42a63750bd9ed Mon Sep 17 00:00:00 2001 From: Serge Kvashnin <75180587+serge-kvashnin@users.noreply.github.com> Date: Sat, 1 Jun 2024 12:45:38 +0200 Subject: [PATCH] Invoking callables using container. --- docs/content/usage/invoking-callables.md | 38 +++++++++++---- src/Compiler/template.php | 7 ++- src/Configurator.php | 12 +++-- src/Container.php | 26 +++++++++- src/InvokerInterface.php | 12 +++++ tests/BaseTestCase.php | 17 ++++--- tests/Fixtures/Invoke/Fixture289a24a0.php | 9 ++++ tests/Fixtures/Invoke/Fixture3b51a559.php | 9 ++++ tests/Fixtures/Invoke/Fixture9c429bd8.php | 18 +++++++ tests/Integration/AnonymousFunctionTest.php | 6 +-- tests/Integration/CallTest.php | 2 +- tests/Integration/CircularDependencyTest.php | 4 +- tests/Integration/CollectionTest.php | 8 ++-- tests/Integration/CombinedTest.php | 4 +- tests/Integration/ConstructorTest.php | 4 +- .../Integration/EnvironmentVariablesTest.php | 4 +- tests/Integration/FactoryMethodTest.php | 4 +- tests/Integration/FactoryTest.php | 2 +- tests/Integration/InvokeTest.php | 48 +++++++++++++++++++ tests/Integration/ScalarValuesTest.php | 4 +- 20 files changed, 195 insertions(+), 43 deletions(-) create mode 100644 src/InvokerInterface.php create mode 100644 tests/Fixtures/Invoke/Fixture289a24a0.php create mode 100644 tests/Fixtures/Invoke/Fixture3b51a559.php create mode 100644 tests/Fixtures/Invoke/Fixture9c429bd8.php create mode 100644 tests/Integration/InvokeTest.php diff --git a/docs/content/usage/invoking-callables.md b/docs/content/usage/invoking-callables.md index d2f62e6..300e8b0 100644 --- a/docs/content/usage/invoking-callables.md +++ b/docs/content/usage/invoking-callables.md @@ -20,15 +20,17 @@ background tasks, or triggering specific actions in your application. ## Automatic Dependency Injection -When you invoke a callable through the container's `invoke()` method, it will inspect the callable's parameters and +When you invoke a callable through using container as a callable itself, it will inspect the callable's parameters and automatically resolve any dependencies it can find registered in the container. ## Passing Additional Parameters -Besides injecting dependencies, you can also pass additional parameters to the callable through the `invoke()` method: +Besides injecting dependencies, you can also pass additional parameters to the call: ```php -$result = $container->invoke(MyCallable::class, ['extraArg' => 'value']); +$result = $container(MyCallable::class, extraArg: 'value'); +// or +$result = $container->__invoke(MyCallable::class, extraArg: 'value'); ``` Let's illustrate how to invoke different types of callables. @@ -45,18 +47,24 @@ class MyCommand { } // ... -$container->invoke(MyCommand::class, ['message' => 'Command is executed.']); +$container(MyCommand::class, message: 'Command is executed.'); +// or +$container->__invoke(MyCommand::class, message: 'Command is executed.'); ``` ## Class Methods ```php +use function Norvica\Container\ref; + class MyService { public function processData(string $data) { /* ... */ } } // ... -$container->invoke([MyService::class, 'processData'], ['data' => 'some_data']); +$container([ref(MyService::class), 'processData'], data: 'some_data'); +// or +$container->__invoke([ref(MyService::class), 'processData'], data: 'some_data'); ``` ## Static Methods @@ -67,13 +75,25 @@ class Utils { } // ... -$container->invoke(Utils::generateReport(...), ['startDate' => '2024-01-01', 'endDate' => '2024-01-31']); +$container(Utils::generateReport(...), startDate: '2024-01-01', endDate: '2024-01-31'); +// or +$container->__invoke(Utils::generateReport(...), startDate: '2024-01-01', endDate: '2024-01-31'); ``` ## Anonymous Functions ```php -$container->invoke(function (LoggerInterface $logger, string $message) { - $logger->info($message); -}, ['message' => 'Logging a message from the closure']); +$container( + function (LoggerInterface $logger, string $message) { + $logger->info($message); + }, + message: 'Logging a message from the closure', +); +// or +$container->__invoke( + function (LoggerInterface $logger, string $message) { + $logger->info($message); + }, + message: 'Logging a message from the closure', +); ``` diff --git a/src/Compiler/template.php b/src/Compiler/template.php index 73bfb91..5024919 100644 --- a/src/Compiler/template.php +++ b/src/Compiler/template.php @@ -5,7 +5,7 @@ /** * @internal This class has been auto-generated by the 'norvica/container' library. */ -final class T implements \Psr\Container\ContainerInterface { +final class T implements \Psr\Container\ContainerInterface, \Norvica\Container\InvokerInterface { private const MAP = []; private array $resolved = []; @@ -24,4 +24,9 @@ public function has(string $id): bool { return isset(self::MAP[$id]); } + + public function __invoke(Closure|callable|array|string $callable, mixed ...$arguments): mixed + { + throw new \Norvica\Container\Exception\ContainerException('Autowiring must be enabled for invoking callables using container.'); + } } diff --git a/src/Configurator.php b/src/Configurator.php index 25e1c7a..4e736f3 100644 --- a/src/Configurator.php +++ b/src/Configurator.php @@ -23,7 +23,7 @@ final class Configurator private const INITIALIZED = 4; private Definitions $definitions; - private ContainerInterface|null $container = null; + private (ContainerInterface&InvokerInterface)|null $container = null; private string|null $class = null; private string|null $filename = null; private string|null $dir = null; @@ -154,7 +154,7 @@ public function load(Definitions|callable|array|string $configuration): self ); } - public function container(): ContainerInterface + public function container(): ContainerInterface&InvokerInterface { if ($this->container) { return $this->container; @@ -171,13 +171,17 @@ public function container(): ContainerInterface $this->state = self::INITIALIZED; return $this->container = $this->autowiring - ? new Container(new Definitions(), new ($this->class)(), $this->autowiring) + ? new Container( + definitions: new Definitions(), + compiled: new ($this->class)(), + autowiring: $this->autowiring, + ) : new ($this->class)(); } $this->state = self::INITIALIZED; - return $this->container = new Container($this->definitions); + return $this->container = new Container(definitions: $this->definitions, autowiring: $this->autowiring); } public function definitions(): Definitions diff --git a/src/Container.php b/src/Container.php index f1c6d8d..7e4b7c5 100644 --- a/src/Container.php +++ b/src/Container.php @@ -25,7 +25,7 @@ /** * @internal */ -final class Container implements ContainerInterface +final class Container implements ContainerInterface, InvokerInterface { /** * @var array @@ -89,6 +89,25 @@ public function has(string $id): bool return $this->definitions->has($id) || $this->compiled?->has($id); } + public function __invoke(Closure|callable|array|string $callable, mixed ...$arguments): mixed + { + if (!$this->autowiring) { + throw new ContainerException('Autowiring must be enabled for invoking callables using container.'); + } + + if (array_is_list($arguments)) { + throw new ContainerException('Extra parameters should be passed as named parameters.'); + } + + $closure = $this->closure($callable); + $parameters = $this->parameters( + $arguments, + new ReflectionFunction($closure), + ); + + return $closure(...$parameters); + } + private function resolve(mixed $definition): mixed { if (is_array($definition)) { @@ -180,6 +199,11 @@ private function closure(Closure|callable|array|string $callable): Closure $callable[0] = $this->resolve($callable[0]); } + // e.g. Foo::class with __invoke() method + if (is_string($callable) && class_exists($callable)) { + $callable = [$this->get($callable), '__invoke']; + } + return $callable(...); } diff --git a/src/InvokerInterface.php b/src/InvokerInterface.php new file mode 100644 index 0000000..bc0329d --- /dev/null +++ b/src/InvokerInterface.php @@ -0,0 +1,12 @@ +files as $file) { + parent::tearDownAfterClass(); + foreach (static::$files as $file) { if (!is_readable($file)) { continue; } @@ -32,21 +33,23 @@ protected function tearDown(): void } } - protected function container(array|string $configuration): ContainerInterface + protected static function container(array|string $configuration, bool $autowiring = true): ContainerInterface&InvokerInterface { $configurator = new Configurator(); + $configurator->autowiring($autowiring); $configurator->load($configuration); return $configurator->container(); } - protected function compiled(array|string $configuration): ContainerInterface + protected static function compiled(array|string $configuration, bool $autowiring = true): ContainerInterface&InvokerInterface { $configurator = new Configurator(); + $configurator->autowiring($autowiring); $configurator->load($configuration); $hash = bin2hex(random_bytes(2)); - $this->files[] = __DIR__ . "/../var/Container{$hash}.php"; + static::$files[] = __DIR__ . "/../var/Container{$hash}.php"; return $configurator->snapshot(__DIR__ . "/../var", "Container{$hash}")->container(); } diff --git a/tests/Fixtures/Invoke/Fixture289a24a0.php b/tests/Fixtures/Invoke/Fixture289a24a0.php new file mode 100644 index 0000000..6e34a73 --- /dev/null +++ b/tests/Fixtures/Invoke/Fixture289a24a0.php @@ -0,0 +1,9 @@ +a, $b, $c]; + } +} diff --git a/tests/Integration/AnonymousFunctionTest.php b/tests/Integration/AnonymousFunctionTest.php index 1e49563..51500b3 100644 --- a/tests/Integration/AnonymousFunctionTest.php +++ b/tests/Integration/AnonymousFunctionTest.php @@ -139,10 +139,10 @@ static function (array $options) { #[DataProvider('configuration')] public function testCold(array $configuration): void { - $container = $this->container($configuration); + $container = self::container($configuration); $this->assertInstanceOf(Result::class, $container->get('object')); - $compiled = $this->compiled($configuration); + $compiled = self::compiled($configuration); $this->assertInstanceOf(Result::class, $compiled->get('object')); } @@ -155,7 +155,7 @@ public static function files(): Generator #[DataProvider('files')] public function testCompiled(string $file): void { - $compiled = $this->compiled($file); + $compiled = self::compiled($file); $this->assertEquals('foo', $compiled->get('a')); $this->assertEquals('bar', $compiled->get('b')); diff --git a/tests/Integration/CallTest.php b/tests/Integration/CallTest.php index c431b57..21932e6 100644 --- a/tests/Integration/CallTest.php +++ b/tests/Integration/CallTest.php @@ -104,7 +104,7 @@ public static function configuration(): Generator #[DataProvider('configuration')] public function test(array $configuration, string $expectation): void { - $container = $this->container($configuration); + $container = self::container($configuration); $this->assertInstanceOf($expectation, $container->get('object')); } diff --git a/tests/Integration/CircularDependencyTest.php b/tests/Integration/CircularDependencyTest.php index 817371a..9c2690b 100644 --- a/tests/Integration/CircularDependencyTest.php +++ b/tests/Integration/CircularDependencyTest.php @@ -40,7 +40,7 @@ public static function configuration(): Generator public function testCold(array $configuration, string $id): void { $this->expectException(CircularDependencyException::class); - $container = $this->container($configuration); + $container = self::container($configuration); $container->get($id); } @@ -49,7 +49,7 @@ public function testCold(array $configuration, string $id): void public function testCompiled(array $configuration, string $id): void { $this->expectException(CircularDependencyException::class); - $container = $this->compiled($configuration); + $container = self::compiled($configuration); $container->get($id); } diff --git a/tests/Integration/CollectionTest.php b/tests/Integration/CollectionTest.php index be1414f..797a125 100644 --- a/tests/Integration/CollectionTest.php +++ b/tests/Integration/CollectionTest.php @@ -37,8 +37,8 @@ public function testTopLevel(): void $this->assertEquals('d', $collection['d']); }; - $assert($this->container($configuration)); - $assert($this->compiled($configuration)); + $assert(self::container($configuration)); + $assert(self::compiled($configuration)); } public function testNested(): void @@ -63,7 +63,7 @@ public function testNested(): void $this->assertEquals('d', $collection['d']); }; - $assert($this->container($configuration)); - $assert($this->compiled($configuration)); + $assert(self::container($configuration)); + $assert(self::compiled($configuration)); } } diff --git a/tests/Integration/CombinedTest.php b/tests/Integration/CombinedTest.php index 8af26b7..4428ba9 100644 --- a/tests/Integration/CombinedTest.php +++ b/tests/Integration/CombinedTest.php @@ -24,13 +24,13 @@ public static function setUpBeforeClass(): void public function testContainer(): void { - $container = $this->container(__DIR__ . '/../Fixtures/Combined/container.php'); + $container = self::container(__DIR__ . '/../Fixtures/Combined/container.php'); $this->assertions($container); } public function testCompiled(): void { - $container = $this->compiled(__DIR__ . '/../Fixtures/Combined/container.php'); + $container = self::compiled(__DIR__ . '/../Fixtures/Combined/container.php'); $this->assertions($container); } diff --git a/tests/Integration/ConstructorTest.php b/tests/Integration/ConstructorTest.php index 127abed..c60085f 100644 --- a/tests/Integration/ConstructorTest.php +++ b/tests/Integration/ConstructorTest.php @@ -118,10 +118,10 @@ public static function configuration(): Generator #[DataProvider('configuration')] public function testCold(array $configuration, string $expectation): void { - $container = $this->container($configuration); + $container = self::container($configuration); $this->assertInstanceOf($expectation, $container->get('object')); - $compiled = $this->compiled($configuration); + $compiled = self::compiled($configuration); $this->assertInstanceOf($expectation, $compiled->get('object')); } } diff --git a/tests/Integration/EnvironmentVariablesTest.php b/tests/Integration/EnvironmentVariablesTest.php index 45078da..9376238 100644 --- a/tests/Integration/EnvironmentVariablesTest.php +++ b/tests/Integration/EnvironmentVariablesTest.php @@ -36,10 +36,10 @@ public static function configuration(): Generator public function testCold(array $configuration, array $expectation): void { [$id, $value] = $expectation; - $container = $this->container($configuration); + $container = self::container($configuration); $this->assertEquals($value, $container->get($id)); - $compiled = $this->compiled($configuration); + $compiled = self::compiled($configuration); $this->assertEquals($value, $compiled->get($id)); } } diff --git a/tests/Integration/FactoryMethodTest.php b/tests/Integration/FactoryMethodTest.php index e0cf1bd..43a98a1 100644 --- a/tests/Integration/FactoryMethodTest.php +++ b/tests/Integration/FactoryMethodTest.php @@ -112,10 +112,10 @@ public static function configuration(): Generator #[DataProvider('configuration')] public function test(array $configuration, string $expectation): void { - $container = $this->container($configuration); + $container = self::container($configuration); $this->assertInstanceOf($expectation, $container->get('object')); - $compiled = $this->compiled($configuration); + $compiled = self::compiled($configuration); $this->assertInstanceOf($expectation, $compiled->get('object')); } } diff --git a/tests/Integration/FactoryTest.php b/tests/Integration/FactoryTest.php index 9cd6ef8..8229d07 100644 --- a/tests/Integration/FactoryTest.php +++ b/tests/Integration/FactoryTest.php @@ -129,7 +129,7 @@ public static function configuration(): Generator #[DataProvider('configuration')] public function test(array $configuration): void { - $container = $this->container($configuration); + $container = self::container($configuration); $this->assertInstanceOf(Result::class, $container->get('object')); } diff --git a/tests/Integration/InvokeTest.php b/tests/Integration/InvokeTest.php new file mode 100644 index 0000000..cd50094 --- /dev/null +++ b/tests/Integration/InvokeTest.php @@ -0,0 +1,48 @@ + [self::container([])]; + yield 'compiled' => [self::compiled([])]; + } + + #[DataProvider('containers')] + public function testInvoke(InvokerInterface $container): void + { + [$a, $b, $c] = $container(Fixture9c429bd8::class, c: 'foo'); + $this->assertInstanceOf(Fixture289a24a0::class, $a); + $this->assertInstanceOf(Fixture3b51a559::class, $b); + $this->assertEquals('foo', $c); + } + + #[DataProvider('containers')] + public function testPositionalArguments(InvokerInterface $container): void + { + $this->expectException(ContainerException::class); + + $container(Fixture9c429bd8::class, 'foo'); + } + + #[DataProvider('containers')] + public function testAutowiringDisabled(InvokerInterface $container): void + { + $this->expectException(ContainerException::class); + + $container(static fn() => null); + } +} diff --git a/tests/Integration/ScalarValuesTest.php b/tests/Integration/ScalarValuesTest.php index 98572fd..63409e7 100644 --- a/tests/Integration/ScalarValuesTest.php +++ b/tests/Integration/ScalarValuesTest.php @@ -35,10 +35,10 @@ public static function configuration(): Generator public function testCold(array $configuration, array $expectation): void { [$id, $value] = $expectation; - $container = $this->container($configuration); + $container = self::container($configuration); $this->assertEquals($value, $container->get($id)); - $compiled = $this->compiled($configuration); + $compiled = self::compiled($configuration); $this->assertEquals($value, $compiled->get($id)); } }