Skip to content

Commit

Permalink
feature #24155 [FrameworkBundle][HttpKernel] Add DI tag for resettabl…
Browse files Browse the repository at this point in the history
…e services (derrabus)

This PR was merged into the 3.4 branch.

Discussion
----------

[FrameworkBundle][HttpKernel] Add DI tag for resettable services

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

This PR uses #24033 to introduce a DI tag for resettable services.

TODO after merge:
* Add an interface, make the "method" attribute optional and enable autoconfiguration.
* Consider adding a config option to enable/disable this feature.
* Configure leaking services of the core bundles as resettable.

Commits
-------

d9a6b76 A DI tag for resettable services.
  • Loading branch information
fabpot committed Sep 14, 2017
2 parents 30e3b6d + d9a6b76 commit 70bfb50
Show file tree
Hide file tree
Showing 8 changed files with 314 additions and 0 deletions.
2 changes: 2 additions & 0 deletions src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php
Expand Up @@ -33,6 +33,7 @@
use Symfony\Component\HttpKernel\DependencyInjection\ControllerArgumentValueResolverPass;
use Symfony\Component\HttpKernel\DependencyInjection\RegisterControllerArgumentLocatorsPass;
use Symfony\Component\HttpKernel\DependencyInjection\RemoveEmptyControllerArgumentLocatorsPass;
use Symfony\Component\HttpKernel\DependencyInjection\ResettableServicePass;
use Symfony\Component\PropertyInfo\DependencyInjection\PropertyInfoPass;
use Symfony\Component\Routing\DependencyInjection\RoutingResolverPass;
use Symfony\Component\Serializer\DependencyInjection\SerializerPass;
Expand Down Expand Up @@ -117,6 +118,7 @@ public function build(ContainerBuilder $container)
$container->addCompilerPass(new CachePoolPrunerPass(), PassConfig::TYPE_AFTER_REMOVING);
$this->addCompilerPassIfExists($container, FormPass::class);
$container->addCompilerPass(new WorkflowGuardListenerPass());
$container->addCompilerPass(new ResettableServicePass());

