feature #26499 [FrameworkBundle] Allow fetching private services from test clients (nicolas-grekas)

This PR was merged into the 4.1-dev branch.

Discussion
----------

[FrameworkBundle] Allow fetching private services from test clients

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | #25814
| License       | MIT
| Doc PR        | -

With this PR, `$client->getContainer()` returns a special container that gives access to private services as if they were public.

Tests derived from `WebTestCase` and `KernelTestCase` can access this special container by using the new `static::$container` property.

Commits
-------

a840809 [FrameworkBundle] Allow fetching private services from test clients
This commit is contained in:
Nicolas Grekas 2018-03-16 10:51:38 +01:00
commit 5cf0a2ea8f
10 changed files with 230 additions and 9 deletions

View File

@ -30,13 +30,15 @@ class Client extends BaseClient
private $hasPerformedRequest = false;
private $profiler = false;
private $reboot = true;
private $container;
/**
* {@inheritdoc}
*/
public function __construct(KernelInterface $kernel, array $server = array(), History $history = null, CookieJar $cookieJar = null)
public function __construct(KernelInterface $kernel, array $server = array(), History $history = null, CookieJar $cookieJar = null, ContainerInterface $container = null)
{
parent::__construct($kernel, $server, $history, $cookieJar);
$this->container = $container;
}
/**
@ -46,7 +48,7 @@ class Client extends BaseClient
*/
public function getContainer()
{
return $this->kernel->getContainer();
return $this->container ?? $this->kernel->getContainer();
}
/**

View File

@ -0,0 +1,39 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
/**
* @author Nicolas Grekas <p@tchwork.com>
*/
class TestServiceContainerRealRefPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
if (!$container->hasDefinition('test.service_container')) {
return;
}
$testContainer = $container->getDefinition('test.service_container');
$privateContainer = $container->getDefinition((string) $testContainer->getArgument(2));
$definitions = $container->getDefinitions();
foreach ($privateContainer->getArgument(0) as $id => $argument) {
if (isset($definitions[$target = (string) $argument->getValues()[0]])) {
$argument->setValues(array(new Reference($target)));
}
}
}
}

View File

@ -0,0 +1,56 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
/**
* @author Nicolas Grekas <p@tchwork.com>
*/
class TestServiceContainerWeakRefPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
if (!$container->hasDefinition('test.service_container')) {
return;
}
$privateServices = array();
$definitions = $container->getDefinitions();
foreach ($definitions as $id => $definition) {
if (!$definition->isPublic() && !$definition->getErrors() && !$definition->isAbstract()) {
$privateServices[$id] = new ServiceClosureArgument(new Reference($id, ContainerBuilder::IGNORE_ON_UNINITIALIZED_REFERENCE));
}
}
$aliases = $container->getAliases();
foreach ($aliases as $id => $alias) {
if (!$alias->isPublic()) {
while (isset($aliases[$target = (string) $alias])) {
$alias = $aliases[$target];
}
if (isset($definitions[$target]) && !$definitions[$target]->getErrors() && !$definitions[$target]->isAbstract()) {
$privateServices[$id] = new ServiceClosureArgument(new Reference($target, ContainerBuilder::IGNORE_ON_UNINITIALIZED_REFERENCE));
}
}
}
if ($privateServices) {
$definitions[(string) $definitions['test.service_container']->getArgument(2)]->replaceArgument(0, $privateServices);
}
}
}

View File

