Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 2d74a76

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

File tree

Expand file treeCollapse file tree

8 files changed

+225
-4
lines changed
Filter options
Expand file treeCollapse file tree

8 files changed

+225
-4
lines changed
+60Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Component/DependencyInjection/CHANGELOG.md
+1Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Component/DependencyInjection/ContainerBuilder.php
+31-3Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php
+21Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php
+19Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php
+26Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/autowiring_classes.php
+5-1Lines 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
}
+62Lines 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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.