From d9a6b76dbdff123d0c99261b785cdb15c2fbe97e Mon Sep 17 00:00:00 2001 From: "Alexander M. Turek" Date: Mon, 11 Sep 2017 16:29:04 +0200 Subject: [PATCH] A DI tag for resettable services. --- .../FrameworkBundle/FrameworkBundle.php | 2 + .../Resources/config/services.xml | 6 ++ .../ResettableServicePass.php | 69 +++++++++++++++ .../EventListener/ServiceResetListener.php | 50 +++++++++++ .../ResettableServicePassTest.php | 85 +++++++++++++++++++ .../ServiceResetListenerTest.php | 76 +++++++++++++++++ .../Tests/Fixtures/ClearableService.php | 13 +++ .../Tests/Fixtures/ResettableService.php | 13 +++ 8 files changed, 314 insertions(+) create mode 100644 src/Symfony/Component/HttpKernel/DependencyInjection/ResettableServicePass.php create mode 100644 src/Symfony/Component/HttpKernel/EventListener/ServiceResetListener.php create mode 100644 src/Symfony/Component/HttpKernel/Tests/DependencyInjection/ResettableServicePassTest.php create mode 100644 src/Symfony/Component/HttpKernel/Tests/EventListener/ServiceResetListenerTest.php create mode 100644 src/Symfony/Component/HttpKernel/Tests/Fixtures/ClearableService.php create mode 100644 src/Symfony/Component/HttpKernel/Tests/Fixtures/ResettableService.php 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 @@ +