Skip to content

Commit 2d74a76

Browse files
[DependencyInjection] Add support for generating lazy closures
1 parent cb39a7f commit 2d74a76

File tree

8 files changed

+225
-4
lines changed

8 files changed

+225
-4
lines changed
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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\Argument;
13+
14+
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
15+
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
16+
use Symfony\Component\VarExporter\ProxyHelper;
17+
18+
/**
19+
* @author Nicolas Grekas <p@tchwork.com>
20+
*
21+
* @internal
22+
*/
23+
class LazyClosure
24+
{
25+
public readonly object $service;
26+
27+
public function __construct(
28+
private \Closure $initializer,
29+
) {
30+
unset($this->service);
31+
}
32+
33+
public function __get(mixed $name): mixed
34+
{
35+
if ('service' !== $name) {
36+
throw new InvalidArgumentException(sprintf('Cannot read property "%s" from a lazy closure.', $name));
37+
}
38+
39+
if (isset($this->initializer)) {
40+
$this->service = ($this->initializer)();
41+
unset($this->initializer);
42+
}
43+
44+
return $this->service;
45+
}
46+
47+
public static function getCode(string $initializer, ?\ReflectionClass $r, string $method, ?string $id): string
48+
{
49+
if (!$r || !$r->hasMethod($method)) {
50+
throw new RuntimeException(sprintf('Cannot create lazy closure for service "%s" because its corresponding callable is invalid.', $id));
51+
}
52+
53+
$signature = ProxyHelper::exportSignature($r->getMethod($method));
54+
$signature = preg_replace('/: static$/', ': \\'.$r->name, $signature);
55+
56+
return '(new class('.$initializer.') extends \\'.self::class.' { '
57+
.$signature.' { return $this->service->'.$method.'(...\func_get_args()); } '
58+
.'})->'.$method.'(...)';
59+
}
60+
}

src/Symfony/Component/DependencyInjection/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
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
* Deprecate `#[MapDecorated]`, use `#[AutowireDecorated]` instead
2021
* Deprecate the `@required` annotation, use the `Symfony\Contracts\Service\Attribute\Required` attribute instead

src/Symfony/Component/DependencyInjection/ContainerBuilder.php

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
use Symfony\Component\Config\Resource\ResourceInterface;
2323
use Symfony\Component\DependencyInjection\Argument\AbstractArgument;
2424
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
25+
use Symfony\Component\DependencyInjection\Argument\LazyClosure;
2526
use Symfony\Component\DependencyInjection\Argument\RewindableGenerator;
2627
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
2728
use Symfony\Component\DependencyInjection\Argument\ServiceLocator;
@@ -1050,13 +1051,40 @@ private function createService(Definition $definition, array &$inlineServices, b
10501051
}
10511052

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

