Skip to content

Commit 9d54a4a

Browse files
[DependencyInjection] Add support for generating lazy closures
1 parent 2f1ccef commit 9d54a4a

File tree

7 files changed

+151
-3
lines changed

7 files changed

+151
-3
lines changed

src/Symfony/Component/DependencyInjection/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ CHANGELOG
1515
* Allow to trim XML service parameters value by using `trim="true"` attribute
1616
* Allow extending the `Autowire` attribute
1717
* Add `#[Exclude]` to skip autoregistering a class
18+
* Add support for generating lazy closures
1819
* Add support for autowiring services as closures using `#[AutowireCallable]` or `#[AutowireServiceClosure]`
1920

2021
6.2

src/Symfony/Component/DependencyInjection/ContainerBuilder.php

+26-3
Original file line numberDiff line numberDiff line change
@@ -1048,12 +1048,35 @@ private function createService(Definition $definition, array &$inlineServices, b
10481048
}
10491049

10501050
$parameterBag = $this->getParameterBag();
1051+
$class = ($parameterBag->resolveValue($definition->getClass()) ?: (['Closure', 'fromCallable'] === $definition->getFactory() ? 'Closure' : null));
10511052

1052-
if (true === $tryProxy && $definition->isLazy() && !$tryProxy = !($proxy = $this->proxyInstantiator ??= new LazyServiceInstantiator()) || $proxy instanceof RealServiceInstantiator) {
1053+
if ('Closure' === $class && $definition->isLazy() && ['Closure', 'fromCallable'] === $definition->getFactory()) {
1054+
$callable = $parameterBag->unescapeValue($parameterBag->resolveValue($definition->getArgument(0)));
1055+
1056+
if ($callable instanceof Reference || $callable instanceof Definition) {
1057+
$callable = [$callable, '__invoke'];
1058+
}
1059+
1060+
if (\is_array($callable) && (
1061+
$callable[0] instanceof Reference
1062+
|| $callable[0] instanceof Definition && !isset($inlineServices[spl_object_hash($callable[0])])
1063+
)) {
1064+
$proxy = function (...$arguments) use ($callable, &$inlineServices) {
1065+
return $this->doResolveServices($callable, $inlineServices)(...$arguments);
1066+
};
1067+
$this->shareService($definition, $proxy, $id, $inlineServices);
1068+
1069+
return $proxy;
1070+
}
1071+
}
1072+
1073+
if (true === $tryProxy && $definition->isLazy() && 'Closure' !== $class
1074+
&& !$tryProxy = !($proxy = $this->proxyInstantiator ??= new LazyServiceInstantiator()) || $proxy instanceof RealServiceInstantiator
1075+
) {
10531076
$proxy = $proxy->instantiateProxy(
10541077
$this,
10551078
(clone $definition)
1056-
->setClass($parameterBag->resolveValue($definition->getClass()))
1079+
->setClass($class)
10571080
->setTags(($definition->hasTag('proxy') ? ['proxy' => $parameterBag->resolveValue($definition->getTag('proxy'))] : []) + $definition->getTags()),
10581081
$id, function ($proxy = false) use ($definition, &$inlineServices, $id) {
10591082
return $this->createService($definition, $inlineServices, true, $id, $proxy);
@@ -1102,7 +1125,7 @@ private function createService(Definition $definition, array &$inlineServices, b
11021125
}
11031126
}
11041127
} else {
1105-
$r = new \ReflectionClass($parameterBag->resolveValue($definition->getClass()));
1128+
$r = new \ReflectionClass($class);
11061129

11071130
if (\is_object($tryProxy)) {
11081131
if ($r->getConstructor()) {

src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php

+13
Original file line numberDiff line numberDiff line change
@@ -1179,6 +1179,15 @@ private function addNewInstance(Definition $definition, string $return = '', str
11791179
throw new RuntimeException(sprintf('Cannot dump definition because of invalid factory method (%s).', $callable[1] ?: 'n/a'));
11801180
}
11811181

1182+
if (['...'] === $arguments && $definition->isLazy() && 'Closure' === ($definition->getClass() ?? 'Closure') && (
1183+
$callable[0] instanceof Reference
1184+
|| ($callable[0] instanceof Definition && !$this->definitionVariables->contains($callable[0]))
1185+
)) {
1186+
$this->addContainerRef = true;
1187+
1188+
return $return.sprintf('function (...$arguments) use ($containerRef) { $container = $containerRef->get(); return (%s)->%s(...$arguments); }', $this->dumpValue($callable[0]), $callable[1]).$tail;
1189+
}
1190+
11821191
if ($callable[0] instanceof Reference
11831192
|| ($callable[0] instanceof Definition && $this->definitionVariables->contains($callable[0]))
11841193
) {
@@ -2327,6 +2336,10 @@ private function isProxyCandidate(Definition $definition, ?bool &$asGhostObject,
23272336
{
23282337
$asGhostObject = false;
23292338

2339+
if ('Closure' === ($definition->getClass() ?: (['Closure', 'fromCallable'] === $definition->getFactory() ? 'Closure' : null))) {
2340+
return null;
2341+
}
2342+
23302343
if (!$definition->isLazy() || !$this->hasProxyDumper) {
23312344
return null;
23322345
}

src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php

+18
Original file line numberDiff line numberDiff line change
@@ -1961,6 +1961,24 @@ public function testNamedArgumentBeforeCompile()
19611961

19621962
$this->assertSame(1, $e->first);
19631963
}
1964+
1965+
public function testLazyClosure()
1966+
{
1967+
$container = new ContainerBuilder();
1968+
$container->register('closure', 'Closure')
1969+
->setPublic('true')
1970+
->setFactory(['Closure', 'fromCallable'])
1971+
->setLazy(true)
1972+
->setArguments([[new Reference('foo'), 'cloneFoo']]);
1973+
$container->register('foo', Foo::class);
1974+
$container->compile();
1975+
1976+
$cloned = Foo::$cloned;
1977+
$this->assertInstanceOf(\Closure::class, $container->get('closure'));
1978+
$this->assertSame($cloned, Foo::$cloned);
1979+
$this->assertInstanceOf(Foo::class, $container->get('closure')());
1980+
$this->assertSame(1 + $cloned, Foo::$cloned);
1981+
}
19641982
}
19651983

19661984
class FooClass

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

+25
Original file line numberDiff line numberDiff line change
@@ -1686,6 +1686,31 @@ public function testAutowireClosure()
16861686
$this->assertInstanceOf(Foo::class, $fooClone = ($bar->buz)());
16871687
$this->assertNotSame($container->get('foo'), $fooClone);
16881688
}
1689+
1690+
public function testLazyClosure()
1691+
{
1692+
$container = new ContainerBuilder();
1693+
$container->register('closure', 'Closure')
1694+
->setPublic('true')
1695+
->setFactory(['Closure', 'fromCallable'])
1696+
->setLazy(true)
1697+
->setArguments([[new Reference('foo'), 'cloneFoo']]);
1698+
$container->register('foo', Foo::class);
1699+
$container->compile();
1700+
$dumper = new PhpDumper($container);
1701+
1702+
$this->assertStringEqualsFile(self::$fixturesPath.'/php/lazy_closure.php', $dumper->dump(['class' => 'Symfony_DI_PhpDumper_Test_Lazy_Closure']));
1703+
1704+
require self::$fixturesPath.'/php/lazy_closure.php';
1705+
1706+
$container = new \Symfony_DI_PhpDumper_Test_Lazy_Closure();
1707+
1708+
$cloned = Foo::$cloned;
1709+
$this->assertInstanceOf(\Closure::class, $container->get('closure'));
1710+
$this->assertSame($cloned, Foo::$cloned);
1711+
$this->assertInstanceOf(Foo::class, $container->get('closure')());
1712+
$this->assertSame(1 + $cloned, Foo::$cloned);
1713+
}
16891714
}
16901715

16911716
class Rot13EnvVarProcessor implements EnvVarProcessorInterface

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

+4
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,15 @@
1313

1414
class Foo
1515
{
16+
public static int $cloned = 0;
17+
1618
/**
1719
* @required
1820
*/
1921
public function cloneFoo(): static
2022
{
23+
++self::$cloned;
24+
2125
return clone $this;
2226
}
2327
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
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 Symfony_DI_PhpDumper_Test_Lazy_Closure 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+
'closure' => 'getClosureService',
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+
'foo' => true,
45+
];
46+
}
47+
48+
protected function createProxy($class, \Closure $factory)
49+
{
50+
return $factory();
51+
}
52+
53+
/**
54+
* Gets the public 'closure' shared service.
55+
*
56+
* @return \Closure
57+
*/
58+
protected static function getClosureService($container, $lazyLoad = true)
59+
{
60+
$containerRef = $container->ref;
61+
62+
return $container->services['closure'] = function (...$arguments) use ($containerRef) { $container = $containerRef->get(); return (new \Symfony\Component\DependencyInjection\Tests\Compiler\Foo())->cloneFoo(...$arguments); };
63+
}
64+
}

0 commit comments

Comments
 (0)