Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/container/src/Container.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,6 @@ public function invoke(ClassReflector|MethodReflector|FunctionReflector|callable
* @param ClassReflector<T>|class-string<T>|class-string<U> $initializerClass
*/
public function addInitializer(ClassReflector|string $initializerClass): self;

public function addDecorator(ClassReflector|string $decoratorClass, ClassReflector|string $decoratedClass): self;
}
15 changes: 15 additions & 0 deletions packages/container/src/Decorator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace Tempest\Container;

use Attribute;

#[Attribute(Attribute::TARGET_CLASS)]
final readonly class Decorator
{
public function __construct(
public string $decorates,
) {}
}
41 changes: 41 additions & 0 deletions packages/container/src/DecoratorDiscovery.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

namespace Tempest\Container;

use Tempest\Discovery\Discovery;
use Tempest\Discovery\DiscoveryLocation;
use Tempest\Discovery\IsDiscovery;
use Tempest\Reflection\ClassReflector;

/**
* @property GenericContainer $container
*/
final class DecoratorDiscovery implements Discovery
{
use IsDiscovery;

public function __construct(
private readonly Container $container,
) {}

public function discover(DiscoveryLocation $location, ClassReflector $class): void
{
$decorator = $class->getAttribute(Decorator::class);

if ($decorator === null) {
return;
}

$this->discoveryItems->add($location, [$class, $decorator]);
}

public function apply(): void
{
foreach ($this->discoveryItems as [$class, $decorator]) {
/** @var Decorator $decorator */
$this->container->addDecorator($class, $decorator->decorates);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

namespace Tempest\Container\Exceptions;

use Exception;

final class DecoratorDIdNotImplementInterface extends Exception implements ContainerException
{
public function __construct(
string $className,
string $decoratorName,
string $missingInterface,
) {
$message = "Cannot resolve {$className} because it is decorated by decorator {$decoratorName}, which does not implement {$missingInterface}." . PHP_EOL;
parent::__construct($message);
}
}
68 changes: 68 additions & 0 deletions packages/container/src/GenericContainer.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use ArrayIterator;
use Closure;
use ReflectionFunction;
use Tempest\Container\Exceptions\DecoratorDIdNotImplementInterface;
use Tempest\Container\Exceptions\DependencyCouldNotBeAutowired;
use Tempest\Container\Exceptions\DependencyCouldNotBeInstantiated;
use Tempest\Container\Exceptions\InvokedCallableWasInvalid;
Expand Down Expand Up @@ -35,6 +36,9 @@ public function __construct(

/** @var ArrayIterator<array-key, class-string> $dynamicInitializers */
private ArrayIterator $dynamicInitializers = new ArrayIterator(),

/** @var ArrayIterator<array-key, class-string[]> $decorators */
private ArrayIterator $decorators = new ArrayIterator(),
private ?DependencyChain $chain = null,
) {}

Expand Down Expand Up @@ -66,6 +70,13 @@ public function setDynamicInitializers(array $dynamicInitializers): self
return $this;
}

public function setDecorators(array $decorators): self
{
$this->decorators = new ArrayIterator($decorators);

return $this;
}

public function getDefinitions(): array
{
return $this->definitions->getArrayCopy();
Expand Down Expand Up @@ -99,6 +110,11 @@ public function getDynamicInitializers(): array
return $this->dynamicInitializers->getArrayCopy();
}

public function getDecorators(): array
{
return $this->decorators->getArrayCopy();
}

public function register(string $className, callable $definition): self
{
$this->definitions[$className] = $definition;
Expand Down Expand Up @@ -299,7 +315,28 @@ public function removeInitializer(ClassReflector|string $initializerClass): Cont
return $this;
}

public function addDecorator(ClassReflector|string $decoratorClass, ClassReflector|string $decoratedClass): Container
{
$decoratorClass = is_string($decoratorClass) ? $decoratorClass : $decoratorClass->getName();
$decoratedClass = is_string($decoratedClass) ? $decoratedClass : $decoratedClass->getName();

$this->decorators[$decoratedClass][] = $decoratorClass;

return $this;
}

private function resolve(string $className, null|string|UnitEnum $tag = null, mixed ...$params): ?object
{
$instance = $this->resolveDependency($className, $tag, ...$params);

if ($this->decorators[$className] ?? null) {
$instance = $this->resolveDecorator($className, $instance, $tag, ...$params);
}

return $instance;
}

private function resolveDependency(string $className, null|string|UnitEnum $tag = null, mixed ...$params): ?object
{
$class = new ClassReflector($className);

Expand Down Expand Up @@ -602,4 +639,35 @@ private function resolveTaggedName(string $className, null|string|UnitEnum $tag)
? "{$className}#{$tag}"
: $className;
}

private function resolveDecorator(string $className, mixed $instance, null|string|UnitEnum $tag = null, mixed ...$params): ?object
{
foreach ($this->decorators[$className] ?? [] as $decoratorClass) {
$decoratorClassReflector = new ClassReflector($decoratorClass);
$constructor = $decoratorClassReflector->getConstructor();
$parameters = $constructor?->getParameters();

// we look for parameter holding decorated instance
foreach ($parameters ?? [] as $parameter) {
if ($parameter->getType()->matches($className) === false) {
continue;
}

// we bind the decorated instance to the parameter, so container won't try to resolve it (it would end up as circular dependency)
$params[$parameter->getName()] = $instance;

break;
}

$decorator = $this->resolveDependency($decoratorClass, $tag, ...$params);

if (! ($decorator instanceof $className)) {
throw new DecoratorDIdNotImplementInterface($className, $decoratorClass, $className);
}

$instance = $decorator;
}

return $instance;
}
}
55 changes: 55 additions & 0 deletions packages/container/tests/ContainerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use PHPUnit\Framework\TestCase;
use ReflectionClass;
use Tempest\Container\Exceptions\CircularDependencyEncountered;
use Tempest\Container\Exceptions\DecoratorDIdNotImplementInterface;
use Tempest\Container\Exceptions\DependencyCouldNotBeAutowired;
use Tempest\Container\Exceptions\DependencyCouldNotBeInstantiated;
use Tempest\Container\Exceptions\InvokedCallableWasInvalid;
Expand All @@ -32,6 +33,12 @@
use Tempest\Container\Tests\Fixtures\ContainerObjectDInitializer;
use Tempest\Container\Tests\Fixtures\ContainerObjectE;
use Tempest\Container\Tests\Fixtures\ContainerObjectEInitializer;
use Tempest\Container\Tests\Fixtures\DecoratedClass;
use Tempest\Container\Tests\Fixtures\DecoratedInterface;
use Tempest\Container\Tests\Fixtures\DecoratorClass;
use Tempest\Container\Tests\Fixtures\DecoratorInvalid;
use Tempest\Container\Tests\Fixtures\DecoratorSecondClass;
use Tempest\Container\Tests\Fixtures\DecoratorWithoutConstructor;
use Tempest\Container\Tests\Fixtures\DependencyWithBuiltinDependencies;
use Tempest\Container\Tests\Fixtures\DependencyWithTaggedDependency;
use Tempest\Container\Tests\Fixtures\EnumTag;
Expand Down Expand Up @@ -632,4 +639,52 @@ public function test_tag_attribute_with_enum(): void
$this->assertSame('foo', $dependency->foo->name);
$this->assertSame('bar', $dependency->bar->name);
}

