Skip to content

[DependencyInjection] Leverage native lazy objects for lazy services #59913

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/Symfony/Bridge/Doctrine/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
CHANGELOG
=========

7.3
---

* Reset the manager registry using native lazy objects when applicable

7.2
---

Expand Down
77 changes: 53 additions & 24 deletions src/Symfony/Bridge/Doctrine/ManagerRegistry.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,31 +45,60 @@ protected function resetService($name): void

return;
}
if (!$manager instanceof LazyLoadingInterface) {
throw new \LogicException(\sprintf('Resetting a non-lazy manager service is not supported. Declare the "%s" service as lazy.', $name));
if (\PHP_VERSION_ID < 80400) {
if (!$manager instanceof LazyLoadingInterface) {
throw new \LogicException(\sprintf('Resetting a non-lazy manager service is not supported. Declare the "%s" service as lazy.', $name));
}
trigger_deprecation('symfony/doctrine-bridge', '7.3', 'Support for proxy-manager is deprecated.');
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This case should never happen (who would use proxy-manager directly instead of Symfony's lazy services for the Doctrine EM!?), still added for completeness.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isn't this deprecation always triggered on PHP 8.3 and older ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's guarded by if (!$manager instanceof LazyLoadingInterface) { (note the !) so only when ppl use proxy-manager, which we don't anymore thanks to var-exporter.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no. This deprecation is triggered after that if, when we haven't thrown an exception (and so in the case where we will actually reset the object)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh, I missed that the case of lazy objects using var-exporter are handled before the check for PHP 8.4, so this is not the code path we reach in modern projects.


if ($manager instanceof GhostObjectInterface) {
throw new \LogicException('Resetting a lazy-ghost-object manager service is not supported.');
}
$manager->setProxyInitializer(\Closure::bind(
function (&$wrappedInstance, LazyLoadingInterface $manager) use ($name) {
$name = $this->aliases[$name] ?? $name;
$wrappedInstance = match (true) {
isset($this->fileMap[$name]) => $this->load($this->fileMap[$name], false),
(new \ReflectionMethod($this, $method = $this->methodMap[$name]))->isStatic() => $this->{$method}($this, false),
default => $this->{$method}(false),
};
$manager->setProxyInitializer(null);

return true;
},
$this->container,
Container::class
));

return;
}
if ($manager instanceof GhostObjectInterface) {
throw new \LogicException('Resetting a lazy-ghost-object manager service is not supported.');

$r = new \ReflectionClass($manager);

if ($r->isUninitializedLazyObject($manager)) {
return;
}

try {
$r->resetAsLazyProxy($manager, \Closure::bind(
function () use ($name) {
$name = $this->aliases[$name] ?? $name;

return match (true) {
isset($this->fileMap[$name]) => $this->load($this->fileMap[$name], false),
(new \ReflectionMethod($this, $method = $this->methodMap[$name]))->isStatic() => $this->{$method}($this, false),
default => $this->{$method}(false),
};
},
$this->container,
Container::class
));
} catch (\Error $e) {
if (__FILE__ !== $e->getFile()) {
throw $e;
}

throw new \LogicException(\sprintf('Resetting a non-lazy manager service is not supported. Declare the "%s" service as lazy.', $name), 0, $e);
}
$manager->setProxyInitializer(\Closure::bind(
function (&$wrappedInstance, LazyLoadingInterface $manager) use ($name) {
if (isset($this->aliases[$name])) {
$name = $this->aliases[$name];
}
if (isset($this->fileMap[$name])) {
$wrappedInstance = $this->load($this->fileMap[$name], false);
} elseif ((new \ReflectionMethod($this, $this->methodMap[$name]))->isStatic()) {
$wrappedInstance = $this->{$this->methodMap[$name]}($this, false);
} else {
$wrappedInstance = $this->{$this->methodMap[$name]}(false);
}

$manager->setProxyInitializer(null);

return true;
},
$this->container,
Container::class
));
}
}
1 change: 1 addition & 0 deletions src/Symfony/Component/DependencyInjection/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ CHANGELOG
* Don't skip classes with private constructor when autodiscovering
* Add `Definition::addExcludeTag()` and `ContainerBuilder::findExcludedServiceIds()`
for auto-configuration of classes excluded from the service container
* Leverage native lazy objects when possible for lazy services

7.2
---
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,19 @@
throw new InvalidArgumentException(\sprintf('Cannot instantiate lazy proxy for service "%s".', $id));
}

