From 30cb03f6532bfceab7d858fca0f3e13542fd27ad Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 10 Jan 2023 11:12:25 +0100 Subject: [PATCH] [FrameworkBundle] Allow setting private services with the test container --- .../Bundle/FrameworkBundle/CHANGELOG.md | 1 + .../TestServiceContainerRealRefPass.php | 10 ++++++ .../TestServiceContainerWeakRefPass.php | 6 ++-- .../FrameworkBundle/Test/TestContainer.php | 36 +++++++++++-------- .../Tests/Functional/KernelTestCaseTest.php | 17 +++++++++ 5 files changed, 52 insertions(+), 18 deletions(-) diff --git a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md index 0fc2874be042b..3179f5ab526c1 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md @@ -11,6 +11,7 @@ CHANGELOG * Add support to pass namespace wildcard in `framework.messenger.routing` * Deprecate `framework:exceptions` tag, unwrap it and replace `framework:exception` tags' `name` attribute by `class` * Deprecate the `notifier.logger_notification_listener` service, use the `notifier.notification_logger_listener` service instead + * Allow setting private services with the test container 6.2 --- diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/TestServiceContainerRealRefPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/TestServiceContainerRealRefPass.php index 942eb635b26f3..8d54563c2c7d0 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/TestServiceContainerRealRefPass.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/TestServiceContainerRealRefPass.php @@ -30,10 +30,14 @@ public function process(ContainerBuilder $container) $privateContainer = $container->getDefinition('test.private_services_locator'); $definitions = $container->getDefinitions(); $privateServices = $privateContainer->getArgument(0); + $renamedIds = []; foreach ($privateServices as $id => $argument) { if (isset($definitions[$target = (string) $argument->getValues()[0]])) { $argument->setValues([new Reference($target)]); + if ($id !== $target) { + $renamedIds[$id] = $target; + } } else { unset($privateServices[$id]); } @@ -47,8 +51,14 @@ public function process(ContainerBuilder $container) if ($definitions[$target]->hasTag('container.private')) { $privateServices[$id] = new ServiceClosureArgument(new Reference($target)); } + + $renamedIds[$id] = $target; } $privateContainer->replaceArgument(0, $privateServices); + + if ($container->hasDefinition('test.service_container') && $renamedIds) { + $container->getDefinition('test.service_container')->setArgument(2, $renamedIds); + } } } diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/TestServiceContainerWeakRefPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/TestServiceContainerWeakRefPass.php index a68f94f7b6134..45166842b5e0d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/TestServiceContainerWeakRefPass.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/TestServiceContainerWeakRefPass.php @@ -14,7 +14,6 @@ use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass; use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; /** @@ -30,10 +29,9 @@ public function process(ContainerBuilder $container) $privateServices = []; $definitions = $container->getDefinitions(); - $hasErrors = method_exists(Definition::class, 'hasErrors') ? 'hasErrors' : 'getErrors'; foreach ($definitions as $id => $definition) { - if ($id && '.' !== $id[0] && (!$definition->isPublic() || $definition->isPrivate() || $definition->hasTag('container.private')) && !$definition->$hasErrors() && !$definition->isAbstract()) { + if ($id && '.' !== $id[0] && (!$definition->isPublic() || $definition->isPrivate() || $definition->hasTag('container.private')) && !$definition->hasErrors() && !$definition->isAbstract()) { $privateServices[$id] = new Reference($id, ContainerBuilder::IGNORE_ON_UNINITIALIZED_REFERENCE); } } @@ -45,7 +43,7 @@ public function process(ContainerBuilder $container) while (isset($aliases[$target = (string) $alias])) { $alias = $aliases[$target]; } - if (isset($definitions[$target]) && !$definitions[$target]->$hasErrors() && !$definitions[$target]->isAbstract()) { + if (isset($definitions[$target]) && !$definitions[$target]->hasErrors() && !$definitions[$target]->isAbstract()) { $privateServices[$id] = new Reference($target, ContainerBuilder::IGNORE_ON_UNINITIALIZED_REFERENCE); } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Test/TestContainer.php b/src/Symfony/Bundle/FrameworkBundle/Test/TestContainer.php index 883928087ea03..933fb6de47a83 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Test/TestContainer.php +++ b/src/Symfony/Bundle/FrameworkBundle/Test/TestContainer.php @@ -13,12 +13,13 @@ use Psr\Container\ContainerInterface; use Symfony\Component\DependencyInjection\Container; +use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; use Symfony\Component\HttpKernel\KernelInterface; /** * A special container used in tests. This gives access to both public and - * private services. The container will not include private services that has + * private services. The container will not include private services that have * been inlined or removed. Private services will be removed when they are not * used by other services. * @@ -28,13 +29,11 @@ */ class TestContainer extends Container { - private KernelInterface $kernel; - private string $privateServicesLocatorId; - - public function __construct(KernelInterface $kernel, string $privateServicesLocatorId) - { - $this->kernel = $kernel; - $this->privateServicesLocatorId = $privateServicesLocatorId; + public function __construct( + private KernelInterface $kernel, + private string $privateServicesLocatorId, + private array $renamedIds = [], + ) { } public function compile() @@ -69,7 +68,20 @@ public function setParameter(string $name, mixed $value) public function set(string $id, mixed $service) { - $this->getPublicContainer()->set($id, $service); + $container = $this->getPublicContainer(); + $renamedId = $this->renamedIds[$id] ?? $id; + + try { + $container->set($renamedId, $service); + } catch (InvalidArgumentException $e) { + if (!str_starts_with($e->getMessage(), "The \"$renamedId\" service is private")) { + throw $e; + } + if (isset($container->privates[$renamedId])) { + throw new InvalidArgumentException(sprintf('The "%s" service is already initialized, you cannot replace it.', $id)); + } + $container->privates[$renamedId] = $service; + } } public function has(string $id): bool @@ -104,11 +116,7 @@ public function getRemovedIds(): array private function getPublicContainer(): Container { - if (null === $container = $this->kernel->getContainer()) { - throw new \LogicException('Cannot access the container on a non-booted kernel. Did you forget to boot it?'); - } - - return $container; + return $this->kernel->getContainer() ?? throw new \LogicException('Cannot access the container on a non-booted kernel. Did you forget to boot it?'); } private function getPrivateContainer(): ContainerInterface diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/KernelTestCaseTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/KernelTestCaseTest.php index 32bee3b587309..aa12467829ed1 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/KernelTestCaseTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/KernelTestCaseTest.php @@ -17,6 +17,7 @@ use Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\TestServiceContainer\PublicService; use Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\TestServiceContainer\UnusedPrivateService; use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; class KernelTestCaseTest extends AbstractWebTestCase { @@ -41,4 +42,20 @@ public function testThatPrivateServicesAreAvailableIfTestConfigIsEnabled() $this->assertTrue($container->has('private_service')); $this->assertFalse($container->has(UnusedPrivateService::class)); } + + public function testThatPrivateServicesCanBeSetIfTestConfigIsEnabled() + { + static::bootKernel(['test_case' => 'TestServiceContainer']); + + $container = static::getContainer(); + + $service = new \stdClass(); + + $container->set('private_service', $service); + $this->assertSame($service, $container->get('private_service')); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The "private_service" service is already initialized, you cannot replace it.'); + $container->set('private_service', new \stdClass()); + } }