public function test_returns_decorated_instance(): void
{
$container = new GenericContainer();
$container->register(DecoratedInterface::class, fn () => new DecoratedClass());
$container->addDecorator(DecoratorClass::class, DecoratedInterface::class);

$instance = $container->get(DecoratedInterface::class);

$this->assertInstanceOf(DecoratorClass::class, $instance);
$this->assertInstanceOf(DecoratedClass::class, $instance->decorated);
}

public function test_returns_multiple_decorated_instance(): void
{
$container = new GenericContainer();
$container->register(DecoratedInterface::class, fn () => new DecoratedClass());
$container->addDecorator(DecoratorClass::class, DecoratedInterface::class);
$container->addDecorator(DecoratorSecondClass::class, DecoratedInterface::class);

$instance = $container->get(DecoratedInterface::class);

$this->assertInstanceOf(DecoratorSecondClass::class, $instance);
$this->assertInstanceOf(DecoratorClass::class, $instance->decorated);
$this->assertInstanceOf(DecoratedClass::class, $instance->decorated->decorated);
}

public function test_throws_on_decorator_not_implementing_interface(): void
{
$container = new GenericContainer();
$container->register(DecoratedInterface::class, fn () => new DecoratedClass());
$container->addDecorator(DecoratorInvalid::class, DecoratedInterface::class);

$this->expectException(DecoratorDIdNotImplementInterface::class);

$container->get(DecoratedInterface::class);
}

