Skip to content

Commit 5817058

Browse files
[DependencyInjection] Leverage native lazy objects for lazy services
1 parent 66deb32 commit 5817058

25 files changed

+1154
-163
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ CHANGELOG
99
* Don't skip classes with private constructor when autodiscovering
1010
* Add `Definition::addExcludeTag()` and `ContainerBuilder::findExcludedServiceIds()`
1111
for auto-configuration of classes excluded from the service container
12+
* Leverage native lazy objects when possible for lazy services
1213

1314
7.2
1415
---

LazyProxy/Instantiator/LazyServiceInstantiator.php

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,19 @@ 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), false)) {
32+
if (\PHP_VERSION_ID >= 80400 && $asGhostObject) {
33+
return (new \ReflectionClass($definition->getClass()))->newLazyGhost(static function ($ghost) use ($realInstantiator) { $realInstantiator($ghost); });
34+
}
35+
36+
$class = null;
37+
if (!class_exists($proxyClass = $dumper->getProxyClass($definition, $asGhostObject, $class), false)) {
3338
eval($dumper->getProxyCode($definition, $id));
3439
}
3540

36-
return $asGhostObject ? $proxyClass::createLazyGhost($realInstantiator) : $proxyClass::createLazyProxy($realInstantiator);
41+
if ($definition->getClass() === $proxyClass) {
42+
return $class->newLazyProxy($realInstantiator);
43+
}
44+
45+
return \PHP_VERSION_ID < 80400 && $asGhostObject ? $proxyClass::createLazyGhost($realInstantiator) : $proxyClass::createLazyProxy($realInstantiator);
3746
}
3847
}

LazyProxy/PhpDumper/LazyServiceDumper.php

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,21 @@ public function isProxyCandidate(Definition $definition, ?bool &$asGhostObject =
5656
}
5757
}
5858

59+
if (\PHP_VERSION_ID < 80400) {
60+
try {
61+
$asGhostObject = (bool) ProxyHelper::generateLazyGhost(new \ReflectionClass($class));
62+
} catch (LogicException) {
63+
}
64+
65+
return true;
66+
}
67+
5968
try {
60-
$asGhostObject = (bool) ProxyHelper::generateLazyGhost(new \ReflectionClass($class));
61-
} catch (LogicException) {
69+
$asGhostObject = (bool) (new \ReflectionClass($class))->newLazyGhost(static fn () => null);
70+
} catch (\Error $e) {
71+
if (__FILE__ !== $e->getFile()) {
72+
throw $e;
73+
}
6274
}
6375

