From d8a46808370937eda0205008ddcbad172514e070 Mon Sep 17 00:00:00 2001 From: "hubert.lenoir" Date: Wed, 23 Mar 2022 17:52:31 +0100 Subject: [PATCH] [DependencyInjection] add AsDecorator class attribute and InnerService parameter attribute --- .../Attribute/AsDecorator.php | 25 ++++++++++ .../Attribute/InnerService.php | 17 +++++++ .../Compiler/AutowireAsDecoratorPass.php | 49 +++++++++++++++++++ .../Compiler/AutowirePass.php | 7 +++ .../Compiler/PassConfig.php | 1 + .../Tests/Compiler/AutowirePassTest.php | 22 +++++++++ .../includes/autowiring_classes_80.php | 35 +++++++++++++ 7 files changed, 156 insertions(+) create mode 100644 src/Symfony/Component/DependencyInjection/Attribute/AsDecorator.php create mode 100644 src/Symfony/Component/DependencyInjection/Attribute/InnerService.php create mode 100644 src/Symfony/Component/DependencyInjection/Compiler/AutowireAsDecoratorPass.php diff --git a/src/Symfony/Component/DependencyInjection/Attribute/AsDecorator.php b/src/Symfony/Component/DependencyInjection/Attribute/AsDecorator.php new file mode 100644 index 0000000000000..0f80c16e974dc --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Attribute/AsDecorator.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Attribute; + +use Symfony\Component\DependencyInjection\ContainerInterface; + +#[\Attribute(\Attribute::TARGET_CLASS)] +class AsDecorator +{ + public function __construct( + public string $decorates, + public int $priority = 0, + public int $onInvalid = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, + ) { + } +} diff --git a/src/Symfony/Component/DependencyInjection/Attribute/InnerService.php b/src/Symfony/Component/DependencyInjection/Attribute/InnerService.php new file mode 100644 index 0000000000000..46e987e435183 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Attribute/InnerService.php @@ -0,0 +1,17 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Attribute; + +#[\Attribute(\Attribute::TARGET_PARAMETER)] +class InnerService +{ +} diff --git a/src/Symfony/Component/DependencyInjection/Compiler/AutowireAsDecoratorPass.php b/src/Symfony/Component/DependencyInjection/Compiler/AutowireAsDecoratorPass.php new file mode 100644 index 0000000000000..66eed9a37fa77 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Compiler/AutowireAsDecoratorPass.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Compiler; + +use Symfony\Component\DependencyInjection\Attribute\AsDecorator; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; + +/** + * Reads #[AsDecorator] attributes on definitions that are autowired + * and don't have the "container.ignore_attributes" tag. + */ +final class AutowireAsDecoratorPass implements CompilerPassInterface +{ + /** + * {@inheritdoc} + */ + public function process(ContainerBuilder $container) + { + foreach ($container->getDefinitions() as $definition) { + if ($this->accept($definition) && $reflectionClass = $container->getReflectionClass($definition->getClass(), false)) { + $this->processClass($definition, $reflectionClass); + } + } + } + + private function accept(Definition $definition): bool + { + return !$definition->hasTag('container.ignore_attributes') && $definition->isAutowired(); + } + + private function processClass(Definition $definition, \ReflectionClass $reflectionClass) + { + foreach ($reflectionClass->getAttributes(AsDecorator::class, \ReflectionAttribute::IS_INSTANCEOF) as $attribute) { + $attribute = $attribute->newInstance(); + + $definition->setDecoratedService($attribute->decorates, null, $attribute->priority, $attribute->onInvalid); + } + } +} diff --git a/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php b/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php index 25fa2cfa584fc..a47b6ba69be1f 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php @@ -15,6 +15,7 @@ use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument; use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument; use Symfony\Component\DependencyInjection\Attribute\Autowire; +use Symfony\Component\DependencyInjection\Attribute\InnerService; use Symfony\Component\DependencyInjection\Attribute\TaggedIterator; use Symfony\Component\DependencyInjection\Attribute\TaggedLocator; use Symfony\Component\DependencyInjection\Attribute\Target; @@ -271,6 +272,12 @@ private function autowireMethod(\ReflectionFunctionAbstract $reflectionMethod, a break; } + + if (InnerService::class === $attribute->getName()) { + $arguments[$index] = new Reference($this->currentId.'.inner', ContainerInterface::NULL_ON_INVALID_REFERENCE); + + break; + } } if ('' !== ($arguments[$index] ?? '')) { diff --git a/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php b/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php index 4373833180319..3acbe26de0d3c 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php @@ -43,6 +43,7 @@ public function __construct() 100 => [ new ResolveClassPass(), new RegisterAutoconfigureAttributesPass(), + new AutowireAsDecoratorPass(), new AttributeAutoconfigurationPass(), new ResolveInstanceofConditionalsPass(), new RegisterEnvVarProcessorsPass(), diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php index 10a7eed918e84..c9422d98b26e1 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php @@ -15,6 +15,7 @@ use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; use Symfony\Component\Config\FileLocator; +use Symfony\Component\DependencyInjection\Compiler\AutowireAsDecoratorPass; use Symfony\Component\DependencyInjection\Compiler\AutowirePass; use Symfony\Component\DependencyInjection\Compiler\AutowireRequiredMethodsPass; use Symfony\Component\DependencyInjection\Compiler\DecoratorServicePass; @@ -1164,4 +1165,25 @@ public function testAutowireAttribute() $this->assertSame('@bar', $service->escapedRawValue); $this->assertNull($service->invalid); } + + public function testAsDecoratorAttribute() + { + $container = new ContainerBuilder(); + + $container->register(AsDecoratorFoo::class); + $container->register(AsDecoratorBar10::class)->setAutowired(true)->setArgument(0, 'arg1'); + $container->register(AsDecoratorBar20::class)->setAutowired(true)->setArgument(0, 'arg1'); + $container->register(AsDecoratorBaz::class)->setAutowired(true); + + (new ResolveClassPass())->process($container); + (new AutowireAsDecoratorPass())->process($container); + (new DecoratorServicePass())->process($container); + (new AutowirePass())->process($container); + + $this->assertSame(AsDecoratorBar10::class.'.inner', (string) $container->getDefinition(AsDecoratorBar10::class)->getArgument(1)); + + $this->assertSame(AsDecoratorBar20::class.'.inner', (string) $container->getDefinition(AsDecoratorBar20::class)->getArgument(1)); + $this->assertSame(AsDecoratorBaz::class.'.inner', (string) $container->getDefinition(AsDecoratorBaz::class)->getArgument(0)); + $this->assertSame(2, $container->getDefinition(AsDecoratorBaz::class)->getArgument(0)->getInvalidBehavior()); + } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/autowiring_classes_80.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/autowiring_classes_80.php index 80c3251fa75d7..de2583d3b2ed3 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/autowiring_classes_80.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/autowiring_classes_80.php @@ -2,7 +2,10 @@ namespace Symfony\Component\DependencyInjection\Tests\Compiler; +use Symfony\Component\DependencyInjection\Attribute\AsDecorator; use Symfony\Component\DependencyInjection\Attribute\Autowire; +use Symfony\Component\DependencyInjection\Attribute\InnerService; +use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Contracts\Service\Attribute\Required; class AutowireSetter @@ -50,3 +53,35 @@ public function __construct( ) { } } + +interface AsDecoratorInterface +{ +} + +class AsDecoratorFoo implements AsDecoratorInterface +{ +} + +#[AsDecorator(decorates: AsDecoratorFoo::class, priority: 10)] +class AsDecoratorBar10 implements AsDecoratorInterface +{ + public function __construct(string $arg1, #[InnerService] AsDecoratorInterface $inner) + { + } +} + +#[AsDecorator(decorates: AsDecoratorFoo::class, priority: 20)] +class AsDecoratorBar20 implements AsDecoratorInterface +{ + public function __construct(string $arg1, #[InnerService] AsDecoratorInterface $inner) + { + } +} + +#[AsDecorator(decorates: \NonExistent::class, onInvalid: ContainerInterface::NULL_ON_INVALID_REFERENCE)] +class AsDecoratorBaz implements AsDecoratorInterface +{ + public function __construct(#[InnerService] AsDecoratorInterface $inner = null) + { + } +}