public function test_returns_decorator_without_constructor(): void
{
$container = new GenericContainer();
$container->register(DecoratedInterface::class, fn () => new DecoratedClass());
$container->addDecorator(DecoratorWithoutConstructor::class, DecoratedInterface::class);

$instance = $container->get(DecoratedInterface::class);

$this->assertInstanceOf(DecoratorWithoutConstructor::class, $instance);
}
}
9 changes: 9 additions & 0 deletions packages/container/tests/Fixtures/DecoratedClass.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

namespace Tempest\Container\Tests\Fixtures;

class DecoratedClass implements DecoratedInterface
{
}
9 changes: 9 additions & 0 deletions packages/container/tests/Fixtures/DecoratedInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

namespace Tempest\Container\Tests\Fixtures;

interface DecoratedInterface
{
}
15 changes: 15 additions & 0 deletions packages/container/tests/Fixtures/DecoratorClass.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace Tempest\Container\Tests\Fixtures;

use Tempest\Container\Decorator;

#[Decorator(DecoratedInterface::class)]
class DecoratorClass implements DecoratedInterface
{
public function __construct(
public DecoratedInterface $decorated,
) {}
}
15 changes: 15 additions & 0 deletions packages/container/tests/Fixtures/DecoratorInvalid.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace Tempest\Container\Tests\Fixtures;

use Tempest\Container\Decorator;

#[Decorator(DecoratedInterface::class)]
class DecoratorInvalid
{
public function __construct(
public DecoratedInterface $decorated,
) {}
}
15 changes: 15 additions & 0 deletions packages/container/tests/Fixtures/DecoratorSecondClass.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace Tempest\Container\Tests\Fixtures;

use Tempest\Container\Decorator;

#[Decorator(DecoratedInterface::class)]
class DecoratorSecondClass implements DecoratedInterface
{
public function __construct(
public DecoratedInterface $decorated,
) {}
}
12 changes: 12 additions & 0 deletions packages/container/tests/Fixtures/DecoratorWithoutConstructor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

declare(strict_types=1);

namespace Tempest\Container\Tests\Fixtures;

use Tempest\Container\Decorator;

#[Decorator(DecoratedInterface::class)]
class DecoratorWithoutConstructor implements DecoratedInterface
{
}
4 changes: 4 additions & 0 deletions packages/reflection/src/TypeReflector.php
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,10 @@ public function accepts(mixed $input): bool
return $input instanceof $cleanDefinition;
}

if ($this->isInterface()) {
return $input instanceof $this->definition;
}

if ($this->isIterable()) {
return is_iterable($input);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,13 @@ public function addInitializer(mixed $initializerClass): self

return $this;
}

public function addDecorator(mixed $decoratorClass, mixed $decoratedClass): self
{
$this->container->addDecorator($decoratorClass, $decoratedClass);

return $this;
}
},
);

Expand Down
Loading