Skip to content

Commit 8499bca

Browse files
committed
A DI tag for resettable services.
1 parent 30e3b6d commit 8499bca

File tree

8 files changed

+314
-0
lines changed

8 files changed

+314
-0
lines changed

src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php

+2
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
use Symfony\Component\HttpKernel\DependencyInjection\ControllerArgumentValueResolverPass;
3434
use Symfony\Component\HttpKernel\DependencyInjection\RegisterControllerArgumentLocatorsPass;
3535
use Symfony\Component\HttpKernel\DependencyInjection\RemoveEmptyControllerArgumentLocatorsPass;
36+
use Symfony\Component\HttpKernel\DependencyInjection\ResettableServicePass;
3637
use Symfony\Component\PropertyInfo\DependencyInjection\PropertyInfoPass;
3738
use Symfony\Component\Routing\DependencyInjection\RoutingResolverPass;
3839
use Symfony\Component\Serializer\DependencyInjection\SerializerPass;
@@ -117,6 +118,7 @@ public function build(ContainerBuilder $container)
117118
$container->addCompilerPass(new CachePoolPrunerPass(), PassConfig::TYPE_AFTER_REMOVING);
118119
$this->addCompilerPassIfExists($container, FormPass::class);
119120
$container->addCompilerPass(new WorkflowGuardListenerPass());
121+
$container->addCompilerPass(new ResettableServicePass());
120122