if ($container->getParameter('kernel.debug')) {
$container->addCompilerPass(new AddDebugLogProcessorPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -32);
Expand Down
Expand Up @@ -74,5 +74,11 @@
<service id="Symfony\Component\Config\Resource\SelfCheckingResourceChecker">
<tag name="config_cache.resource_checker" priority="-990" />
</service>

<service id="Symfony\Component\HttpKernel\EventListener\ServiceResetListener">
<argument /> <!-- ResettableServicePass will inject an iterator of initialized services here ($serviceId => $serviceInstance) -->
<argument type="collection" /> <!-- ResettableServicePass will inject an array of reset methods here ($serviceId => $method) -->
<tag name="kernel.event_subscriber" />
</service>
</services>
</container>
@@ -0,0 +1,69 @@
<?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\Component\HttpKernel\DependencyInjection;

use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\HttpKernel\EventListener\ServiceResetListener;

/**
* @author Alexander M. Turek <me@derrabus.de>
*/
class ResettableServicePass implements CompilerPassInterface
{
private $tagName;

/**
* @param string $tagName
*/
public function __construct($tagName = 'kernel.reset')
{
$this->tagName = $tagName;
}

/**
* {@inheritdoc}
*/
public function process(ContainerBuilder $container)
{
if (!$container->has(ServiceResetListener::class)) {
return;
}

$services = $methods = array();

foreach ($container->findTaggedServiceIds($this->tagName, true) as $id => $tags) {
$services[$id] = new Reference($id, ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE);
$attributes = $tags[0];

if (!isset($attributes['method'])) {
throw new RuntimeException(sprintf('Tag %s requires the "method" attribute to be set.', $this->tagName));
}

$methods[$id] = $attributes['method'];
}

if (empty($services)) {
$container->removeDefinition(ServiceResetListener::class);

return;
}

$container->findDefinition(ServiceResetListener::class)
->replaceArgument(0, new IteratorArgument($services))
->replaceArgument(1, $methods);
}
}
@@ -0,0 +1,50 @@
<?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\Component\HttpKernel\EventListener;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\KernelEvents;

/**
* Clean up services between requests.
*
* @author Alexander M. Turek <me@derrabus.de>
*/
class ServiceResetListener implements EventSubscriberInterface
{
private $services;
private $resetMethods;

public function __construct(\Traversable $services, array $resetMethods)
{
$this->services = $services;
$this->resetMethods = $resetMethods;
}

public function onKernelTerminate()
{
foreach ($this->services as $id => $service) {
$method = $this->resetMethods[$id];
$service->$method();
}
}

/**
* {@inheritdoc}
*/
public static function getSubscribedEvents()
{
return array(
KernelEvents::TERMINATE => array('onKernelTerminate', -2048),
);
}
}
@@ -0,0 +1,85 @@
<?php

namespace Symfony\Component\HttpKernel\Tests\DependencyInjection;

use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\HttpKernel\DependencyInjection\ResettableServicePass;
use Symfony\Component\HttpKernel\EventListener\ServiceResetListener;
use Symfony\Component\HttpKernel\Tests\Fixtures\ClearableService;
use Symfony\Component\HttpKernel\Tests\Fixtures\ResettableService;

class ResettableServicePassTest extends TestCase
{
public function testCompilerPass()
{
$container = new ContainerBuilder();
$container->register('one', ResettableService::class)
->addTag('kernel.reset', array('method' => 'reset'));
$container->register('two', ClearableService::class)
->addTag('kernel.reset', array('method' => 'clear'));

$container->register(ServiceResetListener::class)
->setArguments(array(null, array()));
$container->addCompilerPass(new ResettableServicePass('kernel.reset'));

$container->compile();

$definition = $container->getDefinition(ServiceResetListener::class);

$this->assertEquals(
array(
new IteratorArgument(array(
'one' => new Reference('one', ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE),
'two' => new Reference('two', ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE),
)),
array(
'one' => 'reset',
'two' => 'clear',
),
),
$definition->getArguments()
);
}

/**
* @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException
* @expectedExceptionMessage Tag kernel.reset requires the "method" attribute to be set.
*/
public function testMissingMethod()
{
$container = new ContainerBuilder();
$container->register(ResettableService::class)
->addTag('kernel.reset');
$container->register(ServiceResetListener::class)
->setArguments(array(null, array()));
$container->addCompilerPass(new ResettableServicePass('kernel.reset'));

$container->compile();
}

public function testCompilerPassWithoutResetters()
{
$container = new ContainerBuilder();
$container->register(ServiceResetListener::class)
->setArguments(array(null, array()));
$container->addCompilerPass(new ResettableServicePass());

$container->compile();

$this->assertFalse($container->has(ServiceResetListener::class));
}

public function testCompilerPassWithoutListener()
{
$container = new ContainerBuilder();
$container->addCompilerPass(new ResettableServicePass());

$container->compile();

$this->assertFalse($container->has(ServiceResetListener::class));
}
}
@@ -0,0 +1,76 @@
<?php

namespace Symfony\Component\HttpKernel\Tests\EventListener;

use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\HttpKernel\EventListener\ServiceResetListener;
use Symfony\Component\HttpKernel\Tests\Fixtures\ClearableService;
use Symfony\Component\HttpKernel\Tests\Fixtures\ResettableService;

class ServiceResetListenerTest extends TestCase
{
protected function setUp()
{
ResettableService::$counter = 0;
ClearableService::$counter = 0;
}

public function testResetServicesNoOp()
{
$container = $this->buildContainer();
$container->get('reset_subscriber')->onKernelTerminate();

$this->assertEquals(0, ResettableService::$counter);
$this->assertEquals(0, ClearableService::$counter);
}

public function testResetServicesPartially()
{
$container = $this->buildContainer();
$container->get('one');
$container->get('reset_subscriber')->onKernelTerminate();

$this->assertEquals(1, ResettableService::$counter);
$this->assertEquals(0, ClearableService::$counter);
}

public function testResetServicesTwice()
{
$container = $this->buildContainer();
$container->get('one');
$container->get('reset_subscriber')->onKernelTerminate();
$container->get('two');
$container->get('reset_subscriber')->onKernelTerminate();

$this->assertEquals(2, ResettableService::$counter);
$this->assertEquals(1, ClearableService::$counter);
}

/**
* @return ContainerBuilder
*/
private function buildContainer()
{
$container = new ContainerBuilder();
$container->register('one', ResettableService::class);
$container->register('two', ClearableService::class);

$container->register('reset_subscriber', ServiceResetListener::class)
->addArgument(new IteratorArgument(array(
'one' => new Reference('one', ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE),
'two' => new Reference('two', ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE),
)))
->addArgument(array(
'one' => 'reset',
'two' => 'clear',
));

$container->compile();

return $container;
}
}
@@ -0,0 +1,13 @@
<?php

namespace Symfony\Component\HttpKernel\Tests\Fixtures;

class ClearableService
{
public static $counter = 0;

public function clear()
{
++self::$counter;
}
}
@@ -0,0 +1,13 @@
<?php

namespace Symfony\Component\HttpKernel\Tests\Fixtures;

class ResettableService
{
public static $counter = 0;

public function reset()
{
++self::$counter;
}
}

0 comments on commit 70bfb50

Please sign in to comment.