Skip to content

Commit 99081f9

Browse files
feature #49836 [DependencyInjection] Add support for #[Autowire(lazy: class-string)] (nicolas-grekas)
This PR was merged into the 6.3 branch. Discussion ---------- [DependencyInjection] Add support for `#[Autowire(lazy: class-string)]` | Q | A | ------------- | --- | Branch? | 6.3 | Bug fix? | no | New feature? | yes | Deprecations? | no | Tickets | - | License | MIT | Doc PR | - This PR finishes what started in #49685. It adds support for defining the interfaces to proxy when using laziness with the Autowire attribute. ```php public function __construct( #[Autowire(service: 'foo', lazy: FooInterface::class)] FooInterface|BarInterface $foo, ) { } ``` It also adds support for lazy-autowiring of intersection types: ```php public function __construct( #[Autowire(service: 'foobar', lazy: true)] FooInterface&BarInterface $foobar, ) { } ``` Commits ------- d127ebf [DependencyInjection] Add support for `#[Autowire(lazy: class-string)]`
2 parents 827dbbf + d127ebf commit 99081f9

14 files changed

+199
-34
lines changed

src/Symfony/Component/DependencyInjection/Attribute/Autowire.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
class Autowire
2626
{
2727
public readonly string|array|Expression|Reference|ArgumentInterface|null $value;
28+
public readonly bool|array $lazy;
2829

2930
/**
3031
* Use only ONE of the following.
@@ -34,16 +35,17 @@ class Autowire
3435
* @param string|null $expression Expression (ie 'service("some.service").someMethod()')
3536
* @param string|null $env Environment variable name (ie 'SOME_ENV_VARIABLE')
3637
* @param string|null $param Parameter name (ie 'some.parameter.name')
38+
* @param bool|class-string|class-string[] $lazy Whether to use lazy-loading for this argument
3739
*/
3840
public function __construct(
3941
string|array|ArgumentInterface $value = null,
4042
string $service = null,
4143
string $expression = null,
4244
string $env = null,
4345
string $param = null,
44-
public bool $lazy = false,
46+
bool|string|array $lazy = false,
4547
) {
46-
if ($lazy) {
48+
if ($this->lazy = \is_string($lazy) ? [$lazy] : $lazy) {
4749
if (null !== ($expression ?? $env ?? $param)) {
4850
throw new LogicException('#[Autowire] attribute cannot be $lazy and use $expression, $env, or $param.');
4951
}

src/Symfony/Component/DependencyInjection/CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ CHANGELOG
1717
* Add `#[Exclude]` to skip autoregistering a class
1818
* Add support for generating lazy closures
1919
* Add support for autowiring services as closures using `#[AutowireCallable]` or `#[AutowireServiceClosure]`
20-
* Add support for `#[Autowire(lazy: true)]`
20+
* Add support for `#[Autowire(lazy: true|class-string)]`
2121
* Deprecate `#[MapDecorated]`, use `#[AutowireDecorated]` instead
2222
* Deprecate the `@required` annotation, use the `Symfony\Contracts\Service\Attribute\Required` attribute instead
2323

src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -295,12 +295,33 @@ private function autowireMethod(\ReflectionFunctionAbstract $reflectionMethod, a
295295
->setFactory(['Closure', 'fromCallable'])
296296
->setArguments([$value + [1 => '__invoke']])
297297
->setLazy($attribute->lazy);
298-
} elseif ($attribute->lazy && ($value instanceof Reference ? !$this->container->has($value) || !$this->container->findDefinition($value)->isLazy() : null === $attribute->value && $type)) {
299-
$this->container->register('.lazy.'.$value ??= $getValue(), $type)
298+
} elseif ($lazy = $attribute->lazy) {
299+
$definition = (new Definition($type))
300300
->setFactory('current')
301-
->setArguments([[$value]])
301+
->setArguments([[$value ??= $getValue()]])
302302
->setLazy(true);
303-
$value = new Reference('.lazy.'.$value);
303+
304+
if (!\is_array($lazy)) {
305+
if (str_contains($type, '|')) {
306+
throw new AutowiringFailedException($this->currentId, sprintf('Cannot use #[Autowire] with option "lazy: true" on union types for service "%s"; set the option to the interface(s) that should be proxied instead.', $this->currentId));
307+
}
308+
$lazy = str_contains($type, '&') ? explode('&', $type) : [];
309+
}
310+
311+
if ($lazy) {
312+
if (!class_exists($type) && !interface_exists($type, false)) {
313+
$definition->setClass('object');
314+
}
315+
foreach ($lazy as $v) {
316+
$definition->addTag('proxy', ['interface' => $v]);
317+
}
318+
}
319+
320+
if ($definition->getClass() !== (string) $value || $definition->getTag('proxy')) {
321+
$value .= '.'.$this->container->hash([$definition->getClass(), $definition->getTag('proxy')]);
322+
}
323+
$this->container->setDefinition($value = '.lazy.'.$value, $definition);
324+
$value = new Reference($value);
304325
}
305326
$arguments[$index] = $value;
306327

src/Symfony/Component/DependencyInjection/LazyProxy/Instantiator/LazyServiceInstantiator.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public function instantiateProxy(ContainerInterface $container, Definition $defi
2929
throw new InvalidArgumentException(sprintf('Cannot instantiate lazy proxy for service "%s".', $id));
3030
}
3131

32-
if (!class_exists($proxyClass = $dumper->getProxyClass($definition, $asGhostObject, $class), false)) {
32+
if (!class_exists($proxyClass = $dumper->getProxyClass($definition, $asGhostObject), false)) {
3333
eval($dumper->getProxyCode($definition, $id));
3434
}
3535

src/Symfony/Component/DependencyInjection/LazyProxy/PhpDumper/LazyServiceDumper.php

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ public function getProxyCode(Definition $definition, string $id = null): string
120120
if (!interface_exists($tag['interface']) && !class_exists($tag['interface'], false)) {
121121
throw new InvalidArgumentException(sprintf('Invalid definition for service "%s": several "proxy" tags found but "%s" is not an interface.', $id ?? $definition->getClass(), $tag['interface']));
122122
}
123-
if (!is_a($class->name, $tag['interface'], true)) {
123+
if ('object' !== $definition->getClass() && !is_a($class->name, $tag['interface'], true)) {
124124
throw new InvalidArgumentException(sprintf('Invalid "proxy" tag for service "%s": class "%s" doesn\'t implement "%s".', $id ?? $definition->getClass(), $definition->getClass(), $tag['interface']));
125125
}
126126
$interfaces[] = new \ReflectionClass($tag['interface']);
@@ -141,10 +141,11 @@ public function getProxyCode(Definition $definition, string $id = null): string
141141

142142
public function getProxyClass(Definition $definition, bool $asGhostObject, \ReflectionClass &$class = null): string
143143
{
144-
$class = new \ReflectionClass($definition->getClass());
144+
$class = 'object' !== $definition->getClass() ? $definition->getClass() : 'stdClass';
145+
$class = new \ReflectionClass($class);
145146

146-
return preg_replace('/^.*\\\\/', '', $class->name)
147+
return preg_replace('/^.*\\\\/', '', $definition->getClass())
147148
.($asGhostObject ? 'Ghost' : 'Proxy')
148-
.ucfirst(substr(hash('sha256', $this->salt.'+'.$class->name), -7));
149+
.ucfirst(substr(hash('sha256', $this->salt.'+'.$class->name.'+'.serialize($definition->getTag('proxy'))), -7));
149150
}
150151
}

src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,11 @@
4343
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
4444
use Symfony\Component\DependencyInjection\Reference;
4545
use Symfony\Component\DependencyInjection\ServiceLocator;
46+
use Symfony\Component\DependencyInjection\Tests\Compiler\AAndIInterfaceConsumer;
47+
use Symfony\Component\DependencyInjection\Tests\Compiler\AInterface;
4648
use Symfony\Component\DependencyInjection\Tests\Compiler\Foo;
4749
use Symfony\Component\DependencyInjection\Tests\Compiler\FooAnnotation;
50+
use Symfony\Component\DependencyInjection\Tests\Compiler\IInterface;
4851
use Symfony\Component\DependencyInjection\Tests\Compiler\Wither;
4952
use Symfony\Component\DependencyInjection\Tests\Compiler\WitherAnnotation;
5053
use Symfony\Component\DependencyInjection\Tests\Fixtures\CustomDefinition;
@@ -1769,6 +1772,29 @@ public function testLazyAutowireAttribute()
17691772
$this->assertInstanceOf(LazyObjectInterface::class, $container->get('bar')->foo);
17701773
$this->assertSame($container->get('foo'), $container->get('bar')->foo->initializeLazyObject());
17711774
}
1775+
1776+
public function testLazyAutowireAttributeWithIntersection()
1777+
{
1778+
$container = new ContainerBuilder();
1779+
$container->register('foo', AAndIInterfaceConsumer::class)
1780+
->setPublic('true')
1781+
->setAutowired(true);
1782+
1783+
$container->compile();
1784+
1785+
$lazyId = \array_slice(array_keys($container->getDefinitions()), -1)[0];
1786+
$this->assertStringStartsWith('.lazy.foo.', $lazyId);
1787+
$definition = $container->getDefinition($lazyId);
1788+
$this->assertSame('object', $definition->getClass());
1789+
$this->assertSame([
1790+
['interface' => AInterface::class],
1791+
['interface' => IInterface::class],
1792+
], $definition->getTag('proxy'));
1793+
1794+
$dumper = new PhpDumper($container);
1795+
1796+
$this->assertStringEqualsFile(self::$fixturesPath.'/php/lazy_autowire_attribute_with_intersection.php', $dumper->dump());
1797+
}
17721798
}
17731799

17741800
class Rot13EnvVarProcessor implements EnvVarProcessorInterface

src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/autowiring_classes.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Symfony\Component\DependencyInjection\Tests\Compiler;
44

55
use Psr\Log\LoggerInterface;
6+
use Symfony\Component\DependencyInjection\Attribute\Autowire;
67
use Symfony\Contracts\Service\Attribute\Required;
78

89
require __DIR__.'/uniontype_classes.php';
@@ -526,3 +527,12 @@ public function __construct(NotExisting $notExisting)
526527
{
527528
}
528529
}
530+
531+
class AAndIInterfaceConsumer
532+
{
533+
public function __construct(
534+
#[Autowire(service: 'foo', lazy: true)]
535+
AInterface&IInterface $logger,
536+
) {
537+
}
538+
}

src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/lazy_autowire_attribute.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,14 +82,14 @@ protected static function getFoo2Service($container, $lazyLoad = true)
8282
$containerRef = $container->ref;
8383

8484
if (true === $lazyLoad) {
85-
return $container->privates['.lazy.Symfony\\Component\\DependencyInjection\\Tests\\Compiler\\Foo'] = $container->createProxy('FooProxy9f41ec7', static fn () => \FooProxy9f41ec7::createLazyProxy(static fn () => self::getFoo2Service($containerRef->get(), false)));
85+
return $container->privates['.lazy.Symfony\\Component\\DependencyInjection\\Tests\\Compiler\\Foo'] = $container->createProxy('FooProxy4048957', static fn () => \FooProxy4048957::createLazyProxy(static fn () => self::getFoo2Service($containerRef->get(), false)));
8686
}
8787

8888
return ($container->services['foo'] ??= new \Symfony\Component\DependencyInjection\Tests\Compiler\Foo());
8989
}
9090
}
9191

