diff --git a/src/Symfony/Component/DependencyInjection/Compiler/AutoDecorationServicePass.php b/src/Symfony/Component/DependencyInjection/Compiler/AutoDecorationServicePass.php new file mode 100644 index 0000000000000..05ae0d09cc15a --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Compiler/AutoDecorationServicePass.php @@ -0,0 +1,71 @@ + + * + * 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\ContainerBuilder; +use Symfony\Component\DependencyInjection\DecorationPriorityAwareInterface; +use Symfony\Component\DependencyInjection\DecoratorInterface; + +/** + * Adds decorated service to definition of services implementing DecoratorInterface. + * + * @author Grégory SURACI + */ +class AutoDecorationServicePass implements CompilerPassInterface +{ + private $throwOnException; + + public function __construct(bool $throwOnException = true) + { + $this->throwOnException = $throwOnException; + } + + /** + * {@inheritdoc} + */ + public function process(ContainerBuilder $container) + { + foreach ($container->getDefinitions() as $definition) { + $className = $definition->getClass(); + + if (null === $className) { + continue; + } + + try { + $classInterfaces = class_implements($className); + + if (!\in_array(DecoratorInterface::class, $classInterfaces)) { + continue; + } + + if (\in_array(DecorationPriorityAwareInterface::class, $classInterfaces)) { + $definition->setDecoratedService( + $className::getDecoratedServiceId(), + null, + $className::getDecorationPriority() + ); + + continue; + } + + $definition->setDecoratedService($className::getDecoratedServiceId()); + } catch (\Throwable $e) { + if ($this->throwOnException) { + throw $e; + } + + continue; + } + } + } +} diff --git a/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php b/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php index 6cbc5544adbe8..36398d07cde4d 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php @@ -61,6 +61,7 @@ public function __construct() new AutowireRequiredPropertiesPass(), new ResolveBindingsPass(), new ServiceLocatorTagPass(), + new AutoDecorationServicePass(false), new DecoratorServicePass(), new CheckDefinitionValidityPass(), new AutowirePass(false), diff --git a/src/Symfony/Component/DependencyInjection/DecorationPriorityAwareInterface.php b/src/Symfony/Component/DependencyInjection/DecorationPriorityAwareInterface.php new file mode 100644 index 0000000000000..67d7c688d8e3a --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/DecorationPriorityAwareInterface.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; + +/** + * DecorationPriorityAwareInterface sets the decoration_priority value for a class implementing DecoratorInterface. + * + * @author Grégory SURACI + */ +interface DecorationPriorityAwareInterface +{ + /** + * @return int the decoration priority + */ + public static function getDecorationPriority(): int; +} diff --git a/src/Symfony/Component/DependencyInjection/DecoratorInterface.php b/src/Symfony/Component/DependencyInjection/DecoratorInterface.php new file mode 100644 index 0000000000000..8b8dade2d5f88 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/DecoratorInterface.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; + +/** + * DecoratorInterface defines a decorator class without configuration. + * + * @author Grégory SURACI + */ +interface DecoratorInterface +{ + /** + * @return string the serviceId/FQCN that will be decorated by this interface's implementation + */ + public static function getDecoratedServiceId(): string; +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutoDecorationServicePassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutoDecorationServicePassTest.php new file mode 100644 index 0000000000000..ed31a5c0adca3 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutoDecorationServicePassTest.php @@ -0,0 +1,100 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Tests\Compiler; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\DependencyInjection\Compiler\AutoDecorationServicePass; +use Symfony\Component\DependencyInjection\Compiler\DecoratorServicePass; +use Symfony\Component\DependencyInjection\Compiler\ResolveClassPass; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Tests\Fixtures\Dummy; +use Symfony\Component\DependencyInjection\Tests\Fixtures\DummyDecorator1; +use Symfony\Component\DependencyInjection\Tests\Fixtures\DummyDecorator2; +use Symfony\Component\DependencyInjection\Tests\Fixtures\DummyDecorator3; + +class AutoDecorationServicePassTest extends TestCase +{ + public function testProcessUsingFQCN() + { + $container = new ContainerBuilder(); + $dummyDefinition = $container + ->register(Dummy::class) + ->setPublic(true) + ; + $decoratorDefinition = $container + ->register('dummy.extended', DummyDecorator1::class) + ->setPublic(true) + ; + + $this->process($container); + + $this->assertSame($dummyDefinition, $container->getDefinition('dummy.extended.inner')); + $this->assertFalse($container->getDefinition('dummy.extended.inner')->isPublic()); + + $this->assertNull($decoratorDefinition->getDecoratedService()); + } + + public function testProcessUsingStringServiceId() + { + $container = new ContainerBuilder(); + $dummyDefinition = $container + ->register('dummy', Dummy::class) + ->setPublic(true) + ; + $decoratorDefinition = $container + ->register('dummy.extended', DummyDecorator2::class) + ->setPublic(true) + ; + + $this->process($container); + + $this->assertSame($dummyDefinition, $container->getDefinition('dummy.extended.inner')); + $this->assertFalse($container->getDefinition('dummy.extended.inner')->isPublic()); + + $this->assertNull($decoratorDefinition->getDecoratedService()); + } + + public function testProcessWithDecorationPriority() + { + $container = new ContainerBuilder(); + $dummyDefinition = $container + ->register(Dummy::class) + ->setPublic(true) + ; + $decoratorWithHighPriorityDefinition = $container + ->register('dummy.extended', DummyDecorator3::class) + ->setPublic(true) + ; + $decoratorDefinition = $container + ->register('dummy.extended.extended', DummyDecorator1::class) + ->setPublic(true) + ; + + $this->process($container); + + $this->assertSame($dummyDefinition, $container->getDefinition('dummy.extended.inner')); + $this->assertFalse($container->getDefinition('dummy.extended.inner')->isPublic()); + + $this->assertEquals('dummy.extended', $container->getAlias('dummy.extended.extended.inner')); + $this->assertFalse($container->getAlias('dummy.extended.extended.inner')->isPublic()); + + $this->assertNull($decoratorWithHighPriorityDefinition->getDecoratedService()); + $this->assertNull($decoratorDefinition->getDecoratedService()); + } + + protected function process(ContainerBuilder $container) + { + (new ResolveClassPass())->process($container); + (new AutoDecorationServicePass())->process($container); + (new DecoratorServicePass())->process($container); + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/Dummy.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/Dummy.php new file mode 100644 index 0000000000000..4d57f16303f2d --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/Dummy.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Tests\Fixtures; + +class Dummy implements DummyInterface +{ + public function sayHello(): string + { + return 'Hello Dummy'; + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/DummyDecorator1.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/DummyDecorator1.php new file mode 100644 index 0000000000000..145ecee7c6bd6 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/DummyDecorator1.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Tests\Fixtures; + +use Symfony\Component\DependencyInjection\DecoratorInterface; + +class DummyDecorator1 implements DecoratorInterface, DummyInterface +{ + /** + * @var DummyInterface + */ + protected $decorated; + + public function __construct(DummyInterface $decorated) + { + $this->decorated = $decorated; + } + + public static function getDecoratedServiceId(): string + { + return Dummy::class; + } + + public function sayHello(): string + { + return sprintf('%s & Decorator1', $this->decorated->sayHello()); + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/DummyDecorator2.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/DummyDecorator2.php new file mode 100644 index 0000000000000..d416e0f91ed54 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/DummyDecorator2.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Tests\Fixtures; + +use Symfony\Component\DependencyInjection\DecoratorInterface; + +class DummyDecorator2 implements DecoratorInterface, DummyInterface +{ + /** + * @var DummyInterface + */ + protected $decorated; + + public function __construct(DummyInterface $decorated) + { + $this->decorated = $decorated; + } + + public static function getDecoratedServiceId(): string + { + return 'dummy'; + } + + public function sayHello(): string + { + return sprintf('%s & Decorator2', $this->decorated->sayHello()); + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/DummyDecorator3.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/DummyDecorator3.php new file mode 100644 index 0000000000000..431b750be01f3 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/DummyDecorator3.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Tests\Fixtures; + +use Symfony\Component\DependencyInjection\DecorationPriorityAwareInterface; + +class DummyDecorator3 extends DummyDecorator1 implements DecorationPriorityAwareInterface, DummyInterface +{ + public static function getDecorationPriority(): int + { + return 42; + } + + public function sayHello(): string + { + return sprintf('%s & Decorator3', $this->decorated->sayHello()); + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/DummyInterface.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/DummyInterface.php new file mode 100644 index 0000000000000..f3a4054c49aeb --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/DummyInterface.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\Tests\Fixtures; + +interface DummyInterface +{ + public function sayHello(): string; +}