From 55465d086fca0ac02dd608be0b2c5a114b4f0a30 Mon Sep 17 00:00:00 2001 From: aazsamir Date: Sun, 24 Aug 2025 10:55:46 +0200 Subject: [PATCH] feat(container): add `Decorator` --- packages/container/src/Container.php | 2 + packages/container/src/Decorator.php | 15 ++++ packages/container/src/DecoratorDiscovery.php | 41 +++++++++++ .../DecoratorDIdNotImplementInterface.php | 19 ++++++ packages/container/src/GenericContainer.php | 68 +++++++++++++++++++ packages/container/tests/ContainerTest.php | 55 +++++++++++++++ .../tests/Fixtures/DecoratedClass.php | 9 +++ .../tests/Fixtures/DecoratedInterface.php | 9 +++ .../tests/Fixtures/DecoratorClass.php | 15 ++++ .../tests/Fixtures/DecoratorInvalid.php | 15 ++++ .../tests/Fixtures/DecoratorSecondClass.php | 15 ++++ .../Fixtures/DecoratorWithoutConstructor.php | 12 ++++ packages/reflection/src/TypeReflector.php | 4 ++ .../Commands/ContainerShowCommandTest.php | 7 ++ 14 files changed, 286 insertions(+) create mode 100644 packages/container/src/Decorator.php create mode 100644 packages/container/src/DecoratorDiscovery.php create mode 100644 packages/container/src/Exceptions/DecoratorDIdNotImplementInterface.php create mode 100644 packages/container/tests/Fixtures/DecoratedClass.php create mode 100644 packages/container/tests/Fixtures/DecoratedInterface.php create mode 100644 packages/container/tests/Fixtures/DecoratorClass.php create mode 100644 packages/container/tests/Fixtures/DecoratorInvalid.php create mode 100644 packages/container/tests/Fixtures/DecoratorSecondClass.php create mode 100644 packages/container/tests/Fixtures/DecoratorWithoutConstructor.php diff --git a/packages/container/src/Container.php b/packages/container/src/Container.php index dc970491b..05908104f 100644 --- a/packages/container/src/Container.php +++ b/packages/container/src/Container.php @@ -36,4 +36,6 @@ public function invoke(ClassReflector|MethodReflector|FunctionReflector|callable * @param ClassReflector|class-string|class-string $initializerClass */ public function addInitializer(ClassReflector|string $initializerClass): self; + + public function addDecorator(ClassReflector|string $decoratorClass, ClassReflector|string $decoratedClass): self; } diff --git a/packages/container/src/Decorator.php b/packages/container/src/Decorator.php new file mode 100644 index 000000000..45bea188e --- /dev/null +++ b/packages/container/src/Decorator.php @@ -0,0 +1,15 @@ +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); + } + } +} diff --git a/packages/container/src/Exceptions/DecoratorDIdNotImplementInterface.php b/packages/container/src/Exceptions/DecoratorDIdNotImplementInterface.php new file mode 100644 index 000000000..cb15b54fa --- /dev/null +++ b/packages/container/src/Exceptions/DecoratorDIdNotImplementInterface.php @@ -0,0 +1,19 @@ + $dynamicInitializers */ private ArrayIterator $dynamicInitializers = new ArrayIterator(), + + /** @var ArrayIterator $decorators */ + private ArrayIterator $decorators = new ArrayIterator(), private ?DependencyChain $chain = null, ) {} @@ -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(); @@ -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; @@ -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); @@ -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; + } } diff --git a/packages/container/tests/ContainerTest.php b/packages/container/tests/ContainerTest.php index 8cfe9a893..9fb80dc26 100644 --- a/packages/container/tests/ContainerTest.php +++ b/packages/container/tests/ContainerTest.php @@ -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; @@ -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; @@ -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); + } } diff --git a/packages/container/tests/Fixtures/DecoratedClass.php b/packages/container/tests/Fixtures/DecoratedClass.php new file mode 100644 index 000000000..4664908c6 --- /dev/null +++ b/packages/container/tests/Fixtures/DecoratedClass.php @@ -0,0 +1,9 @@ +isInterface()) { + return $input instanceof $this->definition; + } + if ($this->isIterable()) { return is_iterable($input); } diff --git a/tests/Integration/Container/Commands/ContainerShowCommandTest.php b/tests/Integration/Container/Commands/ContainerShowCommandTest.php index bef2344d9..a62d413ba 100644 --- a/tests/Integration/Container/Commands/ContainerShowCommandTest.php +++ b/tests/Integration/Container/Commands/ContainerShowCommandTest.php @@ -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; + } }, );