Skip to content

Commit

Permalink
feature #26499 [FrameworkBundle] Allow fetching private services from…
Browse files Browse the repository at this point in the history
… 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
  • Loading branch information
nicolas-grekas committed Mar 16, 2018
2 parents e317879 + a840809 commit 5cf0a2e
Show file tree
Hide file tree
Showing 10 changed files with 230 additions and 9 deletions.
6 changes: 4 additions & 2 deletions src/Symfony/Bundle/FrameworkBundle/Client.php
Expand Up @@ -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;
}

/**
Expand All @@ -46,7 +48,7 @@ public function __construct(KernelInterface $kernel, array $server = array(), Hi
*/
public function getContainer()
{
return $this->kernel->getContainer();
return $this->container ?? $this->kernel->getContainer();
}

/**
Expand Down
@@ -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)));
}
}
}
}
@@ -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);
}
}
}
4 changes: 4 additions & 0 deletions src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php
Expand Up @@ -23,6 +23,8 @@
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;
Expand Down Expand Up @@ -114,6 +116,8 @@ public function build(ContainerBuilder $container)
$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);
Expand Down
11 changes: 11 additions & 0 deletions src/Symfony/Bundle/FrameworkBundle/Resources/config/test.xml
Expand Up @@ -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" />
Expand All @@ -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>
9 changes: 9 additions & 0 deletions src/Symfony/Bundle/FrameworkBundle/Test/KernelTestCase.php
Expand Up @@ -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;

Expand All @@ -29,6 +30,11 @@ abstract class KernelTestCase extends TestCase
*/
protected static $kernel;

/**
* @var ContainerInterface
*/
protected static $container;

/**
* @return string The Kernel class name
*
Expand Down Expand Up @@ -60,6 +66,9 @@ protected static function bootKernel(array $options = array())
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;
}

Expand Down
105 changes: 105 additions & 0 deletions src/Symfony/Bundle/FrameworkBundle/Test/TestContainer.php
@@ -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();
}
}
Expand Up @@ -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'));
}
}

Expand Down
@@ -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 }
Expand Up @@ -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
Expand Down

0 comments on commit 5cf0a2e

Please sign in to comment.