diff --git a/src/Symfony/Component/DependencyInjection/Attribute/Lazy.php b/src/Symfony/Component/DependencyInjection/Attribute/Lazy.php new file mode 100644 index 0000000000000..54de2fed138a5 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Attribute/Lazy.php @@ -0,0 +1,21 @@ + + * + * 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_CLASS | \Attribute::TARGET_PARAMETER)] +class Lazy +{ + public function __construct( + public bool|string|null $lazy = true, + ) { + } +} diff --git a/src/Symfony/Component/DependencyInjection/CHANGELOG.md b/src/Symfony/Component/DependencyInjection/CHANGELOG.md index ace4f5056cf8b..96ca110562db9 100644 --- a/src/Symfony/Component/DependencyInjection/CHANGELOG.md +++ b/src/Symfony/Component/DependencyInjection/CHANGELOG.md @@ -6,6 +6,7 @@ CHANGELOG * Add argument `$prepend` to `ContainerConfigurator::extension()` to prepend the configuration instead of appending it * Have `ServiceLocator` implement `ServiceCollectionInterface` + * Add `#[Lazy]` attribute as shortcut for `#[Autowire(lazy: [bool|string])]` and `#[Autoconfigure(lazy: [bool|string])]` 7.0 --- diff --git a/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php b/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php index d6564d409fc7c..1c416486bef1f 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\Attribute\Autowire; use Symfony\Component\DependencyInjection\Attribute\AutowireCallable; use Symfony\Component\DependencyInjection\Attribute\AutowireDecorated; +use Symfony\Component\DependencyInjection\Attribute\Lazy; use Symfony\Component\DependencyInjection\Attribute\Target; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -299,7 +300,13 @@ private function autowireMethod(\ReflectionFunctionAbstract $reflectionMethod, a }; if ($checkAttributes) { - foreach ($parameter->getAttributes(Autowire::class, \ReflectionAttribute::IS_INSTANCEOF) as $attribute) { + $attributes = array_merge($parameter->getAttributes(Autowire::class, \ReflectionAttribute::IS_INSTANCEOF), $parameter->getAttributes(Lazy::class, \ReflectionAttribute::IS_INSTANCEOF)); + + if (1 < \count($attributes)) { + throw new AutowiringFailedException($this->currentId, 'Using both attributes #[Lazy] and #[Autowire] on an argument is not allowed; use the "lazy" parameter of #[Autowire] instead.'); + } + + foreach ($attributes as $attribute) { $attribute = $attribute->newInstance(); $invalidBehavior = $parameter->allowsNull() ? ContainerInterface::NULL_ON_INVALID_REFERENCE : ContainerBuilder::EXCEPTION_ON_INVALID_REFERENCE; diff --git a/src/Symfony/Component/DependencyInjection/Compiler/RegisterAutoconfigureAttributesPass.php b/src/Symfony/Component/DependencyInjection/Compiler/RegisterAutoconfigureAttributesPass.php index d479743ecd301..ec40eee2d3872 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/RegisterAutoconfigureAttributesPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/RegisterAutoconfigureAttributesPass.php @@ -12,8 +12,10 @@ namespace Symfony\Component\DependencyInjection\Compiler; use Symfony\Component\DependencyInjection\Attribute\Autoconfigure; +use Symfony\Component\DependencyInjection\Attribute\Lazy; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Exception\AutoconfigureFailedException; use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; /** @@ -42,7 +44,16 @@ public function accept(Definition $definition): bool public function processClass(ContainerBuilder $container, \ReflectionClass $class): void { - foreach ($class->getAttributes(Autoconfigure::class, \ReflectionAttribute::IS_INSTANCEOF) as $attribute) { + $autoconfigure = $class->getAttributes(Autoconfigure::class, \ReflectionAttribute::IS_INSTANCEOF); + $lazy = $class->getAttributes(Lazy::class, \ReflectionAttribute::IS_INSTANCEOF); + + if ($autoconfigure && $lazy) { + throw new AutoconfigureFailedException($class->name, 'Using both attributes #[Lazy] and #[Autoconfigure] on an argument is not allowed; use the "lazy" parameter of #[Autoconfigure] instead.'); + } + + $attributes = array_merge($autoconfigure, $lazy); + + foreach ($attributes as $attribute) { self::registerForAutoconfiguration($container, $class, $attribute); } } diff --git a/src/Symfony/Component/DependencyInjection/Exception/AutoconfigureFailedException.php b/src/Symfony/Component/DependencyInjection/Exception/AutoconfigureFailedException.php new file mode 100644 index 0000000000000..f7ce978599cf2 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Exception/AutoconfigureFailedException.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Exception; + +class AutoconfigureFailedException extends AutowiringFailedException +{ +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php index 3045367aad1a4..62ed73a767cf9 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php @@ -1368,4 +1368,29 @@ public function testNestedAttributes() ]; $this->assertEquals($expected, $container->getDefinition(AutowireNestedAttributes::class)->getArgument(0)); } + + public function testLazyServiceAttribute() + { + $container = new ContainerBuilder(); + $container->register('a', A::class)->setAutowired(true); + $container->register('foo', LazyServiceAttributeAutowiring::class)->setAutowired(true); + + (new AutowirePass())->process($container); + + $expected = new Reference('.lazy.'.A::class); + $this->assertEquals($expected, $container->getDefinition('foo')->getArgument(0)); + } + + public function testLazyNotCompatibleWithAutowire() + { + $container = new ContainerBuilder(); + $container->register('a', A::class)->setAutowired(true); + $container->register('foo', LazyAutowireServiceAttributesAutowiring::class)->setAutowired(true); + + try { + (new AutowirePass())->process($container); + } catch (AutowiringFailedException $e) { + $this->assertSame('Using both attributes #[Lazy] and #[Autowire] on an argument is not allowed; use the "lazy" parameter of #[Autowire] instead.', $e->getMessage()); + } + } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterAutoconfigureAttributesPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterAutoconfigureAttributesPassTest.php index 877c50f027fa2..f1f70ed30cd9a 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterAutoconfigureAttributesPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterAutoconfigureAttributesPassTest.php @@ -16,9 +16,13 @@ use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\Compiler\RegisterAutoconfigureAttributesPass; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Exception\AutoconfigureFailedException; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\Tests\Fixtures\AutoconfigureAttributed; use Symfony\Component\DependencyInjection\Tests\Fixtures\AutoconfiguredInterface; +use Symfony\Component\DependencyInjection\Tests\Fixtures\LazyAutoconfigured; +use Symfony\Component\DependencyInjection\Tests\Fixtures\LazyLoaded; +use Symfony\Component\DependencyInjection\Tests\Fixtures\MultipleAutoconfigureAttributed; use Symfony\Component\DependencyInjection\Tests\Fixtures\ParentNotExists; use Symfony\Component\DependencyInjection\Tests\Fixtures\StaticConstructorAutoconfigure; @@ -104,4 +108,46 @@ public function testStaticConstructor() ; $this->assertEquals([StaticConstructorAutoconfigure::class => $expected], $container->getAutoconfiguredInstanceof()); } + + public function testLazyServiceAttribute() + { + $container = new ContainerBuilder(); + $container->register('foo', LazyLoaded::class) + ->setAutoconfigured(true); + + (new RegisterAutoconfigureAttributesPass())->process($container); + + $expected = (new ChildDefinition('')) + ->setLazy(true) + ; + $this->assertEquals([LazyLoaded::class => $expected], $container->getAutoconfiguredInstanceof()); + } + + public function testLazyNotCompatibleWithAutoconfigureAttribute() + { + $container = new ContainerBuilder(); + $container->register('foo', LazyAutoconfigured::class) + ->setAutoconfigured(true); + + try { + (new RegisterAutoconfigureAttributesPass())->process($container); + } catch (AutoconfigureFailedException $e) { + $this->assertSame('Using both attributes #[Lazy] and #[Autoconfigure] on an argument is not allowed; use the "lazy" parameter of #[Autoconfigure] instead.', $e->getMessage()); + } + } + + public function testMultipleAutoconfigureAllowed() + { + $container = new ContainerBuilder(); + $container->register('foo', MultipleAutoconfigureAttributed::class) + ->setAutoconfigured(true); + + (new RegisterAutoconfigureAttributesPass())->process($container); + + $expected = (new ChildDefinition('')) + ->addTag('foo') + ->addTag('bar') + ; + $this->assertEquals([MultipleAutoconfigureAttributed::class => $expected], $container->getAutoconfiguredInstanceof()); + } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/LazyAutoconfigured.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/LazyAutoconfigured.php new file mode 100644 index 0000000000000..7145e18ee9f6a --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/LazyAutoconfigured.php @@ -0,0 +1,11 @@ +