if (!class_exists($proxyClass = $dumper->getProxyClass($definition, $asGhostObject), false)) {
if (\PHP_VERSION_ID >= 80400 && $asGhostObject) {
return (new \ReflectionClass($definition->getClass()))->newLazyGhost(static function ($ghost) use ($realInstantiator) { $realInstantiator($ghost); });

Check failure on line 33 in src/Symfony/Component/DependencyInjection/LazyProxy/Instantiator/LazyServiceInstantiator.php

View workflow job for this annotation

GitHub Actions / Psalm

UndefinedMethod

src/Symfony/Component/DependencyInjection/LazyProxy/Instantiator/LazyServiceInstantiator.php:33:69: UndefinedMethod: Method ReflectionClass::newLazyGhost does not exist (see https://psalm.dev/022)

Check failure on line 33 in src/Symfony/Component/DependencyInjection/LazyProxy/Instantiator/LazyServiceInstantiator.php

View workflow job for this annotation

GitHub Actions / Psalm

UndefinedMethod

src/Symfony/Component/DependencyInjection/LazyProxy/Instantiator/LazyServiceInstantiator.php:33:69: UndefinedMethod: Method ReflectionClass::newLazyGhost does not exist (see https://psalm.dev/022)
}

$class = null;
if (!class_exists($proxyClass = $dumper->getProxyClass($definition, $asGhostObject, $class), false)) {
eval($dumper->getProxyCode($definition, $id));
}

return $asGhostObject ? $proxyClass::createLazyGhost($realInstantiator) : $proxyClass::createLazyProxy($realInstantiator);
if ($definition->getClass() === $proxyClass) {
return $class->newLazyProxy($realInstantiator);

Check failure on line 42 in src/Symfony/Component/DependencyInjection/LazyProxy/Instantiator/LazyServiceInstantiator.php

View workflow job for this annotation

GitHub Actions / Psalm

UndefinedMethod

src/Symfony/Component/DependencyInjection/LazyProxy/Instantiator/LazyServiceInstantiator.php:42:28: UndefinedMethod: Method ReflectionClass::newLazyProxy does not exist (see https://psalm.dev/022)

Check failure on line 42 in src/Symfony/Component/DependencyInjection/LazyProxy/Instantiator/LazyServiceInstantiator.php

View workflow job for this annotation

GitHub Actions / Psalm

UndefinedMethod

src/Symfony/Component/DependencyInjection/LazyProxy/Instantiator/LazyServiceInstantiator.php:42:28: UndefinedMethod: Method ReflectionClass::newLazyProxy does not exist (see https://psalm.dev/022)
}

return \PHP_VERSION_ID < 80400 && $asGhostObject ? $proxyClass::createLazyGhost($realInstantiator) : $proxyClass::createLazyProxy($realInstantiator);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,21 @@
}
}

if (\PHP_VERSION_ID < 80400) {
try {
$asGhostObject = (bool) ProxyHelper::generateLazyGhost(new \ReflectionClass($class));
} catch (LogicException) {
}

return true;
}

try {
$asGhostObject = (bool) ProxyHelper::generateLazyGhost(new \ReflectionClass($class));
} catch (LogicException) {
$asGhostObject = (bool) (new \ReflectionClass($class))->newLazyGhost(static fn () => null);

Check failure on line 69 in src/Symfony/Component/DependencyInjection/LazyProxy/PhpDumper/LazyServiceDumper.php

View workflow job for this annotation

GitHub Actions / Psalm

UndefinedMethod

src/Symfony/Component/DependencyInjection/LazyProxy/PhpDumper/LazyServiceDumper.php:69:69: UndefinedMethod: Method ReflectionClass::newLazyGhost does not exist (see https://psalm.dev/022)

Check failure on line 69 in src/Symfony/Component/DependencyInjection/LazyProxy/PhpDumper/LazyServiceDumper.php

View workflow job for this annotation

GitHub Actions / Psalm

UndefinedMethod

src/Symfony/Component/DependencyInjection/LazyProxy/PhpDumper/LazyServiceDumper.php:69:69: UndefinedMethod: Method ReflectionClass::newLazyGhost does not exist (see https://psalm.dev/022)
} catch (\Error $e) {
if (__FILE__ !== $e->getFile()) {
throw $e;
}
}

return true;
Expand All @@ -76,6 +88,16 @@
$proxyClass = $this->getProxyClass($definition, $asGhostObject);

if (!$asGhostObject) {
if ($definition->getClass() === $proxyClass) {
return <<<EOF
if (true === \$lazyLoad) {
$instantiation new \ReflectionClass('$proxyClass')->newLazyProxy(static fn () => $factoryCode);
}


EOF;
}

return <<<EOF
if (true === \$lazyLoad) {
$instantiation \$container->createProxy('$proxyClass', static fn () => \\$proxyClass::createLazyProxy(static fn () => $factoryCode));
Expand All @@ -85,11 +107,23 @@
EOF;
}

$factoryCode = \sprintf('static fn ($proxy) => %s', $factoryCode);
if (\PHP_VERSION_ID < 80400) {
$factoryCode = \sprintf('static fn ($proxy) => %s', $factoryCode);

return <<<EOF
if (true === \$lazyLoad) {
$instantiation \$container->createProxy('$proxyClass', static fn () => \\$proxyClass::createLazyGhost($factoryCode));
}


EOF;
}

$factoryCode = \sprintf('static function ($proxy) use ($container) { %s; }', $factoryCode);

return <<<EOF
if (true === \$lazyLoad) {
$instantiation \$container->createProxy('$proxyClass', static fn () => \\$proxyClass::createLazyGhost($factoryCode));
$instantiation new \ReflectionClass('$proxyClass')->newLazyGhost($factoryCode);
}


Expand All @@ -104,12 +138,21 @@
$proxyClass = $this->getProxyClass($definition, $asGhostObject, $class);

if ($asGhostObject) {
if (\PHP_VERSION_ID >= 80400) {
return '';
}

try {
return ($class?->isReadOnly() ? 'readonly ' : '').'class '.$proxyClass.ProxyHelper::generateLazyGhost($class);
} catch (LogicException $e) {
throw new InvalidArgumentException(\sprintf('Cannot generate lazy ghost for service "%s".', $id ?? $definition->getClass()), 0, $e);
}
}

if ($definition->getClass() === $proxyClass) {
return '';
}

$interfaces = [];

if ($definition->hasTag('proxy')) {
Expand Down Expand Up @@ -144,6 +187,16 @@
$class = 'object' !== $definition->getClass() ? $definition->getClass() : 'stdClass';
$class = new \ReflectionClass($class);

if (\PHP_VERSION_ID >= 80400) {
if ($asGhostObject) {
return $class->name;
}

if (!$definition->hasTag('proxy') && !$class->isInterface()) {
return $class->name;
}
}

return preg_replace('/^.*\\\\/', '', $definition->getClass())
.($asGhostObject ? 'Ghost' : 'Proxy')
.ucfirst(substr(hash('xxh128', $this->salt.'+'.$class->name.'+'.serialize($definition->getTag('proxy'))), -7));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1919,8 +1919,12 @@ public function testLazyWither()
$container->compile();

$wither = $container->get('wither');
if (\PHP_VERSION_ID >= 80400) {
$this->assertTrue((new \ReflectionClass($wither))->isUninitializedLazyObject($wither));
} else {
$this->assertTrue($wither->resetLazyObject());
}
$this->assertInstanceOf(Foo::class, $wither->foo);
$this->assertTrue($wither->resetLazyObject());
$this->assertInstanceOf(Wither::class, $wither->withFoo1($wither->foo));
}

Expand Down
Loading
Loading