From a60cff54b6ed544b06b349b6852bc9f6dd2fa6ae Mon Sep 17 00:00:00 2001 From: Peter van Dommelen Date: Fri, 7 Feb 2025 11:36:11 +0100 Subject: [PATCH] [DependencyInjection] Fix cloned lazy services not sharing their dependencies when dumped with PhpDumper --- .../Compiler/InlineServiceDefinitionsPass.php | 4 +- .../DependencyInjection/Dumper/PhpDumper.php | 6 ++ .../Tests/Dumper/PhpDumperTest.php | 55 +++++++++++++++++++ .../Tests/Fixtures/DependencyContainer.php | 25 +++++++++ .../Fixtures/DependencyContainerInterface.php | 17 ++++++ .../Tests/Fixtures/config/child.expected.yml | 4 +- .../config/from_callable.expected.yml | 4 +- .../Tests/Fixtures/php/closure_proxy.php | 2 +- .../Tests/Fixtures/php/lazy_closure.php | 4 +- .../php/services_almost_circular_private.php | 44 ++++++++++++--- .../php/services_almost_circular_public.php | 18 +++++- .../Fixtures/php/services_wither_lazy.php | 2 +- 12 files changed, 168 insertions(+), 17 deletions(-) create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Fixtures/DependencyContainer.php create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Fixtures/DependencyContainerInterface.php diff --git a/src/Symfony/Component/DependencyInjection/Compiler/InlineServiceDefinitionsPass.php b/src/Symfony/Component/DependencyInjection/Compiler/InlineServiceDefinitionsPass.php index e0dc3a653e6b9..884977fff3d1f 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/InlineServiceDefinitionsPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/InlineServiceDefinitionsPass.php @@ -224,6 +224,8 @@ private function isInlineableDefinition(string $id, Definition $definition): boo return false; } - return $this->container->getDefinition($srcId)->isShared(); + $srcDefinition = $this->container->getDefinition($srcId); + + return $srcDefinition->isShared() && !$srcDefinition->isLazy(); } } diff --git a/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php b/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php index 23e65483b03d3..5f544a859ceb1 100644 --- a/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php +++ b/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php @@ -2185,6 +2185,12 @@ private function isSingleUsePrivateNode(ServiceReferenceGraphNode $node): bool if ($edge->isLazy() || !$value instanceof Definition || !$value->isShared()) { return false; } + + // When the source node is a proxy or ghost, it will construct its references only when the node itself is initialized. + // Since the node can be cloned before being fully initialized, we do not know how often its references are used. + if ($this->getProxyDumper()->isProxyCandidate($value)) { + return false; + } $ids[$edge->getSourceNode()->getId()] = true; } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php b/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php index 2d0a8aad0ce16..7622d858a3110 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php @@ -55,6 +55,8 @@ use Symfony\Component\DependencyInjection\Tests\Compiler\Wither; use Symfony\Component\DependencyInjection\Tests\Compiler\WitherAnnotation; use Symfony\Component\DependencyInjection\Tests\Fixtures\CustomDefinition; +use Symfony\Component\DependencyInjection\Tests\Fixtures\DependencyContainer; +use Symfony\Component\DependencyInjection\Tests\Fixtures\DependencyContainerInterface; use Symfony\Component\DependencyInjection\Tests\Fixtures\FooClassWithEnumAttribute; use Symfony\Component\DependencyInjection\Tests\Fixtures\FooUnitEnum; use Symfony\Component\DependencyInjection\Tests\Fixtures\FooWithAbstractArgument; @@ -1677,6 +1679,59 @@ public function testWitherWithStaticReturnType() $this->assertInstanceOf(Foo::class, $wither->foo); } + public function testCloningLazyGhostWithDependency() + { + $container = new ContainerBuilder(); + $container->register('dependency', \stdClass::class); + $container->register(DependencyContainer::class) + ->addArgument(new Reference('dependency')) + ->setLazy(true) + ->setPublic(true); + + $container->compile(); + $dumper = new PhpDumper($container); + $dump = $dumper->dump(['class' => 'Symfony_DI_PhpDumper_Service_CloningLazyGhostWithDependency']); + eval('?>'.$dump); + + $container = new \Symfony_DI_PhpDumper_Service_CloningLazyGhostWithDependency(); + + $bar = $container->get(DependencyContainer::class); + $this->assertInstanceOf(DependencyContainer::class, $bar); + + $first_clone = clone $bar; + $second_clone = clone $bar; + + $this->assertSame($first_clone->dependency, $second_clone->dependency); + } + + public function testCloningProxyWithDependency() + { + $container = new ContainerBuilder(); + $container->register('dependency', \stdClass::class); + $container->register(DependencyContainer::class) + ->addArgument(new Reference('dependency')) + ->setLazy(true) + ->addTag('proxy', [ + 'interface' => DependencyContainerInterface::class, + ]) + ->setPublic(true); + + $container->compile(); + $dumper = new PhpDumper($container); + $dump = $dumper->dump(['class' => 'Symfony_DI_PhpDumper_Service_CloningProxyWithDependency']); + eval('?>'.$dump); + + $container = new \Symfony_DI_PhpDumper_Service_CloningProxyWithDependency(); + + $bar = $container->get(DependencyContainer::class); + $this->assertInstanceOf(DependencyContainerInterface::class, $bar); + + $first_clone = clone $bar; + $second_clone = clone $bar; + + $this->assertSame($first_clone->getDependency(), $second_clone->getDependency()); + } + public function testCurrentFactoryInlining() { $container = new ContainerBuilder(); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/DependencyContainer.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/DependencyContainer.php new file mode 100644 index 0000000000000..5e222bdf060be --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/DependencyContainer.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Tests\Fixtures; + +class DependencyContainer implements DependencyContainerInterface +{ + public function __construct( + public mixed $dependency, + ) { + } + + public function getDependency(): mixed + { + return $this->dependency; + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/DependencyContainerInterface.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/DependencyContainerInterface.php new file mode 100644 index 0000000000000..ed109cad78dcd --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/DependencyContainerInterface.php @@ -0,0 +1,17 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Tests\Fixtures; + +interface DependencyContainerInterface +{ + public function getDependency(): mixed; +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/child.expected.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/child.expected.yml index 44dbbd571b788..97380f388ca2a 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/child.expected.yml +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/child.expected.yml @@ -11,7 +11,9 @@ services: - container.decorator: { id: bar, inner: b } file: file.php lazy: true - arguments: [!service { class: Class1 }] + arguments: ['@b'] + b: + class: Class1 bar: alias: foo public: true diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/from_callable.expected.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/from_callable.expected.yml index d4dbbbadd48bf..1ab1643af1b48 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/from_callable.expected.yml +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/from_callable.expected.yml @@ -8,5 +8,7 @@ services: class: stdClass public: true lazy: true - arguments: [[!service { class: stdClass }, do]] + arguments: [['@bar', do]] factory: [Closure, fromCallable] + bar: + class: stdClass diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/closure_proxy.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/closure_proxy.php index 2bef92604d3a9..eaf303c7d068c 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/closure_proxy.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/closure_proxy.php @@ -55,6 +55,6 @@ protected function createProxy($class, \Closure $factory) */ protected static function getClosureProxyService($container, $lazyLoad = true) { - return $container->services['closure_proxy'] = new class(fn () => new \Symfony\Component\DependencyInjection\Tests\Compiler\Foo()) extends \Symfony\Component\DependencyInjection\Argument\LazyClosure implements \Symfony\Component\DependencyInjection\Tests\Compiler\SingleMethodInterface { public function theMethod() { return $this->service->cloneFoo(...\func_get_args()); } }; + return $container->services['closure_proxy'] = new class(fn () => ($container->privates['foo'] ??= new \Symfony\Component\DependencyInjection\Tests\Compiler\Foo())) extends \Symfony\Component\DependencyInjection\Argument\LazyClosure implements \Symfony\Component\DependencyInjection\Tests\Compiler\SingleMethodInterface { public function theMethod() { return $this->service->cloneFoo(...\func_get_args()); } }; } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/lazy_closure.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/lazy_closure.php index 0af28f2650147..2bf27779df041 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/lazy_closure.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/lazy_closure.php @@ -57,7 +57,7 @@ protected function createProxy($class, \Closure $factory) */ protected static function getClosure1Service($container, $lazyLoad = true) { - return $container->services['closure1'] = (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(...); + return $container->services['closure1'] = (new class(fn () => ($container->privates['foo'] ??= 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(...); } /** @@ -67,6 +67,6 @@ protected static function getClosure1Service($container, $lazyLoad = true) */ protected static function getClosure2Service($container, $lazyLoad = true) { - return $container->services['closure2'] = (new class(fn () => new \Symfony\Component\DependencyInjection\Tests\Compiler\FooVoid()) extends \Symfony\Component\DependencyInjection\Argument\LazyClosure { public function __invoke(string $name): void { $this->service->__invoke(...\func_get_args()); } })->__invoke(...); + return $container->services['closure2'] = (new class(fn () => ($container->privates['foo_void'] ??= new \Symfony\Component\DependencyInjection\Tests\Compiler\FooVoid())) extends \Symfony\Component\DependencyInjection\Argument\LazyClosure { public function __invoke(string $name): void { $this->service->__invoke(...\func_get_args()); } })->__invoke(...); } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_almost_circular_private.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_almost_circular_private.php index 0a9c519c8e69c..0c234ac3934c3 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_almost_circular_private.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_almost_circular_private.php @@ -373,15 +373,13 @@ protected static function getManager2Service($container) */ protected static function getManager3Service($container, $lazyLoad = true) { - $a = ($container->services['listener3'] ?? self::getListener3Service($container)); + $a = ($container->privates['connection3'] ?? self::getConnection3Service($container)); if (isset($container->services['manager3'])) { return $container->services['manager3']; } - $b = new \stdClass(); - $b->listener = [$a]; - return $container->services['manager3'] = new \stdClass($b); + return $container->services['manager3'] = new \stdClass($a); } /** @@ -481,6 +479,34 @@ protected static function getBar6Service($container) return $container->privates['bar6'] = new \stdClass($a); } + /** + * Gets the private 'connection3' shared service. + * + * @return \stdClass + */ + protected static function getConnection3Service($container) + { + $container->privates['connection3'] = $instance = new \stdClass(); + + $instance->listener = [($container->services['listener3'] ?? self::getListener3Service($container))]; + + return $instance; + } + + /** + * Gets the private 'connection4' shared service. + * + * @return \stdClass + */ + protected static function getConnection4Service($container) + { + $container->privates['connection4'] = $instance = new \stdClass(); + + $instance->listener = [($container->services['listener4'] ?? self::getListener4Service($container))]; + + return $instance; + } + /** * Gets the private 'doctrine.listener' shared service. * @@ -572,13 +598,13 @@ protected static function getMailerInline_TransportFactory_AmazonService($contai */ protected static function getManager4Service($container, $lazyLoad = true) { - $a = new \stdClass(); + $a = ($container->privates['connection4'] ?? self::getConnection4Service($container)); - $container->privates['manager4'] = $instance = new \stdClass($a); - - $a->listener = [($container->services['listener4'] ?? self::getListener4Service($container))]; + if (isset($container->privates['manager4'])) { + return $container->privates['manager4']; + } - return $instance; + return $container->privates['manager4'] = new \stdClass($a); } /** diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_almost_circular_public.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_almost_circular_public.php index 2250e860264dc..ae283e556a0da 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_almost_circular_public.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_almost_circular_public.php @@ -259,7 +259,7 @@ protected static function getDispatcher2Service($container, $lazyLoad = true) { $container->services['dispatcher2'] = $instance = new \stdClass(); - $instance->subscriber2 = new \stdClass(($container->services['manager2'] ?? self::getManager2Service($container))); + $instance->subscriber2 = ($container->privates['subscriber2'] ?? self::getSubscriber2Service($container)); return $instance; } @@ -820,4 +820,20 @@ protected static function getManager4Service($container, $lazyLoad = true) return $container->privates['manager4'] = new \stdClass($a); } + + /** + * Gets the private 'subscriber2' shared service. + * + * @return \stdClass + */ + protected static function getSubscriber2Service($container) + { + $a = ($container->services['manager2'] ?? self::getManager2Service($container)); + + if (isset($container->privates['subscriber2'])) { + return $container->privates['subscriber2']; + } + + return $container->privates['subscriber2'] = new \stdClass($a); + } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_wither_lazy.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_wither_lazy.php index f52f226597625..d5c3738a62a0b 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_wither_lazy.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_wither_lazy.php @@ -61,7 +61,7 @@ protected static function getWitherService($container, $lazyLoad = true) $instance = new \Symfony\Component\DependencyInjection\Tests\Compiler\Wither(); - $a = new \Symfony\Component\DependencyInjection\Tests\Compiler\Foo(); + $a = ($container->privates['Symfony\\Component\\DependencyInjection\\Tests\\Compiler\\Foo'] ??= new \Symfony\Component\DependencyInjection\Tests\Compiler\Foo()); $instance = $instance->withFoo1($a); $instance = $instance->withFoo2($a);