6476
return true;
@@ -76,6 +88,16 @@ public function getProxyFactoryCode(Definition $definition, string $id, string $
7688
$proxyClass = $this->getProxyClass($definition, $asGhostObject);
7789

7890
if (!$asGhostObject) {
91+
if ($definition->getClass() === $proxyClass) {
92+
return <<<EOF
93+
if (true === \$lazyLoad) {
94+
$instantiation new \ReflectionClass('$proxyClass')->newLazyProxy(static fn () => $factoryCode);
95+
}
96+
97+
98+
EOF;
99+
}
100+
79101
return <<<EOF
80102
if (true === \$lazyLoad) {
81103
$instantiation \$container->createProxy('$proxyClass', static fn () => \\$proxyClass::createLazyProxy(static fn () => $factoryCode));
@@ -85,11 +107,23 @@ public function getProxyFactoryCode(Definition $definition, string $id, string $
85107
EOF;
86108
}
87109

88-
$factoryCode = \sprintf('static fn ($proxy) => %s', $factoryCode);
110+
if (\PHP_VERSION_ID < 80400) {
111+
$factoryCode = \sprintf('static fn ($proxy) => %s', $factoryCode);
112+
113+
return <<<EOF
114+
if (true === \$lazyLoad) {
115+
$instantiation \$container->createProxy('$proxyClass', static fn () => \\$proxyClass::createLazyGhost($factoryCode));
116+
}
117+
118+
119+
EOF;
120+
}
121+
122+
$factoryCode = \sprintf('static function ($proxy) use ($container) { %s; }', $factoryCode);
89123

90124
return <<<EOF
91125
if (true === \$lazyLoad) {
92-
$instantiation \$container->createProxy('$proxyClass', static fn () => \\$proxyClass::createLazyGhost($factoryCode));
126+
$instantiation new \ReflectionClass('$proxyClass')->newLazyGhost($factoryCode);
93127
}
94128
95129
@@ -104,12 +138,21 @@ public function getProxyCode(Definition $definition, ?string $id = null): string
104138
$proxyClass = $this->getProxyClass($definition, $asGhostObject, $class);
105139

106140
if ($asGhostObject) {
141+
if (\PHP_VERSION_ID >= 80400) {
142+
return '';
143+
}
144+
107145
try {
108146
return ($class?->isReadOnly() ? 'readonly ' : '').'class '.$proxyClass.ProxyHelper::generateLazyGhost($class);
109147
} catch (LogicException $e) {
110148
throw new InvalidArgumentException(\sprintf('Cannot generate lazy ghost for service "%s".', $id ?? $definition->getClass()), 0, $e);
111149
}
112150
}
151+
152+
if ($definition->getClass() === $proxyClass) {
153+
return '';
154+
}
155+
113156
$interfaces = [];
114157

115158
if ($definition->hasTag('proxy')) {
@@ -144,6 +187,16 @@ public function getProxyClass(Definition $definition, bool $asGhostObject, ?\Ref
144187
$class = 'object' !== $definition->getClass() ? $definition->getClass() : 'stdClass';
145188
$class = new \ReflectionClass($class);
146189

190+
if (\PHP_VERSION_ID >= 80400) {
191+
if ($asGhostObject) {
192+
return $class->name;
193+
}
194+
195+
if (!$definition->hasTag('proxy') && !$class->isInterface()) {
196+
return $class->name;
197+
}
198+
}
199+
147200
return preg_replace('/^.*\\\\/', '', $definition->getClass())
148201
.($asGhostObject ? 'Ghost' : 'Proxy')
149202
.ucfirst(substr(hash('xxh128', $this->salt.'+'.$class->name.'+'.serialize($definition->getTag('proxy'))), -7));

Tests/ContainerBuilderTest.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1919,8 +1919,12 @@ public function testLazyWither()
19191919
$container->compile();
19201920

19211921
$wither = $container->get('wither');
1922+
if (\PHP_VERSION_ID >= 80400) {
1923+
$this->assertTrue((new \ReflectionClass($wither))->isUninitializedLazyObject($wither));
1924+
} else {
1925+
$this->assertTrue($wither->resetLazyObject());
1926+
}
19221927
$this->assertInstanceOf(Foo::class, $wither->foo);
1923-
$this->assertTrue($wither->resetLazyObject());
19241928
$this->assertInstanceOf(Wither::class, $wither->withFoo1($wither->foo));
19251929
}
19261930

Tests/Dumper/PhpDumperTest.php

Lines changed: 52 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -340,7 +340,7 @@ public function testDumpAsFilesWithLazyFactoriesInlined()
340340
if ('\\' === \DIRECTORY_SEPARATOR) {
341341
$dump = str_replace("'.\\DIRECTORY_SEPARATOR.'", '/', $dump);
342342
}
343-
$this->assertStringMatchesFormatFile(self::$fixturesPath.'/php/services9_lazy_inlined_factories.txt', $dump);
343+
$this->assertStringMatchesFormatFile(self::$fixturesPath.'/php/'.(\PHP_VERSION_ID < 80400 ? 'legacy_' : '').'services9_lazy_inlined_factories.txt', $dump);
344344
}
345345

346346
public function testServicesWithAnonymousFactories()
@@ -794,18 +794,26 @@ public function testNonSharedLazy()
794794
'inline_class_loader' => false,
795795
]);
796796
$this->assertStringEqualsFile(
797-
self::$fixturesPath.'/php/services_non_shared_lazy_public.php',
797+
self::$fixturesPath.'/php/'.(\PHP_VERSION_ID < 80400 ? 'legacy_' : '').'services_non_shared_lazy_public.php',
798798
'\\' === \DIRECTORY_SEPARATOR ? str_replace("'.\\DIRECTORY_SEPARATOR.'", '/', $dump) : $dump
799799
);
800800
eval('?>'.$dump);
801801

802802
$container = new \Symfony_DI_PhpDumper_Service_Non_Shared_Lazy();
803803

804804
$foo1 = $container->get('foo');
805-
$this->assertTrue($foo1->resetLazyObject());
805+
if (\PHP_VERSION_ID >= 80400) {
806+
$this->assertTrue((new \ReflectionClass($foo1))->isUninitializedLazyObject($foo1));
807+
} else {
808+
$this->assertTrue($foo1->resetLazyObject());
809+
}
806810

807811
$foo2 = $container->get('foo');
808-
$this->assertTrue($foo2->resetLazyObject());
812+
if (\PHP_VERSION_ID >= 80400) {
813+
$this->assertTrue((new \ReflectionClass($foo2))->isUninitializedLazyObject($foo2));
814+
} else {
815+
$this->assertTrue($foo2->resetLazyObject());
816+
}
809817

810818
$this->assertNotSame($foo1, $foo2);
811819
}
@@ -832,7 +840,7 @@ public function testNonSharedLazyAsFiles()
832840

