diff --git a/core/lib/Drupal/Core/CoreBundle.php b/core/lib/Drupal/Core/CoreBundle.php index 871b859a47edf8d967bc77bac825d004f78eb5a3..88b25bf26903555aa3b1fd16484d171a0292ee94 100644 --- a/core/lib/Drupal/Core/CoreBundle.php +++ b/core/lib/Drupal/Core/CoreBundle.php @@ -14,6 +14,7 @@ use Drupal\Core\DependencyInjection\Compiler\RegisterRouteFiltersPass; use Drupal\Core\DependencyInjection\Compiler\RegisterRouteEnhancersPass; use Drupal\Core\DependencyInjection\Compiler\RegisterParamConvertersPass; +use Drupal\Core\DependencyInjection\Compiler\RegisterServicesForDestructionPass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\Reference; @@ -302,6 +303,10 @@ public function build(ContainerBuilder $container) { $container->register('plugin.manager.condition', 'Drupal\Core\Condition\ConditionManager'); + $container->register('kernel_destruct_subscriber', 'Drupal\Core\EventSubscriber\KernelDestructionSubscriber') + ->addMethodCall('setContainer', array(new Reference('service_container'))) + ->addTag('event_subscriber'); + $container->addCompilerPass(new RegisterMatchersPass()); $container->addCompilerPass(new RegisterRouteFiltersPass()); // Add a compiler pass for registering event subscribers. @@ -312,6 +317,8 @@ public function build(ContainerBuilder $container) { // Add a compiler pass for upcasting of entity route parameters. $container->addCompilerPass(new RegisterParamConvertersPass()); $container->addCompilerPass(new RegisterRouteEnhancersPass()); + // Add a compiler pass for registering services needing destruction. + $container->addCompilerPass(new RegisterServicesForDestructionPass()); } /** diff --git a/core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterServicesForDestructionPass.php b/core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterServicesForDestructionPass.php new file mode 100644 index 0000000000000000000000000000000000000000..ab89658f2da223a55a16e902abdce2923e61ffcf --- /dev/null +++ b/core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterServicesForDestructionPass.php @@ -0,0 +1,34 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\DependencyInjection\Compiler\RegisterServicesForDestructionPass. + */ + +namespace Drupal\Core\DependencyInjection\Compiler; + +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; + +/** + * Adds services tagged "needs_destruction" to the "kernel_destruct_subscriber" + * service. + */ +class RegisterServicesForDestructionPass implements CompilerPassInterface { + + /** + * Implements \Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface::process(). + */ + public function process(ContainerBuilder $container) { + if (!$container->hasDefinition('kernel_destruct_subscriber')) { + return; + } + + $definition = $container->getDefinition('kernel_destruct_subscriber'); + + $services = $container->findTaggedServiceIds('needs_destruction'); + foreach ($services as $id => $attributes) { + $definition->addMethodCall('registerService', array($id)); + } + } +} diff --git a/core/lib/Drupal/Core/DestructableInterface.php b/core/lib/Drupal/Core/DestructableInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..e06c6eca006ad85cc73fe0c9fd39caf326623a29 --- /dev/null +++ b/core/lib/Drupal/Core/DestructableInterface.php @@ -0,0 +1,19 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\DestructableInterface. + */ + +namespace Drupal\Core; + +/** + * The interface for services that need explicit destruction. + */ +interface DestructableInterface { + + /** + * Performs destruct operations. + */ + public function destruct(); +} diff --git a/core/lib/Drupal/Core/EventSubscriber/KernelDestructionSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/KernelDestructionSubscriber.php new file mode 100644 index 0000000000000000000000000000000000000000..e71f5caf347a6b5f1d6ac8446d861e1e71581229 --- /dev/null +++ b/core/lib/Drupal/Core/EventSubscriber/KernelDestructionSubscriber.php @@ -0,0 +1,67 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\EventSubscriber\KernelDestructionSubscriber. + */ + +namespace Drupal\Core\EventSubscriber; + +use Symfony\Component\DependencyInjection\ContainerAware; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\HttpKernel\Event\PostResponseEvent; +use Symfony\Component\HttpKernel\KernelEvents; + +/** + * Destructs services that are initiated and tagged with "needs_destruction". + */ +class KernelDestructionSubscriber extends ContainerAware implements EventSubscriberInterface { + + /** + * Holds an array of service ID's that will require destruction. + * + * @var array + */ + protected $services = array(); + + /** + * Registers a service for destruction. + * + * Calls to this method are set up in + * RegisterServicesForDestructionPass::process(). + * + * @param string $id + * Name of the service. + */ + public function registerService($id) { + $this->services[] = $id; + } + + /** + * Invoked by the terminate kernel event. + * + * @param \Symfony\Component\HttpKernel\Event\PostResponseEvent $event + * The event object. + */ + public function onKernelTerminate(PostResponseEvent $event) { + foreach ($this->services as $id) { + // Check if the service was initialized during this request, destruction + // is not necessary if the service was not used. + if ($this->container->initialized($id)) { + $service = $this->container->get($id); + $service->destruct(); + } + } + } + + /** + * Registers the methods in this class that should be listeners. + * + * @return array + * An array of event listener definitions. + */ + static function getSubscribedEvents() { + $events[KernelEvents::TERMINATE][] = array('onKernelTerminate', 100); + return $events; + } +} diff --git a/core/lib/Drupal/Core/Path/AliasManager.php b/core/lib/Drupal/Core/Path/AliasManager.php index 45bdca2ff9324ac754279a2dbe8ae0d047acf66e..06a45005a4df982c4268673656f14c2543266f5a 100644 --- a/core/lib/Drupal/Core/Path/AliasManager.php +++ b/core/lib/Drupal/Core/Path/AliasManager.php @@ -23,7 +23,7 @@ class AliasManager implements AliasManagerInterface { /** * The Key/Value Store to use for state * - * @var \Drupal\Core\KeyValueStore\DatabaseStorage + * @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface */ protected $state; diff --git a/core/modules/system/lib/Drupal/system/Tests/DrupalKernel/ServiceDestructionTest.php b/core/modules/system/lib/Drupal/system/Tests/DrupalKernel/ServiceDestructionTest.php new file mode 100644 index 0000000000000000000000000000000000000000..1310f697524e7d30f5409c1ef2fdef1c1c0c9d6c --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/DrupalKernel/ServiceDestructionTest.php @@ -0,0 +1,68 @@ +<?php + +/** + * @file + * Contains \Drupal\system\Tests\DrupalKernel\ServiceDestructionTest. + */ + +namespace Drupal\system\Tests\DrupalKernel; + +use Drupal\simpletest\DrupalUnitTestBase; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Event\PostResponseEvent; + +/** + * Tests the service destruction functionality. + */ +class ServiceDestructionTest extends DrupalUnitTestBase { + + public static function getInfo() { + return array( + 'name' => 'Service destruction', + 'description' => 'Tests that services are correctly destructed.', + 'group' => 'DrupalKernel', + ); + } + + /** + * Verifies that services are destructed when used. + */ + public function testDestructionUsed() { + // Enable the test module to add it to the container. + $this->enableModules(array('bundle_test')); + + // The service has not been destructed yet. + $this->assertNull(state()->get('bundle_test.destructed')); + + // Get the service destructor. + $service_destruction = $this->container->get('kernel_destruct_subscriber'); + + // Call the class and then invoke the kernel terminate event. + $this->container->get('bundle_test_class'); + $response = new Response(); + $event = new PostResponseEvent($this->container->get('kernel'), $this->container->get('request'), $response); + $service_destruction->onKernelTerminate($event); + $this->assertTrue(state()->get('bundle_test.destructed')); + } + + /** + * Verifies that services are not unnecessarily destructed when not used. + */ + public function testDestructionUnused() { + // Enable the test module to add it to the container. + $this->enableModules(array('bundle_test')); + + // The service has not been destructed yet. + $this->assertNull(state()->get('bundle_test.destructed')); + + // Get the service destructor. + $service_destruction = $this->container->get('kernel_destruct_subscriber'); + + // Simulate a shutdown. The test class has not been called, so it should not + // be destructed. + $response = new Response(); + $event = new PostResponseEvent($this->container->get('kernel'), $this->container->get('request'), $response); + $service_destruction->onKernelTerminate($event); + $this->assertNull(state()->get('bundle_test.destructed')); + } +} diff --git a/core/modules/system/tests/modules/bundle_test/lib/Drupal/bundle_test/BundleTestBundle.php b/core/modules/system/tests/modules/bundle_test/lib/Drupal/bundle_test/BundleTestBundle.php index 21a62d51d5a28a48aaf9c68e70381cbdba323090..19b50e17063a31fe37adee7693d786f7af79e120 100644 --- a/core/modules/system/tests/modules/bundle_test/lib/Drupal/bundle_test/BundleTestBundle.php +++ b/core/modules/system/tests/modules/bundle_test/lib/Drupal/bundle_test/BundleTestBundle.php @@ -19,7 +19,9 @@ class BundleTestBundle extends Bundle { public function build(ContainerBuilder $container) { $container->register('bundle_test_class', 'Drupal\bundle_test\TestClass') - ->addTag('event_subscriber'); + ->addArgument(new Reference('state')) + ->addTag('event_subscriber') + ->addTag('needs_destruction'); // Override a default bundle used by core to a dummy class. $container->register('file.usage', 'Drupal\bundle_test\TestFileUsage'); diff --git a/core/modules/system/tests/modules/bundle_test/lib/Drupal/bundle_test/TestClass.php b/core/modules/system/tests/modules/bundle_test/lib/Drupal/bundle_test/TestClass.php index f58f9c9fc455b4bf8d0ed426c1bac4651e0b18f6..ad711cbb9db848ee1a97a0110626a74ca270b368 100644 --- a/core/modules/system/tests/modules/bundle_test/lib/Drupal/bundle_test/TestClass.php +++ b/core/modules/system/tests/modules/bundle_test/lib/Drupal/bundle_test/TestClass.php @@ -7,11 +7,30 @@ namespace Drupal\bundle_test; -use Symfony\Component\HttpKernel\KernelEvents; -use Symfony\Component\HttpKernel\Event\GetResponseEvent; +use Drupal\Core\KeyValueStore\KeyValueStoreInterface; +use Drupal\Core\DestructableInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\HttpKernel\Event\GetResponseEvent; +use Symfony\Component\HttpKernel\KernelEvents; -class TestClass implements EventSubscriberInterface { +class TestClass implements EventSubscriberInterface, DestructableInterface { + + /** + * The state keyvalue collection. + * + * @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface + */ + protected $state; + + /** + * Constructor. + * + * @param \Drupal\Core\KeyValueStore\KeyValueStoreInterface $state + * The state key value store. + */ + public function __construct(KeyValueStoreInterface $state) { + $this->state = $state; + } /** * A simple kernel listener method. @@ -30,4 +49,11 @@ static function getSubscribedEvents() { $events[KernelEvents::REQUEST][] = array('onKernelRequestTest', 100); return $events; } + + /** + * Implements \Drupal\Core\DestructableInterface::destruct(). + */ + public function destruct() { + $this->state->set('bundle_test.destructed', TRUE); + } } diff --git a/core/modules/views/lib/Drupal/views/ViewsBundle.php b/core/modules/views/lib/Drupal/views/ViewsBundle.php index 0b18d3f5b685d7e28e9489b64c4eb82f846a02ef..e4443f8560b3385ecacae994881d52219cd8cdda 100644 --- a/core/modules/views/lib/Drupal/views/ViewsBundle.php +++ b/core/modules/views/lib/Drupal/views/ViewsBundle.php @@ -35,7 +35,8 @@ public function build(ContainerBuilder $container) { $container->register('views.views_data', 'Drupal\views\ViewsDataCache') ->addArgument(new Reference('cache.views_info')) - ->addArgument(new Reference('config.factory')); + ->addArgument(new Reference('config.factory')) + ->addTag('needs_destruction'); $container->register('views.executable', 'Drupal\views\ViewExecutableFactory'); diff --git a/core/modules/views/lib/Drupal/views/ViewsDataCache.php b/core/modules/views/lib/Drupal/views/ViewsDataCache.php index e8d474730c384072482f206113304e58d16d2acd..15e338c74cfabfac334adc260bc37e6371fb70d5 100644 --- a/core/modules/views/lib/Drupal/views/ViewsDataCache.php +++ b/core/modules/views/lib/Drupal/views/ViewsDataCache.php @@ -7,13 +7,14 @@ namespace Drupal\views; -use Drupal\Core\Config\ConfigFactory; use Drupal\Core\Cache\CacheBackendInterface; +use Drupal\Core\Config\ConfigFactory; +use Drupal\Core\DestructableInterface; /** * Class to manage and lazy load cached views data. */ -class ViewsDataCache { +class ViewsDataCache implements DestructableInterface { /** * The base cache ID to use. @@ -241,25 +242,18 @@ public function fetchBaseTables() { } /** - * Destructs the ViewDataCache object. + * Implements \Drupal\Core\DestructableInterface::destruct(). */ - public function __destruct() { - try { - if ($this->rebuildCache && !empty($this->storage)) { - // Keep a record with all data. - $this->set($this->baseCid, $this->storage); - // Save data in seperate cache entries. - foreach ($this->storage as $table => $data) { - $cid = $this->baseCid . ':' . $table; - $this->set($cid, $data); - } + public function destruct() { + if ($this->rebuildCache && !empty($this->storage)) { + // Keep a record with all data. + $this->set($this->baseCid, $this->storage); + // Save data in seperate cache entries. + foreach ($this->storage as $table => $data) { + $cid = $this->baseCid . ':' . $table; + $this->set($cid, $data); } } - catch (\Exception $e) { - // During testing the table is gone before this fires. - // @todo Use terminate() instead of __destruct(), see - // http://drupal.org/node/512026. - } } }