92-
class FooProxy9f41ec7 extends \Symfony\Component\DependencyInjection\Tests\Compiler\Foo implements \Symfony\Component\VarExporter\LazyObjectInterface
92+
class FooProxy4048957 extends \Symfony\Component\DependencyInjection\Tests\Compiler\Foo implements \Symfony\Component\VarExporter\LazyObjectInterface
9393
{
9494
use \Symfony\Component\VarExporter\LazyProxyTrait;
9595

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
<?php
2+
3+
use Symfony\Component\DependencyInjection\Argument\RewindableGenerator;
4+
use Symfony\Component\DependencyInjection\ContainerInterface;
5+
use Symfony\Component\DependencyInjection\Container;
6+
use Symfony\Component\DependencyInjection\Exception\LogicException;
7+
use Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException;
8+
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
9+
use Symfony\Component\DependencyInjection\ParameterBag\FrozenParameterBag;
10+
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
11+
12+
/**
13+
* @internal This class has been auto-generated by the Symfony Dependency Injection Component.
14+
*/
15+
class ProjectServiceContainer extends Container
16+
{
17+
protected $parameters = [];
18+
protected readonly \WeakReference $ref;
19+
20+
public function __construct()
21+
{
22+
$this->ref = \WeakReference::create($this);
23+
$this->services = $this->privates = [];
24+
$this->methodMap = [
25+
'foo' => 'getFooService',
26+
];
27+
28+
$this->aliases = [];
29+
}
30+
31+
public function compile(): void
32+
{
33+
throw new LogicException('You cannot compile a dumped container that was already compiled.');
34+
}
35+
36+
public function isCompiled(): bool
37+
{
38+
return true;
39+
}
40+
41+
public function getRemovedIds(): array
42+
{
43+
return [
44+
'.lazy.foo.gDmfket' => true,
45+
];
46+
}
47+
48+
protected function createProxy($class, \Closure $factory)
49+
{
50+
return $factory();
51+
}
52+
53+
/**
54+
* Gets the public 'foo' shared autowired service.
55+
*
56+
* @return \Symfony\Component\DependencyInjection\Tests\Compiler\AAndIInterfaceConsumer
57+
*/
58+
protected static function getFooService($container)
59+
{
60+
$a = ($container->privates['.lazy.foo.gDmfket'] ?? self::get_Lazy_Foo_GDmfketService($container));
61+
62+
if (isset($container->services['foo'])) {
63+
return $container->services['foo'];
64+
}
65+
66+
return $container->services['foo'] = new \Symfony\Component\DependencyInjection\Tests\Compiler\AAndIInterfaceConsumer($a);
67+
}
68+
69+
/**
70+
* Gets the private '.lazy.foo.gDmfket' shared service.
71+
*
72+
* @return \object
73+
*/
74+
protected static function get_Lazy_Foo_GDmfketService($container, $lazyLoad = true)
75+
{
76+
$containerRef = $container->ref;
77+
78+
if (true === $lazyLoad) {
79+
return $container->privates['.lazy.foo.gDmfket'] = $container->createProxy('objectProxy8ac8e9a', static fn () => \objectProxy8ac8e9a::createLazyProxy(static fn () => self::get_Lazy_Foo_GDmfketService($containerRef->get(), false)));
80+
}
81+
82+
return ($container->services['foo'] ?? self::getFooService($container));
83+
}
84+
}
85+
86+
class objectProxy8ac8e9a implements \Symfony\Component\DependencyInjection\Tests\Compiler\AInterface, \Symfony\Component\DependencyInjection\Tests\Compiler\IInterface, \Symfony\Component\VarExporter\LazyObjectInterface
87+
{
88+
use \Symfony\Component\VarExporter\LazyProxyTrait;
89+
90+
private const LAZY_OBJECT_PROPERTY_SCOPES = [];
91+
92+
public function initializeLazyObject(): \Symfony\Component\DependencyInjection\Tests\Compiler\AInterface&\Symfony\Component\DependencyInjection\Tests\Compiler\IInterface
93+
{
94+
if ($state = $this->lazyObjectState ?? null) {
95+
return $state->realInstance ??= ($state->initializer)();
96+
}
97+
98+
return $this;
99+
}
100+
}
101+
102+
// Help opcache.preload discover always-needed symbols
103+
class_exists(\Symfony\Component\VarExporter\Internal\Hydrator::class);
104+
class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectRegistry::class);
105+
class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectState::class);

