Skip to content

Commit 55465d0

Browse files
committed
feat(container): add Decorator
1 parent b7dc71e commit 55465d0

14 files changed

+286
-0
lines changed

packages/container/src/Container.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,6 @@ public function invoke(ClassReflector|MethodReflector|FunctionReflector|callable
3636
* @param ClassReflector<T>|class-string<T>|class-string<U> $initializerClass
3737
*/
3838
public function addInitializer(ClassReflector|string $initializerClass): self;
39+
40+
public function addDecorator(ClassReflector|string $decoratorClass, ClassReflector|string $decoratedClass): self;
3941
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tempest\Container;
6+
7+
use Attribute;
8+
9+
#[Attribute(Attribute::TARGET_CLASS)]
10+
final readonly class Decorator
11+
{
12+
public function __construct(
13+
public string $decorates,
14+
) {}
15+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tempest\Container;
6+
7+
use Tempest\Discovery\Discovery;
8+
use Tempest\Discovery\DiscoveryLocation;
9+
use Tempest\Discovery\IsDiscovery;
10+
use Tempest\Reflection\ClassReflector;
11+
12+
/**
13+
* @property GenericContainer $container
14+
*/
15+
final class DecoratorDiscovery implements Discovery
16+
{
17+
use IsDiscovery;
18+
19+
public function __construct(
20+
private readonly Container $container,
21+
) {}
22+
23+
public function discover(DiscoveryLocation $location, ClassReflector $class): void
24+
{
25+
$decorator = $class->getAttribute(Decorator::class);
26+
27+
if ($decorator === null) {
28+
return;
29+
}
30+
31+
$this->discoveryItems->add($location, [$class, $decorator]);
32+
}
33+
34+
public function apply(): void
35+
{
36+
foreach ($this->discoveryItems as [$class, $decorator]) {
37+
/** @var Decorator $decorator */
38+
$this->container->addDecorator($class, $decorator->decorates);
39+
}
40+
}
41+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tempest\Container\Exceptions;
6+
7+
use Exception;
8+
9+
final class DecoratorDIdNotImplementInterface extends Exception implements ContainerException
10+
{
11+
public function __construct(
12+
string $className,
13+
string $decoratorName,
14+
string $missingInterface,
15+
) {
16+
$message = "Cannot resolve {$className} because it is decorated by decorator {$decoratorName}, which does not implement {$missingInterface}." . PHP_EOL;
17+
parent::__construct($message);
18+
}
19+
}

packages/container/src/GenericContainer.php

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use ArrayIterator;
88
use Closure;
99
use ReflectionFunction;
10+
use Tempest\Container\Exceptions\DecoratorDIdNotImplementInterface;
1011
use Tempest\Container\Exceptions\DependencyCouldNotBeAutowired;
1112
use Tempest\Container\Exceptions\DependencyCouldNotBeInstantiated;
1213
use Tempest\Container\Exceptions\InvokedCallableWasInvalid;
@@ -35,6 +36,9 @@ public function __construct(
3536

3637
/** @var ArrayIterator<array-key, class-string> $dynamicInitializers */
3738
private ArrayIterator $dynamicInitializers = new ArrayIterator(),
39+
40+
/** @var ArrayIterator<array-key, class-string[]> $decorators */
41+
private ArrayIterator $decorators = new ArrayIterator(),
3842
private ?DependencyChain $chain = null,
3943
) {}
4044

@@ -66,6 +70,13 @@ public function setDynamicInitializers(array $dynamicInitializers): self
6670
return $this;
6771
}
6872

73+
public function setDecorators(array $decorators): self
74+
{
75+
$this->decorators = new ArrayIterator($decorators);
76+
77+
return $this;
78+
}
79+
6980
public function getDefinitions(): array
7081
{
7182
return $this->definitions->getArrayCopy();
@@ -99,6 +110,11 @@ public function getDynamicInitializers(): array
99110
return $this->dynamicInitializers->getArrayCopy();
100111
}
101112

113+
public function getDecorators(): array
114+
{
115+
return $this->decorators->getArrayCopy();
116+
}
117+
102118
public function register(string $className, callable $definition): self
103119
{
104120
$this->definitions[$className] = $definition;
@@ -299,7 +315,28 @@ public function removeInitializer(ClassReflector|string $initializerClass): Cont
299315
return $this;
300316
}
301317

318+
public function addDecorator(ClassReflector|string $decoratorClass, ClassReflector|string $decoratedClass): Container
319+
{
320+
$decoratorClass = is_string($decoratorClass) ? $decoratorClass : $decoratorClass->getName();
321+
$decoratedClass = is_string($decoratedClass) ? $decoratedClass : $decoratedClass->getName();
322+
323+
$this->decorators[$decoratedClass][] = $decoratorClass;
324+
325+
return $this;
326+
}
327+
302328
private function resolve(string $className, null|string|UnitEnum $tag = null, mixed ...$params): ?object
329+
{
330+
$instance = $this->resolveDependency($className, $tag, ...$params);
331+
332+
if ($this->decorators[$className] ?? null) {
333+
$instance = $this->resolveDecorator($className, $instance, $tag, ...$params);
334+
}
335+
336+
return $instance;
337+
}
338+
339+
private function resolveDependency(string $className, null|string|UnitEnum $tag = null, mixed ...$params): ?object
303340
{
304341
$class = new ClassReflector($className);
305342

@@ -602,4 +639,35 @@ private function resolveTaggedName(string $className, null|string|UnitEnum $tag)
602639
? "{$className}#{$tag}"
603640
: $className;
604641
}
642+
643+
private function resolveDecorator(string $className, mixed $instance, null|string|UnitEnum $tag = null, mixed ...$params): ?object
644+
{
645+
foreach ($this->decorators[$className] ?? [] as $decoratorClass) {
646+
$decoratorClassReflector = new ClassReflector($decoratorClass);
647+
$constructor = $decoratorClassReflector->getConstructor();
648+
$parameters = $constructor?->getParameters();
649+
650+
// we look for parameter holding decorated instance
651+
foreach ($parameters ?? [] as $parameter) {
652+
if ($parameter->getType()->matches($className) === false) {
653+
continue;
654+
}
655+
656+
// we bind the decorated instance to the parameter, so container won't try to resolve it (it would end up as circular dependency)
657+
$params[$parameter->getName()] = $instance;
658+
659+
break;
660+
}
661+
662+
$decorator = $this->resolveDependency($decoratorClass, $tag, ...$params);
663+
664+
if (! ($decorator instanceof $className)) {
665+
throw new DecoratorDIdNotImplementInterface($className, $decoratorClass, $className);
666+
}
667+
668+
$instance = $decorator;
669+
}
670+
671+
return $instance;
672+
}
605673
}

packages/container/tests/ContainerTest.php

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use PHPUnit\Framework\TestCase;
88
use ReflectionClass;
99
use Tempest\Container\Exceptions\CircularDependencyEncountered;
10+
use Tempest\Container\Exceptions\DecoratorDIdNotImplementInterface;
1011
use Tempest\Container\Exceptions\DependencyCouldNotBeAutowired;
1112
use Tempest\Container\Exceptions\DependencyCouldNotBeInstantiated;
1213
use Tempest\Container\Exceptions\InvokedCallableWasInvalid;
@@ -32,6 +33,12 @@
3233
use Tempest\Container\Tests\Fixtures\ContainerObjectDInitializer;
3334
use Tempest\Container\Tests\Fixtures\ContainerObjectE;
3435
use Tempest\Container\Tests\Fixtures\ContainerObjectEInitializer;
36+
use Tempest\Container\Tests\Fixtures\DecoratedClass;
37+
use Tempest\Container\Tests\Fixtures\DecoratedInterface;
38+
use Tempest\Container\Tests\Fixtures\DecoratorClass;
39+
use Tempest\Container\Tests\Fixtures\DecoratorInvalid;
40+
use Tempest\Container\Tests\Fixtures\DecoratorSecondClass;
41+
use Tempest\Container\Tests\Fixtures\DecoratorWithoutConstructor;
3542
use Tempest\Container\Tests\Fixtures\DependencyWithBuiltinDependencies;
3643
use Tempest\Container\Tests\Fixtures\DependencyWithTaggedDependency;
3744
use Tempest\Container\Tests\Fixtures\EnumTag;
@@ -632,4 +639,52 @@ public function test_tag_attribute_with_enum(): void
632639
$this->assertSame('foo', $dependency->foo->name);
633640
$this->assertSame('bar', $dependency->bar->name);
634641
}
642+
643+
public function test_returns_decorated_instance(): void
644+
{
645+
$container = new GenericContainer();
646+
$container->register(DecoratedInterface::class, fn () => new DecoratedClass());
647+
$container->addDecorator(DecoratorClass::class, DecoratedInterface::class);
648+
649+
$instance = $container->get(DecoratedInterface::class);
650+
651+
$this->assertInstanceOf(DecoratorClass::class, $instance);
652+
$this->assertInstanceOf(DecoratedClass::class, $instance->decorated);
653+
}
654+
655+
public function test_returns_multiple_decorated_instance(): void
656+
{
657+
$container = new GenericContainer();
658+
$container->register(DecoratedInterface::class, fn () => new DecoratedClass());
659+
$container->addDecorator(DecoratorClass::class, DecoratedInterface::class);
660+
$container->addDecorator(DecoratorSecondClass::class, DecoratedInterface::class);
661+
662+
$instance = $container->get(DecoratedInterface::class);
663+
664+
$this->assertInstanceOf(DecoratorSecondClass::class, $instance);
665+
$this->assertInstanceOf(DecoratorClass::class, $instance->decorated);
666+
$this->assertInstanceOf(DecoratedClass::class, $instance->decorated->decorated);
667+
}
668+
669+
public function test_throws_on_decorator_not_implementing_interface(): void
670+
{
671+
$container = new GenericContainer();
672+
$container->register(DecoratedInterface::class, fn () => new DecoratedClass());
673+
$container->addDecorator(DecoratorInvalid::class, DecoratedInterface::class);
674+
675+
$this->expectException(DecoratorDIdNotImplementInterface::class);
676+
677+
$container->get(DecoratedInterface::class);
678+
}
679+
680+
public function test_returns_decorator_without_constructor(): void
681+
{
682+
$container = new GenericContainer();
683+
$container->register(DecoratedInterface::class, fn () => new DecoratedClass());
684+
$container->addDecorator(DecoratorWithoutConstructor::class, DecoratedInterface::class);
685+
686+
$instance = $container->get(DecoratedInterface::class);
687+
688+
$this->assertInstanceOf(DecoratorWithoutConstructor::class, $instance);
689+
}
635690
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tempest\Container\Tests\Fixtures;
6+
7+
class DecoratedClass implements DecoratedInterface
8+
{
9+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tempest\Container\Tests\Fixtures;
6+
7+
interface DecoratedInterface
8+
{
9+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tempest\Container\Tests\Fixtures;
6+
7+
use Tempest\Container\Decorator;
8+
9+
#[Decorator(DecoratedInterface::class)]
10+
class DecoratorClass implements DecoratedInterface
11+
{
12+
public function __construct(
13+
public DecoratedInterface $decorated,
14+
) {}
15+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tempest\Container\Tests\Fixtures;
6+
7+
use Tempest\Container\Decorator;
8+
9+
#[Decorator(DecoratedInterface::class)]
10+
class DecoratorInvalid
11+
{
12+
public function __construct(
13+
public DecoratedInterface $decorated,
14+
) {}
15+
}

0 commit comments

Comments
 (0)