1054-
if (true === $tryProxy && $definition->isLazy() && !$tryProxy = !($proxy = $this->proxyInstantiator ??= new LazyServiceInstantiator()) || $proxy instanceof RealServiceInstantiator) {
1056+
if ('Closure' === $class && $definition->isLazy() && ['Closure', 'fromCallable'] === $definition->getFactory()) {
1057+
$callable = $parameterBag->unescapeValue($parameterBag->resolveValue($definition->getArgument(0)));
1058+
1059+
if ($callable instanceof Reference || $callable instanceof Definition) {
1060+
$callable = [$callable, '__invoke'];
1061+
}
1062+
1063+
if (\is_array($callable) && (
1064+
$callable[0] instanceof Reference
1065+
|| $callable[0] instanceof Definition && !isset($inlineServices[spl_object_hash($callable[0])])
1066+
)) {
1067+
$containerRef = $this->containerRef ??= \WeakReference::create($this);
1068+
$class = ($callable[0] instanceof Reference ? $this->findDefinition($callable[0]) : $callable[0])->getClass();
1069+
$initializer = static function () use ($containerRef, $callable, &$inlineServices) {
1070+
return $containerRef->get()->doResolveServices($callable[0], $inlineServices);
1071+
};
1072+
1073+
$proxy = eval('return '.LazyClosure::getCode('$initializer', $this->getReflectionClass($class), $callable[1], $id).';');
1074+
$this->shareService($definition, $proxy, $id, $inlineServices);
1075+
1076+
return $proxy;
1077+
}
1078+
}
1079+
1080+
if (true === $tryProxy && $definition->isLazy() && 'Closure' !== $class
1081+
&& !$tryProxy = !($proxy = $this->proxyInstantiator ??= new LazyServiceInstantiator()) || $proxy instanceof RealServiceInstantiator
1082+
) {
10551083
$containerRef = $this->containerRef ??= \WeakReference::create($this);
10561084
$proxy = $proxy->instantiateProxy(
10571085
$this,
10581086
(clone $definition)
1059-
->setClass($parameterBag->resolveValue($definition->getClass()))
1087+
->setClass($class)
10601088
->setTags(($definition->hasTag('proxy') ? ['proxy' => $parameterBag->resolveValue($definition->getTag('proxy'))] : []) + $definition->getTags()),
10611089
$id, static function ($proxy = false) use ($containerRef, $definition, &$inlineServices, $id) {
10621090
return $containerRef->get()->createService($definition, $inlineServices, true, $id, $proxy);
@@ -1105,7 +1133,7 @@ private function createService(Definition $definition, array &$inlineServices, b
11051133
}
11061134
}
11071135
} else {
1108-
$r = new \ReflectionClass($parameterBag->resolveValue($definition->getClass()));
1136+
$r = new \ReflectionClass($class);
11091137

11101138
if (\is_object($tryProxy)) {
11111139
if ($r->getConstructor()) {

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use Symfony\Component\DependencyInjection\Argument\AbstractArgument;
1616
use Symfony\Component\DependencyInjection\Argument\ArgumentInterface;
1717
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
18+
use Symfony\Component\DependencyInjection\Argument\LazyClosure;
1819
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
1920
use Symfony\Component\DependencyInjection\Argument\ServiceLocator;
2021
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
@@ -1179,6 +1180,22 @@ private function addNewInstance(Definition $definition, string $return = '', str
11791180
throw new RuntimeException(sprintf('Cannot dump definition because of invalid factory method (%s).', $callable[1] ?: 'n/a'));
11801181
}
11811182

1183+
if (['...'] === $arguments && $definition->isLazy() && 'Closure' === ($definition->getClass() ?? 'Closure') && (
1184+
$callable[0] instanceof Reference
1185+
|| ($callable[0] instanceof Definition && !$this->definitionVariables->contains($callable[0]))
1186+
)) {
1187+
$class = ($callable[0] instanceof Reference ? $this->container->findDefinition($callable[0]) : $callable[0])->getClass();
1188+
1189+
if (str_contains($initializer = $this->dumpValue($callable[0]), '$container')) {
1190+
$this->addContainerRef = true;
1191+
$initializer = sprintf('function () use ($containerRef) { $container = $containerRef; return %s; }', $initializer);
1192+
} else {
1193+
$initializer = 'fn () => '.$initializer;
1194+
}
1195+
1196+
return $return.LazyClosure::getCode($initializer, $this->container->getReflectionClass($class), $callable[1], $id).$tail;
1197+
}
1198+
11821199
if ($callable[0] instanceof Reference
11831200
|| ($callable[0] instanceof Definition && $this->definitionVariables->contains($callable[0]))
11841201
) {
@@ -2327,6 +2344,10 @@ private function isProxyCandidate(Definition $definition, ?bool &$asGhostObject,
23272344
{
23282345
$asGhostObject = false;
23292346

2347+
if ('Closure' === ($definition->getClass() ?: (['Closure', 'fromCallable'] === $definition->getFactory() ? 'Closure' : null))) {
2348+
return null;
2349+
}
2350+
23302351
if (!$definition->isLazy() || !$this->hasProxyDumper) {
23312352
return null;
23322353
}

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1985,6 +1985,25 @@ public function testNamedArgumentBeforeCompile()
19851985

19861986
$this->assertSame(1, $e->first);
19871987
}
1988+
1989+
public function testLazyClosure()
1990+
{
1991+
$container = new ContainerBuilder();
1992+
$container->register('closure', 'Closure')
1993+
->setPublic('true')
1994+
->setFactory(['Closure', 'fromCallable'])
1995+
->setLazy(true)
1996+
->setArguments([[new Reference('foo'), 'cloneFoo']]);
1997+
$container->register('foo', Foo::class);
1998+
$container->compile();
1999+
2000+
$cloned = Foo::$counter;
2001+
$this->assertInstanceOf(\Closure::class, $container->get('closure'));
2002+
$this->assertSame($cloned, Foo::$counter);
2003+
$this->assertInstanceOf(Foo::class, $container->get('closure')());
2004+
$this->assertSame(1 + $cloned, Foo::$counter);
2005+
$this->assertSame(1, (new \ReflectionFunction($container->get('closure')))->getNumberOfParameters());
2006+
}
19882007
}
19892008

19902009
class FooClass

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

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1719,6 +1719,32 @@ public function testAutowireClosure()
17191719
$this->assertInstanceOf(Foo::class, $fooClone = ($bar->buz)());
17201720
$this->assertNotSame($container->get('foo'), $fooClone);
17211721
}
1722+
1723+
public function testLazyClosure()
1724+
{
1725+
$container = new ContainerBuilder();
1726+
$container->register('closure', 'Closure')
1727+
->setPublic('true')
1728+
->setFactory(['Closure', 'fromCallable'])
1729+
->setLazy(true)
1730+
->setArguments([[new Reference('foo'), 'cloneFoo']]);
1731+
$container->register('foo', Foo::class);
1732+
$container->compile();
1733+
$dumper = new PhpDumper($container);
1734+
1735+
$this->assertStringEqualsFile(self::$fixturesPath.'/php/lazy_closure.php', $dumper->dump(['class' => 'Symfony_DI_PhpDumper_Test_Lazy_Closure']));
1736+
1737+
require self::$fixturesPath.'/php/lazy_closure.php';
1738+
1739+
$container = new \Symfony_DI_PhpDumper_Test_Lazy_Closure();
1740+
1741+
$cloned = Foo::$counter;
1742+
$this->assertInstanceOf(\Closure::class, $container->get('closure'));
1743+
$this->assertSame($cloned, Foo::$counter);
1744+
$this->assertInstanceOf(Foo::class, $container->get('closure')());
1745+
$this->assertSame(1 + $cloned, Foo::$counter);
1746+
$this->assertSame(1, (new \ReflectionFunction($container->get('closure')))->getNumberOfParameters());
1747+
}
17221748
}
17231749

17241750
class Rot13EnvVarProcessor implements EnvVarProcessorInterface

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,13 @@ public function cloneFoo(): static
2626

2727
class Foo
2828
{
29+
public static int $counter = 0;
30+
2931
#[Required]
30-
public function cloneFoo(): static
32+
public function cloneFoo(\stdClass $bar = null): static
3133
{
34+
++self::$counter;
35+
3236
return clone $this;
3337
}
3438
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
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+
return $container->services['closure'] = (new class(fn () => new \Symfony\Component\DependencyInjection\Tests\Compiler\Foo()) extends \Symfony\Component\DependencyInjection\Argument\LazyClosure { public function cloneFoo(?\stdClass $bar = null): \Symfony\Component\DependencyInjection\Tests\Compiler\Foo { return $this->service->cloneFoo(...\func_get_args()); } })->cloneFoo(...);
61+
}
62+
}

0 commit comments

Comments
 (0)