Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 70bfb50

Browse filesBrowse files
committed
feature #24155 [FrameworkBundle][HttpKernel] Add DI tag for resettable 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.
2 parents 30e3b6d + d9a6b76 commit 70bfb50
Copy full SHA for 70bfb50

File tree

8 files changed

+314
-0
lines changed
Filter options

8 files changed

+314
-0
lines changed

‎src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php
+2Lines changed: 2 additions & 0 deletions
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

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/Resources/config/services.xml
+6Lines changed: 6 additions & 0 deletions
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>
+69Lines changed: 69 additions & 0 deletions
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+
}
+50Lines changed: 50 additions & 0 deletions
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+
}
+85Lines changed: 85 additions & 0 deletions
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+
}
+76Lines changed: 76 additions & 0 deletions
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+
protected 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+
}
+13Lines changed: 13 additions & 0 deletions
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+
}
+13Lines changed: 13 additions & 0 deletions
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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.