src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9_lazy_inlined_factories.txt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ namespace Container%s;
66

77
include_once $container->targetDir.''.'/Fixtures/includes/foo.php';
88

9-
class FooClassGhost2b16075 extends \Bar\FooClass implements \Symfony\Component\VarExporter\LazyObjectInterface
9+
class FooClassGhostEe53b95 extends \Bar\FooClass implements \Symfony\Component\VarExporter\LazyObjectInterface
1010
%A
1111

12-
if (!\class_exists('FooClassGhost2b16075', false)) {
13-
\class_alias(__NAMESPACE__.'\\FooClassGhost2b16075', 'FooClassGhost2b16075', false);
12+
if (!\class_exists('FooClassGhostEe53b95', false)) {
13+
\class_alias(__NAMESPACE__.'\\FooClassGhostEe53b95', 'FooClassGhostEe53b95', false);
1414
}
1515

1616
[Container%s/ProjectServiceContainer.php] => <?php
@@ -78,7 +78,7 @@ class ProjectServiceContainer extends Container
7878
$containerRef = $container->ref;
7979

8080
if (true === $lazyLoad) {
81-
return $container->services['lazy_foo'] = $container->createProxy('FooClassGhost2b16075', static fn () => \FooClassGhost2b16075::createLazyGhost(static fn ($proxy) => self::getLazyFooService($containerRef->get(), $proxy)));
81+
return $container->services['lazy_foo'] = $container->createProxy('FooClassGhostEe53b95', static fn () => \FooClassGhostEe53b95::createLazyGhost(static fn ($proxy) => self::getLazyFooService($containerRef->get(), $proxy)));
8282
}
8383

8484
include_once $container->targetDir.''.'/Fixtures/includes/foo_lazy.php';

src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_dedup_lazy.php

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ protected static function getBarService($container, $lazyLoad = true)
5656
$containerRef = $container->ref;
5757

5858
if (true === $lazyLoad) {
59-
return $container->services['bar'] = $container->createProxy('stdClassGhost5a8a5eb', static fn () => \stdClassGhost5a8a5eb::createLazyGhost(static fn ($proxy) => self::getBarService($containerRef->get(), $proxy)));
59+
return $container->services['bar'] = $container->createProxy('stdClassGhost2fc7938', static fn () => \stdClassGhost2fc7938::createLazyGhost(static fn ($proxy) => self::getBarService($containerRef->get(), $proxy)));
6060
}
6161

6262
return $lazyLoad;
@@ -72,7 +72,7 @@ protected static function getBazService($container, $lazyLoad = true)
7272
$containerRef = $container->ref;
7373

7474
if (true === $lazyLoad) {
75-
return $container->services['baz'] = $container->createProxy('stdClassProxy5a8a5eb', static fn () => \stdClassProxy5a8a5eb::createLazyProxy(static fn () => self::getBazService($containerRef->get(), false)));
75+
return $container->services['baz'] = $container->createProxy('stdClassProxy2fc7938', static fn () => \stdClassProxy2fc7938::createLazyProxy(static fn () => self::getBazService($containerRef->get(), false)));
7676
}
7777

7878
return \foo_bar();
@@ -88,7 +88,7 @@ protected static function getBuzService($container, $lazyLoad = true)
8888
$containerRef = $container->ref;
8989

9090
if (true === $lazyLoad) {
91-
return $container->services['buz'] = $container->createProxy('stdClassProxy5a8a5eb', static fn () => \stdClassProxy5a8a5eb::createLazyProxy(static fn () => self::getBuzService($containerRef->get(), false)));
91+
return $container->services['buz'] = $container->createProxy('stdClassProxy2fc7938', static fn () => \stdClassProxy2fc7938::createLazyProxy(static fn () => self::getBuzService($containerRef->get(), false)));
9292
}
9393

9494
return \foo_bar();
@@ -104,14 +104,14 @@ protected static function getFooService($container, $lazyLoad = true)
104104
$containerRef = $container->ref;
105105

106106
if (true === $lazyLoad) {
107-
return $container->services['foo'] = $container->createProxy('stdClassGhost5a8a5eb', static fn () => \stdClassGhost5a8a5eb::createLazyGhost(static fn ($proxy) => self::getFooService($containerRef->get(), $proxy)));
107+
return $container->services['foo'] = $container->createProxy('stdClassGhost2fc7938', static fn () => \stdClassGhost2fc7938::createLazyGhost(static fn ($proxy) => self::getFooService($containerRef->get(), $proxy)));
108108
}
109109

110110
return $lazyLoad;
111111
}
112112
}
113113

114-
class stdClassGhost5a8a5eb extends \stdClass implements \Symfony\Component\VarExporter\LazyObjectInterface
114+
class stdClassGhost2fc7938 extends \stdClass implements \Symfony\Component\VarExporter\LazyObjectInterface
115115
{
116116
use \Symfony\Component\VarExporter\LazyGhostTrait;
117117

@@ -123,7 +123,7 @@ class_exists(\Symfony\Component\VarExporter\Internal\Hydrator::class);
123123
class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectRegistry::class);
124124
class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectState::class);
125125

126-
class stdClassProxy5a8a5eb extends \stdClass implements \Symfony\Component\VarExporter\LazyObjectInterface
126+
class stdClassProxy2fc7938 extends \stdClass implements \Symfony\Component\VarExporter\LazyObjectInterface
127127
{
128128
use \Symfony\Component\VarExporter\LazyProxyTrait;
129129

0 commit comments

Comments
 (0)