833841
$stringDump = print_r($dumps, true);
834842
$this->assertStringMatchesFormatFile(
835-
self::$fixturesPath.'/php/services_non_shared_lazy_as_files.txt',
843+
self::$fixturesPath.'/php/'.(\PHP_VERSION_ID < 80400 ? 'legacy_' : '').'services_non_shared_lazy_as_files.txt',
836844
'\\' === \DIRECTORY_SEPARATOR ? str_replace("'.\\DIRECTORY_SEPARATOR.'", '/', $stringDump) : $stringDump
837845
);
838846

@@ -844,10 +852,18 @@ public function testNonSharedLazyAsFiles()
844852
$container = eval('?>'.$lastDump);
845853

846854
$foo1 = $container->get('non_shared_foo');
847-
$this->assertTrue($foo1->resetLazyObject());
855+
if (\PHP_VERSION_ID >= 80400) {
856+
$this->assertTrue((new \ReflectionClass($foo1))->isUninitializedLazyObject($foo1));
857+
} else {
858+
$this->assertTrue($foo1->resetLazyObject());
859+
}
848860

849861
$foo2 = $container->get('non_shared_foo');
850-
$this->assertTrue($foo2->resetLazyObject());
862+
if (\PHP_VERSION_ID >= 80400) {
863+
$this->assertTrue((new \ReflectionClass($foo2))->isUninitializedLazyObject($foo2));
864+
} else {
865+
$this->assertTrue($foo2->resetLazyObject());
866+
}
851867

852868
$this->assertNotSame($foo1, $foo2);
853869
}
@@ -869,7 +885,7 @@ public function testNonSharedLazyDefinitionReferences(bool $asGhostObject)
869885
$dumper->setProxyDumper(new \DummyProxyDumper());
870886
}
871887

872-
$this->assertStringEqualsFile(self::$fixturesPath.'/php/services_non_shared_lazy'.($asGhostObject ? '_ghost' : '').'.php', $dumper->dump());
888+
$this->assertStringEqualsFile(self::$fixturesPath.'/php/'.(\PHP_VERSION_ID < 80400 ? 'legacy_' : '').'services_non_shared_lazy'.($asGhostObject ? '_ghost' : '').'.php', $dumper->dump());
873889
}
874890

