diff --git a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php
index ba117ceeb82a2..c6936d35be83d 100644
--- a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php
+++ b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php
@@ -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;
@@ -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);
diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.xml
index 9ff2c259ee43b..0bae93663bf5c 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.xml
+++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.xml
@@ -74,5 +74,11 @@
+
+
+
+
+
+
diff --git a/src/Symfony/Component/HttpKernel/DependencyInjection/ResettableServicePass.php b/src/Symfony/Component/HttpKernel/DependencyInjection/ResettableServicePass.php
new file mode 100644
index 0000000000000..56cd059284afe
--- /dev/null
+++ b/src/Symfony/Component/HttpKernel/DependencyInjection/ResettableServicePass.php
@@ -0,0 +1,69 @@
+
+ *
+ * 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
+ */
+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);
+ }
+}
diff --git a/src/Symfony/Component/HttpKernel/EventListener/ServiceResetListener.php b/src/Symfony/Component/HttpKernel/EventListener/ServiceResetListener.php
new file mode 100644
index 0000000000000..cf6d15930315f
--- /dev/null
+++ b/src/Symfony/Component/HttpKernel/EventListener/ServiceResetListener.php
@@ -0,0 +1,50 @@
+
+ *
+ * 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
+ */
+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),
+ );
+ }
+}
diff --git a/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/ResettableServicePassTest.php b/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/ResettableServicePassTest.php
new file mode 100644
index 0000000000000..7363586f99588
--- /dev/null
+++ b/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/ResettableServicePassTest.php
@@ -0,0 +1,85 @@
+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));
+ }
+}
diff --git a/src/Symfony/Component/HttpKernel/Tests/EventListener/ServiceResetListenerTest.php b/src/Symfony/Component/HttpKernel/Tests/EventListener/ServiceResetListenerTest.php
new file mode 100644
index 0000000000000..4675fbe71e5a2
--- /dev/null
+++ b/src/Symfony/Component/HttpKernel/Tests/EventListener/ServiceResetListenerTest.php
@@ -0,0 +1,76 @@
+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;
+ }
+}
diff --git a/src/Symfony/Component/HttpKernel/Tests/Fixtures/ClearableService.php b/src/Symfony/Component/HttpKernel/Tests/Fixtures/ClearableService.php
new file mode 100644
index 0000000000000..35acb419ce3e5
--- /dev/null
+++ b/src/Symfony/Component/HttpKernel/Tests/Fixtures/ClearableService.php
@@ -0,0 +1,13 @@
+