Skip to content

Commit bdcaee0

Browse files
committed
feature #52922 [DependencyInjection] Add Lazy attribute for classes and arguments (Tiriel)
This PR was squashed before being merged into the 7.1 branch. Discussion ---------- [DependencyInjection] Add Lazy attribute for classes and arguments | Q | A | ------------- | --- | Branch? | 7.1 | Bug fix? | no | New feature? | yes | Deprecations? | no | Issues | # | License | MIT This commit adds a Lazy attribute shortcut for service definitions. The goal here is simply to improve the DX with a single attribute for lazy loaded services, while adding the strict minimum amount of new code. This attribute can be used as a replacement for the `Autowire(lazy: bool|string)` attribute on any autowired argument or for the `Autoconfigure(lazy: bool|string)` attribute on an class definition. Both use case support an optional parameter to specify with class/interface should be used for the proxy. Usage on a class: ```php use Symfony\Component\DependencyInjection\Attribute\Lazy; #[Lazy] class LazyLoaded { // ... } ``` On a method argument: ```php use Symfony\Component\DependencyInjection\Attribute\Lazy; class SuperAwesomeService { public function __construct(#[Lazy] SomeOtherService $service) { // ... } } ``` Commits ------- e53db3e145 [DependencyInjection] Add Lazy attribute for classes and arguments
2 parents 1ebf00c + 7323dc1 commit bdcaee0

11 files changed

+178
-2
lines changed

Attribute/Lazy.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\DependencyInjection\Attribute;
13+
14+
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_PARAMETER)]
15+
class Lazy
16+
{
17+
public function __construct(
18+
public bool|string|null $lazy = true,
19+
) {
20+
}
21+
}

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ CHANGELOG
66

77
* Add argument `$prepend` to `ContainerConfigurator::extension()` to prepend the configuration instead of appending it
88
* Have `ServiceLocator` implement `ServiceCollectionInterface`
9+
* Add `#[Lazy]` attribute as shortcut for `#[Autowire(lazy: [bool|string])]` and `#[Autoconfigure(lazy: [bool|string])]`
910

1011
7.0
1112
---

Compiler/AutowirePass.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use Symfony\Component\DependencyInjection\Attribute\Autowire;
1616
use Symfony\Component\DependencyInjection\Attribute\AutowireCallable;
1717
use Symfony\Component\DependencyInjection\Attribute\AutowireDecorated;
18+
use Symfony\Component\DependencyInjection\Attribute\Lazy;
1819
use Symfony\Component\DependencyInjection\Attribute\Target;
1920
use Symfony\Component\DependencyInjection\ContainerBuilder;
2021
use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -299,7 +300,13 @@ private function autowireMethod(\ReflectionFunctionAbstract $reflectionMethod, a
299300
};
300301