875891
public function testNonSharedDuplicates()
@@ -942,7 +958,7 @@ public function testDedupLazyProxy()
942958

943959
$dumper = new PhpDumper($container);
944960

945-
$this->assertStringEqualsFile(self::$fixturesPath.'/php/services_dedup_lazy.php', $dumper->dump());
961+
$this->assertStringEqualsFile(self::$fixturesPath.'/php/'.(\PHP_VERSION_ID < 80400 ? 'legacy_' : '').'services_dedup_lazy.php', $dumper->dump());
946962
}
947963

948964
public function testLazyArgumentProvideGenerator()
@@ -1607,14 +1623,18 @@ public function testLazyWither()
16071623
$container->compile();
16081624
$dumper = new PhpDumper($container);
16091625
$dump = $dumper->dump(['class' => 'Symfony_DI_PhpDumper_Service_Wither_Lazy']);
1610-
$this->assertStringEqualsFile(self::$fixturesPath.'/php/services_wither_lazy.php', $dump);
1626+
$this->assertStringEqualsFile(self::$fixturesPath.'/php/'.(\PHP_VERSION_ID < 80400 ? 'legacy_' : '').'services_wither_lazy.php', $dump);
16111627
eval('?>'.$dump);
16121628

16131629
$container = new \Symfony_DI_PhpDumper_Service_Wither_Lazy();
16141630

16151631
$wither = $container->get('wither');
1632+
if (\PHP_VERSION_ID >= 80400) {
1633+
$this->assertTrue((new \ReflectionClass($wither))->isUninitializedLazyObject($wither));
1634+
} else {
1635+
$this->assertTrue($wither->resetLazyObject());
1636+
}
16161637
$this->assertInstanceOf(Foo::class, $wither->foo);
1617-
$this->assertTrue($wither->resetLazyObject());
16181638
}
16191639

16201640
public function testLazyWitherNonShared()
@@ -1632,18 +1652,26 @@ public function testLazyWitherNonShared()
16321652
$container->compile();
16331653
$dumper = new PhpDumper($container);
16341654
$dump = $dumper->dump(['class' => 'Symfony_DI_PhpDumper_Service_Wither_Lazy_Non_Shared']);
1635-
$this->assertStringEqualsFile(self::$fixturesPath.'/php/services_wither_lazy_non_shared.php', $dump);
1655+
$this->assertStringEqualsFile(self::$fixturesPath.'/php/'.(\PHP_VERSION_ID < 80400 ? 'legacy_' : '').'services_wither_lazy_non_shared.php', $dump);
16361656
eval('?>'.$dump);
16371657

16381658
$container = new \Symfony_DI_PhpDumper_Service_Wither_Lazy_Non_Shared();
16391659

16401660
$wither1 = $container->get('wither');
1661+
if (\PHP_VERSION_ID >= 80400) {
1662+
$this->assertTrue((new \ReflectionClass($wither1))->isUninitializedLazyObject($wither1));
1663+
} else {
1664+
$this->assertTrue($wither1->resetLazyObject());
1665+
}
16411666
$this->assertInstanceOf(Foo::class, $wither1->foo);
1642-
$this->assertTrue($wither1->resetLazyObject());
16431667

16441668
$wither2 = $container->get('wither');
1669+
if (\PHP_VERSION_ID >= 80400) {
1670+
$this->assertTrue((new \ReflectionClass($wither2))->isUninitializedLazyObject($wither2));
1671+
} else {
1672+
$this->assertTrue($wither2->resetLazyObject());
1673+
}
16451674
$this->assertInstanceOf(Foo::class, $wither2->foo);
1646-
$this->assertTrue($wither2->resetLazyObject());
16471675

16481676
$this->assertNotSame($wither1, $wither2);
16491677
}
@@ -1971,15 +1999,21 @@ public function testLazyAutowireAttribute()
19711999
$container->compile();
19722000
$dumper = new PhpDumper($container);
19732001