@ -23,6 +23,8 @@ use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\ProfilerPass;
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\LoggingTranslatorPass;
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AddExpressionLanguageProvidersPass;
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\ContainerBuilderDebugDumpPass;
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\TestServiceContainerWeakRefPass;
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\TestServiceContainerRealRefPass;
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\UnusedTagsPass;
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\WorkflowGuardListenerPass;
use Symfony\Component\Console\Application;
@ -114,6 +116,8 @@ class FrameworkBundle extends Bundle
$this->addCompilerPassIfExists($container, FormPass::class);
$container->addCompilerPass(new WorkflowGuardListenerPass());
$container->addCompilerPass(new ResettableServicePass());
$container->addCompilerPass(new TestServiceContainerWeakRefPass(), PassConfig::TYPE_BEFORE_REMOVING, -32);
$container->addCompilerPass(new TestServiceContainerRealRefPass(), PassConfig::TYPE_AFTER_REMOVING);
if ($container->getParameter('kernel.debug')) {
$container->addCompilerPass(new AddDebugLogProcessorPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -32);

View File

@ -16,6 +16,7 @@
<argument>%test.client.parameters%</argument>
<argument type="service" id="test.client.history" />
<argument type="service" id="test.client.cookiejar" />
<argument type="service" id="test.service_container" />
</service>
<service id="test.client.history" class="Symfony\Component\BrowserKit\History" shared="false" />
@ -33,5 +34,15 @@
</service>
</argument>
</service>
<service id="test.service_container" class="Symfony\Bundle\FrameworkBundle\Test\TestContainer" public="true">
<argument type="service" id="parameter_bag" on-invalid="null" />
<argument type="service" id="service_container" />
<argument type="service">
<service class="Symfony\Component\DependencyInjection\ServiceLocator">
<argument type="collection" />
</service>
</argument>
</service>
</services>
</container>

View File

@ -12,6 +12,7 @@
namespace Symfony\Bundle\FrameworkBundle\Test;
use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\ResettableContainerInterface;
use Symfony\Component\HttpKernel\KernelInterface;
@ -29,6 +30,11 @@ abstract class KernelTestCase extends TestCase
*/
protected static $kernel;
/**
* @var ContainerInterface
*/
protected static $container;
/**
* @return string The Kernel class name
*
@ -60,6 +66,9 @@ abstract class KernelTestCase extends TestCase
static::$kernel = static::createKernel($options);
static::$kernel->boot();
$container = static::$kernel->getContainer();
static::$container = $container->has('test.service_container') ? $container->get('test.service_container') : $container;
return static::$kernel;
}

View File

@ -0,0 +1,105 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bundle\FrameworkBundle\Test;
use Psr\Container\ContainerInterface as PsrContainerInterface;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\DependencyInjection\ContainerInterface as SymfonyContainerInterface;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
/**
* @author Nicolas Grekas <p@tchwork.com>
*/
class TestContainer extends Container
{
private $publicContainer;
private $privateContainer;
public function __construct(?ParameterBagInterface $parameterBag, SymfonyContainerInterface $publicContainer, PsrContainerInterface $privateContainer)
{
$this->parameterBag = $parameterBag ?? $publicContainer->getParameterBag();
$this->publicContainer = $publicContainer;
$this->privateContainer = $privateContainer;
}
/**
* {@inheritdoc}
*/
public function compile()
{
$this->publicContainer->compile();
}
/**
* {@inheritdoc}
*/
public function isCompiled()
{
return $this->publicContainer->isCompiled();
}
/**
* {@inheritdoc}
*/
public function set($id, $service)
{
$this->publicContainer->set($id, $service);
}
/**
* {@inheritdoc}
*/
public function has($id)
{
return $this->publicContainer->has($id) || $this->privateContainer->has($id);
}
/**
* {@inheritdoc}
*/
public function get($id, $invalidBehavior = /* self::EXCEPTION_ON_INVALID_REFERENCE */ 1)
{
return $this->privateContainer->has($id) ? $this->privateContainer->get($id) : $this->publicContainer->get($id, $invalidBehavior);
}
/**
* {@inheritdoc}
*/
public function initialized($id)
{
return $this->publicContainer->initialized($id);
}
/**
* {@inheritdoc}
*/
public function reset()
{
$this->publicContainer->reset();
}
/**
* {@inheritdoc}
*/
public function getServiceIds()
{
return $this->publicContainer->getServiceIds();
}
/**
* {@inheritdoc}
*/
public function getRemovedIds()
{
return $this->publicContainer->getRemovedIds();
}
}

View File

@ -18,9 +18,8 @@ class PropertyInfoTest extends WebTestCase
public function testPhpDocPriority()
{
static::bootKernel(array('test_case' => 'Serializer'));
$container = static::$kernel->getContainer();
$this->assertEquals(array(new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_INT))), $container->get('test.property_info')->getTypes('Symfony\Bundle\FrameworkBundle\Tests\Functional\Dummy', 'codes'));
$this->assertEquals(array(new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_INT))), static::$container->get('property_info')->getTypes('Symfony\Bundle\FrameworkBundle\Tests\Functional\Dummy', 'codes'));
}
}

View File

@ -1,10 +1,6 @@
imports:
- { resource: ../config/default.yml }
services:
_defaults: { public: true }
test.property_info: '@property_info'
framework:
serializer: { enabled: true }
property_info: { enabled: true }

View File

@ -4,7 +4,7 @@ framework:
validation: { enabled: true, enable_annotations: true }
csrf_protection: true
form: true
test: ~
test: true
default_locale: en
session:
storage_id: session.storage.mock_file