Skip to content

Commit

Permalink
[FrameworkBundle] Allow setting private services with the test container
Browse files Browse the repository at this point in the history
  • Loading branch information
nicolas-grekas committed Jan 11, 2023
1 parent e36776d commit f38b1db
Show file tree
Hide file tree
Showing 5 changed files with 52 additions and 18 deletions.
1 change: 1 addition & 0 deletions src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
---
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
}
Expand All @@ -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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -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);
}
}
Expand All @@ -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);
}
}
Expand Down
36 changes: 22 additions & 14 deletions src/Symfony/Bundle/FrameworkBundle/Test/TestContainer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -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()
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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());
}
}

0 comments on commit f38b1db

Please sign in to comment.