1974-
$this->assertStringEqualsFile(self::$fixturesPath.'/php/lazy_autowire_attribute.php', $dumper->dump(['class' => 'Symfony_DI_PhpDumper_Test_Lazy_Autowire_Attribute']));
2002+
$this->assertStringEqualsFile(self::$fixturesPath.'/php/'.(\PHP_VERSION_ID < 80400 ? 'legacy_' : '').'lazy_autowire_attribute.php', $dumper->dump(['class' => 'Symfony_DI_PhpDumper_Test_Lazy_Autowire_Attribute']));
19752003

1976-
require self::$fixturesPath.'/php/lazy_autowire_attribute.php';
2004+
require self::$fixturesPath.'/php/'.(\PHP_VERSION_ID < 80400 ? 'legacy_' : '').'lazy_autowire_attribute.php';
19772005

19782006
$container = new \Symfony_DI_PhpDumper_Test_Lazy_Autowire_Attribute();
19792007

19802008
$this->assertInstanceOf(Foo::class, $container->get('bar')->foo);
1981-
$this->assertInstanceOf(LazyObjectInterface::class, $container->get('bar')->foo);
1982-
$this->assertSame($container->get('foo'), $container->get('bar')->foo->initializeLazyObject());
2009+
if (\PHP_VERSION_ID >= 80400) {
2010+
$r = new \ReflectionClass(Foo::class);
2011+
$this->assertTrue($r->isUninitializedLazyObject($container->get('bar')->foo));
2012+
$this->assertSame($container->get('foo'), $r->initializeLazyObject($container->get('bar')->foo));
2013+
} else {
2014+
$this->assertInstanceOf(LazyObjectInterface::class, $container->get('bar')->foo);
2015+
$this->assertSame($container->get('foo'), $container->get('bar')->foo->initializeLazyObject());
2016+
}
19832017
}
19842018

19852019
public function testLazyAutowireAttributeWithIntersection()

Tests/Fixtures/includes/autowiring_classes.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
class Foo
1515
{
1616
public static int $counter = 0;
17+
public int $foo = 0;
1718

1819
#[Required]
1920
public function cloneFoo(?\stdClass $bar = null): static

Tests/Fixtures/includes/foo_lazy.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@
44

55
class FooLazyClass
66
{
7+
public int $foo = 0;
78
}

Tests/Fixtures/php/lazy_autowire_attribute.php

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -77,21 +77,9 @@ protected static function getFooService($container)
7777
protected static function getFoo2Service($container, $lazyLoad = true)
7878
{
7979
if (true === $lazyLoad) {
80-
return $container->privates['.lazy.Symfony\\Component\\DependencyInjection\\Tests\\Compiler\\Foo'] = $container->createProxy('FooProxyCd8d23a', static fn () => \FooProxyCd8d23a::createLazyProxy(static fn () => self::getFoo2Service($container, false)));
80+
return $container->privates['.lazy.Symfony\\Component\\DependencyInjection\\Tests\\Compiler\\Foo'] = new \ReflectionClass('Symfony\Component\DependencyInjection\Tests\Compiler\Foo')->newLazyProxy(static fn () => self::getFoo2Service($container, false));
8181
}
8282

8383
return ($container->services['foo'] ??= new \Symfony\Component\DependencyInjection\Tests\Compiler\Foo());
8484
}
8585
}
86-
87-
class FooProxyCd8d23a extends \Symfony\Component\DependencyInjection\Tests\Compiler\Foo implements \Symfony\Component\VarExporter\LazyObjectInterface
88-
{
89-
use \Symfony\Component\VarExporter\LazyProxyTrait;
90-
91-
private const LAZY_OBJECT_PROPERTY_SCOPES = [];
92-
}
93-
94-
// Help opcache.preload discover always-needed symbols
95-
class_exists(\Symfony\Component\VarExporter\Internal\Hydrator::class);
96-
class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectRegistry::class);
97-
class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectState::class);

0 commit comments

Comments
 (0)