121123
if ($container->getParameter('kernel.debug')) {
122124
$container->addCompilerPass(new AddDebugLogProcessorPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -32);

src/Symfony/Bundle/FrameworkBundle/Resources/config/services.xml

+6
Original file line numberDiff line numberDiff line change
@@ -74,5 +74,11 @@
7474
<service id="Symfony\Component\Config\Resource\SelfCheckingResourceChecker">
7575
<tag name="config_cache.resource_checker" priority="-990" />
7676
</service>
77+
78+
<service id="Symfony\Component\HttpKernel\EventListener\ServiceResetListener">
79+
<argument /> <!-- ResettableServicePass will inject an iterator of initialized services here ($serviceId => $serviceInstance) -->
80+
<argument type="collection" /> <!-- ResettableServicePass will inject an array of reset methods here ($serviceId => $method) -->
81+
<tag name="kernel.event_subscriber" />
82+
</service>
7783
</services>
7884
</container>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpKernel\DependencyInjection;
13+
14+
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
15+
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
16+
use Symfony\Component\DependencyInjection\ContainerBuilder;
17+
use Symfony\Component\DependencyInjection\ContainerInterface;
18+
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
19+
use Symfony\Component\DependencyInjection\Reference;
20+
use Symfony\Component\HttpKernel\EventListener\ServiceResetListener;
21+
22+
/**
23+
* @author Alexander M. Turek <me@derrabus.de>
24+
*/
25+
class ResettableServicePass implements CompilerPassInterface
26+
{
27+
private $tagName;
28+
29+
/**
30+
* @param string $tagName
31+
*/
32+
public function __construct($tagName = 'kernel.reset')
33+
{
34+
$this->tagName = $tagName;
35+
}
36+
37+
/**
38+
* {@inheritdoc}
39+
*/
40+
public function process(ContainerBuilder $container)
41+
{
42+
if (!$container->has(ServiceResetListener::class)) {
43+
return;
44+
}
45+
46+
$services = $methods = array();
47+
48+
foreach ($container->findTaggedServiceIds($this->tagName, true) as $id => $tags) {
49+
$services[$id] = new Reference($id, ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE);
50+
$attributes = $tags[0];
51+
52+
if (!isset($attributes['method'])) {
53+
throw new RuntimeException(sprintf('Tag %s requires the "method" attribute to be set.', $this->tagName));
54+
}
55+
56+
$methods[$id] = $attributes['method'];
57+
}
58+
59+
if (empty($services)) {
60+
$container->removeDefinition(ServiceResetListener::class);
61+
62+
return;
63+
}
64+
65+
$container->findDefinition(ServiceResetListener::class)
66+
->replaceArgument(0, new IteratorArgument($services))
67+
->replaceArgument(1, $methods);
68+
}
69+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpKernel\EventListener;
13+
14+
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
15+
use Symfony\Component\HttpKernel\KernelEvents;
16+
17+
/**
18+
* Clean up services between requests.
19+
*
20+
* @author Alexander M. Turek <me@derrabus.de>
21+
*/
22+
class ServiceResetListener implements EventSubscriberInterface
23+
{
24+
private $services;
25+
private $resetMethods;
26+
27+
public function __construct(\Traversable $services, array $resetMethods)
28+
{
29+
$this->services = $services;
30+
$this->resetMethods = $resetMethods;
31+
}
32+
33+
public function onKernelTerminate()
34+
{
35+
foreach ($this->services as $id => $service) {
36+
$method = $this->resetMethods[$id];
37+
$service->$method();
38+
}
39+
}
40+
41+
/**
42+
* {@inheritdoc}
43+
*/
44+
public static function getSubscribedEvents()
45+
{
46+
return array(
47+
KernelEvents::TERMINATE => array('onKernelTerminate', -2048),
48+
);
49+
}
50+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<?php
2+
3+
namespace Symfony\Component\HttpKernel\Tests\DependencyInjection;
4+
5+
use PHPUnit\Framework\TestCase;
6+
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
7+
use Symfony\Component\DependencyInjection\ContainerBuilder;
8+
use Symfony\Component\DependencyInjection\ContainerInterface;
9+
use Symfony\Component\DependencyInjection\Reference;
10+
use Symfony\Component\HttpKernel\DependencyInjection\ResettableServicePass;
11+
use Symfony\Component\HttpKernel\EventListener\ServiceResetListener;
12+
use Symfony\Component\HttpKernel\Tests\Fixtures\ClearableService;
13+
use Symfony\Component\HttpKernel\Tests\Fixtures\ResettableService;
14+
15+
class ResettableServicePassTest extends TestCase
16+
{
17+
public function testCompilerPass()
18+
{
19+
$container = new ContainerBuilder();
20+
$container->register('one', ResettableService::class)
21+
->addTag('kernel.reset', array('method' => 'reset'));
22+
$container->register('two', ClearableService::class)
23+
->addTag('kernel.reset', array('method' => 'clear'));
24+
25+
$container->register(ServiceResetListener::class)
26+
->setArguments(array(null, array()));
27+
$container->addCompilerPass(new ResettableServicePass('kernel.reset'));
28+
29+
$container->compile();
30+
31+
$definition = $container->getDefinition(ServiceResetListener::class);
32+
33+
$this->assertEquals(
34+
array(
35+
new IteratorArgument(array(
36+
'one' => new Reference('one', ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE),
37+
'two' => new Reference('two', ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE),
38+
)),
39+
array(
40+
'one' => 'reset',
41+
'two' => 'clear',
42+
),
43+
),
44+
$definition->getArguments()
45+
);
46+
}
47+
48+
/**
49+
* @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException
50+
* @expectedExceptionMessage Tag kernel.reset requires the "method" attribute to be set.
51+
*/
52+
public function testMissingMethod()
53+
{
54+
$container = new ContainerBuilder();
55+
$container->register(ResettableService::class)
56+
->addTag('kernel.reset');
57+
$container->register(ServiceResetListener::class)
58+
->setArguments(array(null, array()));
59+
$container->addCompilerPass(new ResettableServicePass('kernel.reset'));
60+
61+
$container->compile();
62+
}
63+
64+
public function testCompilerPassWithoutResetters()
65+
{
66+
$container = new ContainerBuilder();
67+
$container->register(ServiceResetListener::class)
68+
->setArguments(array(null, array()));
69+
$container->addCompilerPass(new ResettableServicePass());
70+
71+
$container->compile();
72+
73+
$this->assertFalse($container->has(ServiceResetListener::class));
74+
}
75+
76+
public function testCompilerPassWithoutListener()
77+
{
78+
$container = new ContainerBuilder();
79+
$container->addCompilerPass(new ResettableServicePass());
80+
81+
$container->compile();
82+
83+
$this->assertFalse($container->has(ServiceResetListener::class));
84+
}
85+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<?php
2+
3+
namespace Symfony\Component\HttpKernel\Tests\EventListener;
4+
5+
use PHPUnit\Framework\TestCase;
6+
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
7+
use Symfony\Component\DependencyInjection\ContainerBuilder;
8+
use Symfony\Component\DependencyInjection\ContainerInterface;
9+
use Symfony\Component\DependencyInjection\Reference;
10+
use Symfony\Component\HttpKernel\EventListener\ServiceResetListener;
11+
use Symfony\Component\HttpKernel\Tests\Fixtures\ClearableService;
12+
use Symfony\Component\HttpKernel\Tests\Fixtures\ResettableService;
13+
14+
class ServiceResetListenerTest extends TestCase
15+
{
16+
public function setUp()
17+
{
18+
ResettableService::$counter = 0;
19+
ClearableService::$counter = 0;
20+
}
21+
22+
public function testResetServicesNoOp()
23+
{
24+
$container = $this->buildContainer();
25+
$container->get('reset_subscriber')->onKernelTerminate();
26+
27+
$this->assertEquals(0, ResettableService::$counter);
28+
$this->assertEquals(0, ClearableService::$counter);
29+
}
30+
31+
public function testResetServicesPartially()
32+
{
33+
$container = $this->buildContainer();
34+
$container->get('one');
35+
$container->get('reset_subscriber')->onKernelTerminate();
36+
37+
$this->assertEquals(1, ResettableService::$counter);
38+
$this->assertEquals(0, ClearableService::$counter);
39+
}
40+
41+
public function testResetServicesTwice()
42+
{
43+
$container = $this->buildContainer();
44+
$container->get('one');
45+
$container->get('reset_subscriber')->onKernelTerminate();
46+
$container->get('two');
47+
$container->get('reset_subscriber')->onKernelTerminate();
48+
49+
$this->assertEquals(2, ResettableService::$counter);
50+
$this->assertEquals(1, ClearableService::$counter);
51+
}
52+
53+
/**
54+
* @return ContainerBuilder
55+
*/
56+
private function buildContainer()
57+
{
58+
$container = new ContainerBuilder();
59+
$container->register('one', ResettableService::class);
60+
$container->register('two', ClearableService::class);
61+
62+
$container->register('reset_subscriber', ServiceResetListener::class)
63+
->addArgument(new IteratorArgument(array(
64+
'one' => new Reference('one', ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE),
65+
'two' => new Reference('two', ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE),
66+
)))
67+
->addArgument(array(
68+
'one' => 'reset',
69+
'two' => 'clear',
70+
));
71+
72+
$container->compile();
73+
74+
return $container;
75+
}
76+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace Symfony\Component\HttpKernel\Tests\Fixtures;
4+
5+
class ClearableService
6+
{
7+
public static $counter = 0;
8+
9+
public function clear()
10+
{
11+
++self::$counter;
12+
}
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace Symfony\Component\HttpKernel\Tests\Fixtures;
4+
5+
class ResettableService
6+
{
7+
public static $counter = 0;
8+
9+
public function reset()
10+
{
11+
++self::$counter;
12+
}
13+
}

0 commit comments

Comments
 (0)