Skip to content

Commit

Permalink
Invoking callables using container.
Browse files Browse the repository at this point in the history
  • Loading branch information
serge-kvashnin committed Jun 1, 2024
1 parent e9efec8 commit b1a6fc3
Show file tree
Hide file tree
Showing 20 changed files with 195 additions and 43 deletions.
38 changes: 29 additions & 9 deletions docs/content/usage/invoking-callables.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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',
);
```
7 changes: 6 additions & 1 deletion src/Compiler/template.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [];

Expand All @@ -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.');
}
}
12 changes: 8 additions & 4 deletions src/Configurator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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
Expand Down
26 changes: 25 additions & 1 deletion src/Container.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
/**
* @internal
*/
final class Container implements ContainerInterface
final class Container implements ContainerInterface, InvokerInterface
{
/**
* @var array<string, mixed>
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -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(...);
}

Expand Down
12 changes: 12 additions & 0 deletions src/InvokerInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

declare(strict_types=1);

namespace Norvica\Container;

use Closure;

interface InvokerInterface
{
public function __invoke(Closure|callable|array|string $callable, mixed ...$arguments): mixed;
}
17 changes: 10 additions & 7 deletions tests/BaseTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@
namespace Tests\Norvica\Container;

use Norvica\Container\Configurator;
use Norvica\Container\InvokerInterface;
use PHPUnit\Framework\TestCase;
use Psr\Container\ContainerInterface;

abstract class BaseTestCase extends TestCase
{
protected array $files = [];
protected static array $files = [];

public static function setUpBeforeClass(): void
{
Expand All @@ -20,10 +21,10 @@ public static function setUpBeforeClass(): void
}
}

protected function tearDown(): void
public static function tearDownAfterClass(): void
{
parent::tearDown();
foreach ($this->files as $file) {
parent::tearDownAfterClass();
foreach (static::$files as $file) {
if (!is_readable($file)) {
continue;
}
Expand All @@ -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();
}
Expand Down
9 changes: 9 additions & 0 deletions tests/Fixtures/Invoke/Fixture289a24a0.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

namespace Tests\Norvica\Container\Fixtures\Invoke;

final class Fixture289a24a0
{
}
9 changes: 9 additions & 0 deletions tests/Fixtures/Invoke/Fixture3b51a559.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

namespace Tests\Norvica\Container\Fixtures\Invoke;

final class Fixture3b51a559
{
}
18 changes: 18 additions & 0 deletions tests/Fixtures/Invoke/Fixture9c429bd8.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

namespace Tests\Norvica\Container\Fixtures\Invoke;

final readonly class Fixture9c429bd8
{
public function __construct(
private Fixture289a24a0 $a,
) {
}

public function __invoke(Fixture3b51a559 $b, string $c): array
{
return [$this->a, $b, $c];
}
}
6 changes: 3 additions & 3 deletions tests/Integration/AnonymousFunctionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
}

Expand All @@ -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'));
Expand Down
2 changes: 1 addition & 1 deletion tests/Integration/CallTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
}
Expand Down
4 changes: 2 additions & 2 deletions tests/Integration/CircularDependencyTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -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);
}
Expand Down
8 changes: 4 additions & 4 deletions tests/Integration/CollectionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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));
}
}
4 changes: 2 additions & 2 deletions tests/Integration/CombinedTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
4 changes: 2 additions & 2 deletions tests/Integration/ConstructorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
}
}
Loading

0 comments on commit b1a6fc3

Please sign in to comment.