301302
if ($checkAttributes) {
302-
foreach ($parameter->getAttributes(Autowire::class, \ReflectionAttribute::IS_INSTANCEOF) as $attribute) {
303+
$attributes = array_merge($parameter->getAttributes(Autowire::class, \ReflectionAttribute::IS_INSTANCEOF), $parameter->getAttributes(Lazy::class, \ReflectionAttribute::IS_INSTANCEOF));
304+
305+
if (1 < \count($attributes)) {
306+
throw new AutowiringFailedException($this->currentId, 'Using both attributes #[Lazy] and #[Autowire] on an argument is not allowed; use the "lazy" parameter of #[Autowire] instead.');
307+
}
308+
309+
foreach ($attributes as $attribute) {
303310
$attribute = $attribute->newInstance();
304311
$invalidBehavior = $parameter->allowsNull() ? ContainerInterface::NULL_ON_INVALID_REFERENCE : ContainerBuilder::EXCEPTION_ON_INVALID_REFERENCE;
305312

Compiler/RegisterAutoconfigureAttributesPass.php

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@
1212
namespace Symfony\Component\DependencyInjection\Compiler;
1313

1414
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
15+
use Symfony\Component\DependencyInjection\Attribute\Lazy;
1516
use Symfony\Component\DependencyInjection\ContainerBuilder;
1617
use Symfony\Component\DependencyInjection\Definition;
18+
use Symfony\Component\DependencyInjection\Exception\AutoconfigureFailedException;
1719
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
1820

1921
/**
@@ -42,7 +44,16 @@ public function accept(Definition $definition): bool
4244

4345
public function processClass(ContainerBuilder $container, \ReflectionClass $class): void
4446
{
45-
foreach ($class->getAttributes(Autoconfigure::class, \ReflectionAttribute::IS_INSTANCEOF) as $attribute) {
47+
$autoconfigure = $class->getAttributes(Autoconfigure::class, \ReflectionAttribute::IS_INSTANCEOF);
48+
$lazy = $class->getAttributes(Lazy::class, \ReflectionAttribute::IS_INSTANCEOF);
49+
50+
if ($autoconfigure && $lazy) {
51+
throw new AutoconfigureFailedException($class->name, 'Using both attributes #[Lazy] and #[Autoconfigure] on an argument is not allowed; use the "lazy" parameter of #[Autoconfigure] instead.');
52+
}
53+
54+
$attributes = array_merge($autoconfigure, $lazy);
55+
56+
foreach ($attributes as $attribute) {
4657
self::registerForAutoconfiguration($container, $class, $attribute);
4758
}
4859
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\DependencyInjection\Exception;
13+
14+
class AutoconfigureFailedException extends AutowiringFailedException
15+
{
16+
}

Tests/Compiler/AutowirePassTest.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1368,4 +1368,29 @@ public function testNestedAttributes()
13681368
];
13691369
$this->assertEquals($expected, $container->getDefinition(AutowireNestedAttributes::class)->getArgument(0));
13701370
}
1371+
1372+
public function testLazyServiceAttribute()
1373+
{
1374+
$container = new ContainerBuilder();
1375+
$container->register('a', A::class)->setAutowired(true);
1376+
$container->register('foo', LazyServiceAttributeAutowiring::class)->setAutowired(true);
1377+
1378+
(new AutowirePass())->process($container);
1379+
1380+
$expected = new Reference('.lazy.'.A::class);
1381+
$this->assertEquals($expected, $container->getDefinition('foo')->getArgument(0));
1382+
}
1383+
1384+
public function testLazyNotCompatibleWithAutowire()
1385+
{
1386+
$container = new ContainerBuilder();
1387+
$container->register('a', A::class)->setAutowired(true);
1388+
$container->register('foo', LazyAutowireServiceAttributesAutowiring::class)->setAutowired(true);
1389+
1390+
try {
1391+
(new AutowirePass())->process($container);
1392+
} catch (AutowiringFailedException $e) {
1393+
$this->assertSame('Using both attributes #[Lazy] and #[Autowire] on an argument is not allowed; use the "lazy" parameter of #[Autowire] instead.', $e->getMessage());
1394+
}
1395+
}
13711396
}

Tests/Compiler/RegisterAutoconfigureAttributesPassTest.php

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,13 @@
1616
use Symfony\Component\DependencyInjection\ChildDefinition;
1717
use Symfony\Component\DependencyInjection\Compiler\RegisterAutoconfigureAttributesPass;
1818
use Symfony\Component\DependencyInjection\ContainerBuilder;
19+
use Symfony\Component\DependencyInjection\Exception\AutoconfigureFailedException;
1920
use Symfony\Component\DependencyInjection\Reference;
2021
use Symfony\Component\DependencyInjection\Tests\Fixtures\AutoconfigureAttributed;
2122
use Symfony\Component\DependencyInjection\Tests\Fixtures\AutoconfiguredInterface;
23+
use Symfony\Component\DependencyInjection\Tests\Fixtures\LazyAutoconfigured;
24+
use Symfony\Component\DependencyInjection\Tests\Fixtures\LazyLoaded;
25+
use Symfony\Component\DependencyInjection\Tests\Fixtures\MultipleAutoconfigureAttributed;
2226
use Symfony\Component\DependencyInjection\Tests\Fixtures\ParentNotExists;
2327
use Symfony\Component\DependencyInjection\Tests\Fixtures\StaticConstructorAutoconfigure;
2428

@@ -104,4 +108,46 @@ public function testStaticConstructor()
104108
;
105109
$this->assertEquals([StaticConstructorAutoconfigure::class => $expected], $container->getAutoconfiguredInstanceof());
106110
}
111+
112+
public function testLazyServiceAttribute()
113+
{
114+
$container = new ContainerBuilder();
115+
$container->register('foo', LazyLoaded::class)
116+
->setAutoconfigured(true);
117+
118+
(new RegisterAutoconfigureAttributesPass())->process($container);
119+
120+
$expected = (new ChildDefinition(''))
121+
->setLazy(true)
122+
;
123+
$this->assertEquals([LazyLoaded::class => $expected], $container->getAutoconfiguredInstanceof());
124+
}
125+
126+
public function testLazyNotCompatibleWithAutoconfigureAttribute()
127+
{
128+
$container = new ContainerBuilder();
129+
$container->register('foo', LazyAutoconfigured::class)
130+
->setAutoconfigured(true);
131+
132+
try {
133+
(new RegisterAutoconfigureAttributesPass())->process($container);
134+
} catch (AutoconfigureFailedException $e) {
135+
$this->assertSame('Using both attributes #[Lazy] and #[Autoconfigure] on an argument is not allowed; use the "lazy" parameter of #[Autoconfigure] instead.', $e->getMessage());
136+
}
137+
}
138+
139+
public function testMultipleAutoconfigureAllowed()
140+
{
141+
$container = new ContainerBuilder();
142+
$container->register('foo', MultipleAutoconfigureAttributed::class)
143+
->setAutoconfigured(true);
144+
145+
(new RegisterAutoconfigureAttributesPass())->process($container);
146+
147+
$expected = (new ChildDefinition(''))
148+
->addTag('foo')
149+
->addTag('bar')
150+
;
151+
$this->assertEquals([MultipleAutoconfigureAttributed::class => $expected], $container->getAutoconfiguredInstanceof());
152+
}
107153
}

Tests/Fixtures/LazyAutoconfigured.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
namespace Symfony\Component\DependencyInjection\Tests\Fixtures;
4+
5+
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
6+
use Symfony\Component\DependencyInjection\Attribute\Lazy;
7+
8+
#[Lazy, Autoconfigure(lazy: true)]
9+
class LazyAutoconfigured
10+
{
11+
}

Tests/Fixtures/LazyLoaded.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
namespace Symfony\Component\DependencyInjection\Tests\Fixtures;
4+
5+
use Symfony\Component\DependencyInjection\Attribute\Lazy;
6+
7+
#[Lazy]
8+
class LazyLoaded
9+
{
10+
11+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
namespace Symfony\Component\DependencyInjection\Tests\Fixtures;
4+
5+
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
6+
7+
#[Autoconfigure(tags: ['foo'])]
8+
#[Autoconfigure(tags: ['bar'])]
9+
class MultipleAutoconfigureAttributed
10+
{
11+
12+
}

0 commit comments

Comments
 (0)