From afc4b899455ce0683591de7bd58581e9df5c2e41 Mon Sep 17 00:00:00 2001 From: catch <catch@35733.no-reply.drupal.org> Date: Mon, 22 Oct 2012 10:14:49 +0100 Subject: [PATCH] Issue #1706064 by chx, katbailey, effulgentsia, podarok et al: Compile the Injection Container to disk. --- core/includes/bootstrap.inc | 4 +- core/includes/common.inc | 5 +- core/includes/file.inc | 52 ++++--- core/includes/install.core.inc | 8 +- .../PhpStorage/FileReadOnlyStorage.php | 14 ++ .../Component/PhpStorage/FileStorage.php | 23 +++ .../PhpStorage/PhpStorageInterface.php | 12 ++ core/lib/Drupal/Core/CoreBundle.php | 61 ++++---- .../Compiler/RegisterMatchersPass.php | 35 +++++ .../Compiler/RegisterNestedMatchersPass.php | 35 +++++ core/lib/Drupal/Core/DrupalKernel.php | 134 ++++++++++++++++-- .../FinishResponseSubscriber.php | 23 ++- .../lib/Drupal/simpletest/TestBase.php | 2 +- .../Tests/DrupalKernel/DrupalKernelTest.php | 111 +++++++++++++++ .../Drupal/bundle_test/BundleTestBundle.php | 4 - core/scripts/run-tests.sh | 2 +- index.php | 2 +- 17 files changed, 443 insertions(+), 84 deletions(-) create mode 100644 core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterMatchersPass.php create mode 100644 core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterNestedMatchersPass.php create mode 100644 core/modules/system/lib/Drupal/system/Tests/DrupalKernel/DrupalKernelTest.php diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc index fdcb01bf4321..dfb61e1514fa 100644 --- a/core/includes/bootstrap.inc +++ b/core/includes/bootstrap.inc @@ -894,7 +894,7 @@ function drupal_get_filename($type, $name, $filename = NULL) { // Verify that we have an keyvalue service before using it. This is required // because this function is called during installation. // @todo Inject database connection into KeyValueStore\DatabaseStorage. - if (drupal_container()->hasDefinition('keyvalue') && function_exists('db_query')) { + if (drupal_container()->has('keyvalue') && function_exists('db_query')) { try { $file_list = state()->get('system.' . $type . '.files'); if ($file_list && isset($file_list[$name]) && file_exists(DRUPAL_ROOT . '/' . $file_list[$name])) { @@ -2442,7 +2442,7 @@ function drupal_container(Container $new_container = NULL, $rebuild = FALSE) { ->addArgument(config_get_config_directory(CONFIG_ACTIVE_DIRECTORY)); // @todo Replace this with a cache.factory service plus 'config' argument. $container - ->register('cache.config') + ->register('cache.config', 'Drupal\Core\Cache\CacheBackendInterface') ->setFactoryClass('Drupal\Core\Cache\CacheFactory') ->setFactoryMethod('get') ->addArgument('config'); diff --git a/core/includes/common.inc b/core/includes/common.inc index 0f252cacfd62..6800fa5dda31 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -3901,7 +3901,7 @@ function drupal_get_js($scope = 'header', $javascript = NULL, $skip_alter = FALS // this is an AJAX request. // @todo Clean up container call. $container = drupal_container(); - if ($container->has('request') && $container->has('content_negotiation')) { + if ($container->has('content_negotiation') && $container->isScopeActive('request')) { $type = $container->get('content_negotiation')->getContentType($container->get('request')); } if (!empty($items['settings']) || (!empty($type) && $type == 'ajax')) { @@ -6853,6 +6853,9 @@ function drupal_flush_all_caches() { drupal_container()->get('router.builder')->rebuild(); menu_router_rebuild(); + // Wipe the DIC cache. + drupal_php_storage('service_container')->deleteAll(); + // Re-initialize the maintenance theme, if the current request attempted to // use it. Unlike regular usages of this function, the installer and update // scripts need to flush all caches during GET requests/page building. diff --git a/core/includes/file.inc b/core/includes/file.inc index 977d18636ae6..16ed6192f711 100644 --- a/core/includes/file.inc +++ b/core/includes/file.inc @@ -1441,32 +1441,40 @@ function file_scan_directory($dir, $mask, $options = array(), $depth = 0) { $options['key'] = in_array($options['key'], array('uri', 'filename', 'name')) ? $options['key'] : 'uri'; $files = array(); - if (is_dir($dir) && $handle = opendir($dir)) { - while (FALSE !== ($filename = readdir($handle))) { - if (!preg_match($options['nomask'], $filename) && $filename[0] != '.') { - $uri = "$dir/$filename"; - $uri = file_stream_wrapper_uri_normalize($uri); - if (is_dir($uri) && $options['recurse']) { - // Give priority to files in this folder by merging them in after any subdirectory files. - $files = array_merge(file_scan_directory($uri, $mask, $options, $depth + 1), $files); - } - elseif ($depth >= $options['min_depth'] && preg_match($mask, $filename)) { - // Always use this match over anything already set in $files with the - // same $$options['key']. - $file = new stdClass(); - $file->uri = $uri; - $file->filename = $filename; - $file->name = pathinfo($filename, PATHINFO_FILENAME); - $key = $options['key']; - $files[$file->$key] = $file; - if ($options['callback']) { - $options['callback']($uri); + // Avoid warnings when opendir does not have the permissions to open a + // directory. + if (is_dir($dir)) { + if($handle = @opendir($dir)) { + while (FALSE !== ($filename = readdir($handle))) { + if (!preg_match($options['nomask'], $filename) && $filename[0] != '.') { + $uri = "$dir/$filename"; + $uri = file_stream_wrapper_uri_normalize($uri); + if (is_dir($uri) && $options['recurse']) { + // Give priority to files in this folder by merging them in after + // any subdirectory files. + $files = array_merge(file_scan_directory($uri, $mask, $options, $depth + 1), $files); + } + elseif ($depth >= $options['min_depth'] && preg_match($mask, $filename)) { + // Always use this match over anything already set in $files with + // the same $options['key']. + $file = new stdClass(); + $file->uri = $uri; + $file->filename = $filename; + $file->name = pathinfo($filename, PATHINFO_FILENAME); + $key = $options['key']; + $files[$file->$key] = $file; + if ($options['callback']) { + $options['callback']($uri); + } } } } - } - closedir($handle); + closedir($handle); + } + else { + watchdog('file', '@dir can not be opened', array('@dir' => $dir), WATCHDOG_ERROR); + } } return $files; diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc index 0fcdd946ba54..3968682c1320 100644 --- a/core/includes/install.core.inc +++ b/core/includes/install.core.inc @@ -1495,11 +1495,9 @@ function install_bootstrap_full(&$install_state) { // Clear the module list that was overriden earlier in the process. // This will allow all freshly installed modules to be loaded. module_list_reset(); - // @todo The constructor parameters for the Kernel class are for environment, - // e.g. 'prod', 'dev', and a boolean indicating whether it is in debug mode. - // Drupal does not currently make use of either of these, though that may - // change with http://drupal.org/node/1537198. - $kernel = new DrupalKernel('prod', FALSE); + + // Instantiate the kernel. + $kernel = new DrupalKernel('prod', FALSE, NULL); $kernel->boot(); drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL); } diff --git a/core/lib/Drupal/Component/PhpStorage/FileReadOnlyStorage.php b/core/lib/Drupal/Component/PhpStorage/FileReadOnlyStorage.php index 3d1bd950862c..bfb7a2a07fc4 100644 --- a/core/lib/Drupal/Component/PhpStorage/FileReadOnlyStorage.php +++ b/core/lib/Drupal/Component/PhpStorage/FileReadOnlyStorage.php @@ -69,4 +69,18 @@ public function delete($name) { protected function getFullPath($name) { return $this->directory . '/' . $name; } + + /** + * Implements Drupal\Component\PhpStorage\PhpStorageInterface::writeable(). + */ + function writeable() { + return FALSE; + } + + /** + * Implements Drupal\Component\PhpStorage\PhpStorageInterface::deleteAll(). + */ + public function deleteAll() { + return FALSE; + } } diff --git a/core/lib/Drupal/Component/PhpStorage/FileStorage.php b/core/lib/Drupal/Component/PhpStorage/FileStorage.php index 80f3ec440311..a07c27e9a152 100644 --- a/core/lib/Drupal/Component/PhpStorage/FileStorage.php +++ b/core/lib/Drupal/Component/PhpStorage/FileStorage.php @@ -71,4 +71,27 @@ public function delete($name) { protected function getFullPath($name) { return $this->directory . '/' . $name; } + + /** + * Implements Drupal\Component\PhpStorage\PhpStorageInterface::writeable(). + */ + function writeable() { + return TRUE; + } + + /** + * Implements Drupal\Component\PhpStorage\PhpStorageInterface::deleteAll(). + */ + function deleteAll() { + return file_unmanaged_delete_recursive($this->directory, array(__CLASS__, 'filePreDeleteCallback')); + } + + /** + * Ensures files and directories are deletable. + */ + public static function filePreDeleteCallback($path) { + if (file_exists($path)) { + chmod($path, 0700); + } + } } diff --git a/core/lib/Drupal/Component/PhpStorage/PhpStorageInterface.php b/core/lib/Drupal/Component/PhpStorage/PhpStorageInterface.php index 1eaece362038..03ca8124f3c6 100644 --- a/core/lib/Drupal/Component/PhpStorage/PhpStorageInterface.php +++ b/core/lib/Drupal/Component/PhpStorage/PhpStorageInterface.php @@ -56,6 +56,13 @@ public function load($name); */ public function save($name, $code); + /** + * Whether this is a writeable storage. + * + * @return bool + */ + public function writeable(); + /** * Deletes PHP code from storage. * @@ -66,4 +73,9 @@ public function save($name, $code); * TRUE if the delete succeeded, FALSE if it failed. */ public function delete($name); + + /** + * Removes all files in this bin. + */ + public function deleteAll(); } diff --git a/core/lib/Drupal/Core/CoreBundle.php b/core/lib/Drupal/Core/CoreBundle.php index f1e43d64c31d..aeb270c75172 100644 --- a/core/lib/Drupal/Core/CoreBundle.php +++ b/core/lib/Drupal/Core/CoreBundle.php @@ -8,6 +8,8 @@ namespace Drupal\Core; use Drupal\Core\DependencyInjection\Compiler\RegisterKernelListenersPass; +use Drupal\Core\DependencyInjection\Compiler\RegisterMatchersPass; +use Drupal\Core\DependencyInjection\Compiler\RegisterNestedMatchersPass; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; @@ -26,6 +28,7 @@ */ class CoreBundle extends Bundle { + public function build(ContainerBuilder $container) { // The 'request' scope and service enable services to depend on the Request @@ -61,41 +64,34 @@ public function build(ContainerBuilder $container) { ->addArgument(new Reference('database')) ->addArgument(new Reference('lock')); - $container->register('router.dumper', '\Drupal\Core\Routing\MatcherDumper') + $container->register('router.dumper', 'Drupal\Core\Routing\MatcherDumper') ->addArgument(new Reference('database')); $container->register('router.builder', 'Drupal\Core\Routing\RouteBuilder') ->addArgument(new Reference('router.dumper')); - // @todo Replace below lines with the commented out block below it when it's - // performant to do so: http://drupal.org/node/1706064. - $dispatcher = $container->get('dispatcher'); - $matcher = new \Drupal\Core\Routing\ChainMatcher(); - $matcher->add(new \Drupal\Core\LegacyUrlMatcher()); - - $nested = new \Drupal\Core\Routing\NestedMatcher(); - $nested->setInitialMatcher(new \Drupal\Core\Routing\PathMatcher(Database::getConnection())); - $nested->addPartialMatcher(new \Drupal\Core\Routing\HttpMethodMatcher()); - $nested->setFinalMatcher(new \Drupal\Core\Routing\FirstEntryFinalMatcher()); - $matcher->add($nested, 5); + $container->register('matcher', 'Drupal\Core\Routing\ChainMatcher'); + $container->register('legacy_url_matcher', 'Drupal\Core\LegacyUrlMatcher') + ->addTag('chained_matcher'); + $container->register('nested_matcher', 'Drupal\Core\Routing\NestedMatcher') + ->addTag('chained_matcher', array('priority' => 5)); - $content_negotation = new \Drupal\Core\ContentNegotiation(); - $dispatcher->addSubscriber(new \Symfony\Component\HttpKernel\EventListener\RouterListener($matcher)); - $dispatcher->addSubscriber(new \Drupal\Core\EventSubscriber\ViewSubscriber($content_negotation)); - $dispatcher->addSubscriber(new \Drupal\Core\EventSubscriber\AccessSubscriber()); - $dispatcher->addSubscriber(new \Drupal\Core\EventSubscriber\MaintenanceModeSubscriber()); - $dispatcher->addSubscriber(new \Drupal\Core\EventSubscriber\PathSubscriber()); - $dispatcher->addSubscriber(new \Drupal\Core\EventSubscriber\LegacyRequestSubscriber()); - $dispatcher->addSubscriber(new \Drupal\Core\EventSubscriber\LegacyControllerSubscriber()); - $dispatcher->addSubscriber(new \Drupal\Core\EventSubscriber\FinishResponseSubscriber()); - $dispatcher->addSubscriber(new \Drupal\Core\EventSubscriber\RequestCloseSubscriber()); - $dispatcher->addSubscriber(new \Drupal\Core\EventSubscriber\ConfigGlobalOverrideSubscriber()); - $dispatcher->addSubscriber(new \Drupal\Core\EventSubscriber\RouteProcessorSubscriber()); - $container->set('content_negotiation', $content_negotation); - $dispatcher->addSubscriber(\Drupal\Core\ExceptionController::getExceptionListener($container)); + // The following services are tagged as 'nested_matcher' services and are + // processed in the RegisterNestedMatchersPass compiler pass. Each one + // needs to be set on the matcher using a different method, so we use a + // tag attribute, 'method', which can be retrieved and passed to the + // addMethodCall() method that gets called on the matcher service in the + // compiler pass. + $container->register('path_matcher', 'Drupal\Core\Routing\PathMatcher') + ->addArgument(new Reference('database')) + ->addTag('nested_matcher', array('method' => 'setInitialMatcher')); + $container->register('http_method_matcher', 'Drupal\Core\Routing\HttpMethodMatcher') + ->addTag('nested_matcher', array('method' => 'addPartialMatcher')); + $container->register('first_entry_final_matcher', 'Drupal\Core\Routing\FirstEntryFinalMatcher') + ->addTag('nested_matcher', array('method' => 'setFinalMatcher')); - /* - $container->register('matcher', 'Drupal\Core\LegacyUrlMatcher'); - $container->register('router_listener', 'Drupal\Core\EventSubscriber\RouterListener') + $container->register('router_processor_subscriber', 'Drupal\Core\EventSubscriber\RouteProcessorSubscriber') + ->addTag('kernel.event_subscriber'); + $container->register('router_listener', 'Symfony\Component\HttpKernel\EventListener\RouterListener') ->addArgument(new Reference('matcher')) ->addTag('kernel.event_subscriber'); $container->register('content_negotiation', 'Drupal\Core\ContentNegotiation'); @@ -118,15 +114,18 @@ public function build(ContainerBuilder $container) { ->addTag('kernel.event_subscriber'); $container->register('request_close_subscriber', 'Drupal\Core\EventSubscriber\RequestCloseSubscriber') ->addTag('kernel.event_subscriber'); - $container->register('config_global_override_subscriber', '\Drupal\Core\EventSubscriber\ConfigGlobalOverrideSubscriber'); + $container->register('config_global_override_subscriber', 'Drupal\Core\EventSubscriber\ConfigGlobalOverrideSubscriber') + ->addTag('kernel.event_subscriber'); $container->register('exception_listener', 'Drupal\Core\EventSubscriber\ExceptionListener') ->addTag('kernel.event_subscriber') ->addArgument(new Reference('service_container')) ->setFactoryClass('Drupal\Core\ExceptionController') ->setFactoryMethod('getExceptionListener'); + $container->addCompilerPass(new RegisterMatchersPass()); + $container->addCompilerPass(new RegisterNestedMatchersPass()); // Add a compiler pass for registering event subscribers. $container->addCompilerPass(new RegisterKernelListenersPass(), PassConfig::TYPE_AFTER_REMOVING); - */ } + } diff --git a/core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterMatchersPass.php b/core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterMatchersPass.php new file mode 100644 index 000000000000..793394ba7f58 --- /dev/null +++ b/core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterMatchersPass.php @@ -0,0 +1,35 @@ +<?php + +/** + * @file + * Contains Drupal\Core\DependencyInjection\Compiler\RegisterMatchersPass. + */ + +namespace Drupal\Core\DependencyInjection\Compiler; + +use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; + +/** + * Adds services tagged 'chained_matcher' to the 'matcher' service. + */ +class RegisterMatchersPass implements CompilerPassInterface { + + /** + * Adds services tagged 'chained_matcher' to the 'matcher' service. + * + * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container + * The container to process. + */ + public function process(ContainerBuilder $container) { + if (!$container->hasDefinition('matcher')) { + return; + } + $matcher = $container->getDefinition('matcher'); + foreach ($container->findTaggedServiceIds('chained_matcher') as $id => $attributes) { + $priority = isset($attributes[0]['priority']) ? $attributes[0]['priority'] : 0; + $matcher->addMethodCall('add', array(new Reference($id), $priority)); + } + } +} diff --git a/core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterNestedMatchersPass.php b/core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterNestedMatchersPass.php new file mode 100644 index 000000000000..b245952c6330 --- /dev/null +++ b/core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterNestedMatchersPass.php @@ -0,0 +1,35 @@ +<?php + +/** + * @file + * Contains Drupal\Core\DependencyInjection\Compiler\RegisterNestedMatchersPass. + */ + +namespace Drupal\Core\DependencyInjection\Compiler; + +use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; + +/** + * Adds services tagged 'nested_matcher' to the tagged_matcher service. + */ +class RegisterNestedMatchersPass implements CompilerPassInterface { + + /** + * Adds services tagged 'nested_matcher' to the tagged_matcher service. + * + * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container + * The container to process. + */ + public function process(ContainerBuilder $container) { + if (!$container->hasDefinition('nested_matcher')) { + return; + } + $nested = $container->getDefinition('nested_matcher'); + foreach ($container->findTaggedServiceIds('nested_matcher') as $id => $attributes) { + $method = $attributes[0]['method']; + $nested->addMethodCall($method, array(new Reference($id))); + } + } +} diff --git a/core/lib/Drupal/Core/DrupalKernel.php b/core/lib/Drupal/Core/DrupalKernel.php index 869d4ee36bdb..e2991e1c065e 100644 --- a/core/lib/Drupal/Core/DrupalKernel.php +++ b/core/lib/Drupal/Core/DrupalKernel.php @@ -7,11 +7,14 @@ namespace Drupal\Core; +use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\CoreBundle; +use Drupal\Component\PhpStorage\PhpStorageInterface; use Symfony\Component\HttpKernel\Kernel; use Drupal\Core\DependencyInjection\ContainerBuilder; use Symfony\Component\Config\Loader\LoaderInterface; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; +use Symfony\Component\DependencyInjection\Dumper\PhpDumper; /** * The DrupalKernel class is the core of Drupal itself. @@ -25,6 +28,60 @@ */ class DrupalKernel extends Kernel { + /** + * Holds the list of enabled modules. + * + * @var array + */ + protected $moduleList; + + /** + * Cache object for getting or setting the compiled container's class name. + * + * @var \Drupal\Core\Cache\CacheBackendInterface + */ + protected $compilationIndexCache; + + /** + * PHP code storage object to use for the compiled container. + * + * @var \Drupal\Component\PhpStorage\PhpStorageInterface + */ + protected $storage; + + /** + * Constructs a DrupalKernel object. + * + * @param string $environment + * String indicating the environment, e.g. 'prod' or 'dev'. Used by + * Symfony\Component\HttpKernel\Kernel::__construct(). Drupal does not use + * this value currently. Pass 'prod'. + * @param bool $debug + * Boolean indicating whether we are in debug mode. Used by + * Symfony\Component\HttpKernel\Kernel::__construct(). Drupal does not use + * this value currently. Pass TRUE. + * @param array $module_list + * (optional) The array of enabled modules as returned by module_list(). + * @param Drupal\Core\Cache\CacheBackendInterface $compilation_index_cache + * (optional) If wanting to dump a compiled container to disk or use a + * previously compiled container, the cache object for the bin that stores + * the class name of the compiled container. + */ + public function __construct($environment, $debug, array $module_list = NULL, CacheBackendInterface $compilation_index_cache = NULL) { + parent::__construct($environment, $debug); + $this->compilationIndexCache = $compilation_index_cache; + $this->storage = drupal_php_storage('service_container'); + if (isset($module_list)) { + $this->moduleList = $module_list; + } + else { + // @todo This is a temporary measure which will no longer be necessary + // once we have an ExtensionHandler for managing this list. See + // http://drupal.org/node/1331486. + $this->moduleList = module_list(); + } + } + /** * Overrides Kernel::init(). */ @@ -43,10 +100,7 @@ public function registerBundles() { new CoreBundle(), ); - // @todo Remove the necessity of calling system_list() to find out which - // bundles exist. See http://drupal.org/node/1331486 - $modules = array_keys(system_list('module_enabled')); - foreach ($modules as $module) { + foreach ($this->moduleList as $module) { $camelized = ContainerBuilder::camelize($module); $class = "Drupal\\{$module}\\{$camelized}Bundle"; if (class_exists($class)) { @@ -61,12 +115,43 @@ public function registerBundles() { * Initializes the service container. */ protected function initializeContainer() { - // @todo We should be compiling the container and dumping to php so we don't - // have to recompile every time. There is a separate issue for this, see - // http://drupal.org/node/1668892. - $this->container = $this->buildContainer(); + $this->container = NULL; + if ($this->compilationIndexCache) { + // The name of the compiled container class is generated from the hash of + // its contents and cached. This enables multiple compiled containers + // (for example, for different states of which modules are enabled) to + // exist simultaneously on disk and in memory. + if ($cache = $this->compilationIndexCache->get(implode(':', array('service_container', $this->environment, $this->debug)))) { + $class = $cache->data; + $cache_file = $class . '.php'; + + // First, try to load. + if (!class_exists($class, FALSE)) { + $this->storage->load($cache_file); + } + // If the load succeeded or the class already existed, use it. + if (class_exists($class, FALSE)) { + $fully_qualified_class_name = '\\' . $class; + $this->container = new $fully_qualified_class_name; + } + } + } + if (!isset($this->container)) { + $this->container = $this->buildContainer(); + if ($this->compilationIndexCache && !$this->dumpDrupalContainer($this->container, $this->getContainerBaseClass())) { + // We want to log this as an error but we cannot call watchdog() until + // the container has been fully built and set in drupal_container(). + $error = 'Container cannot be written to disk'; + } + } + $this->container->set('kernel', $this); + drupal_container($this->container); + + if (isset($error)) { + watchdog('DrupalKernel', $error); + } } /** @@ -84,10 +169,7 @@ protected function buildContainer() { foreach ($this->bundles as $bundle) { $bundle->build($container); } - - // @todo Compile the container: http://drupal.org/node/1706064. - //$container->compile(); - + $container->compile(); return $container; } @@ -100,6 +182,34 @@ protected function getContainerBuilder() { return new ContainerBuilder(new ParameterBag($this->getKernelParameters())); } + /** + * Dumps the service container to PHP code in the config directory. + * + * This method is based on the dumpContainer method in the parent class, but + * that method is reliant on the Config component which we do not use here. + * + * @param ContainerBuilder $container + * The service container. + * @param string $baseClass + * The name of the container's base class + * + * @return bool + * TRUE if the container was successfully dumped to disk. + */ + protected function dumpDrupalContainer(ContainerBuilder $container, $baseClass) { + if (!$this->storage->writeable()) { + return FALSE; + } + // Cache the container. + $dumper = new PhpDumper($container); + $content = $dumper->dump(array('class' => 'DrupalServiceContainerStub', 'base_class' => $baseClass)); + $class = 'c' . hash('sha256', $content); + $content = str_replace('DrupalServiceContainerStub', $class, $content); + $this->compilationIndexCache->set(implode(':', array('service_container', $this->environment, $this->debug)), $class); + + return $this->storage->save($class . '.php', $content); + } + /** * Overrides and eliminates this method from the parent class. Do not use. * diff --git a/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php index 8065349e204b..a4ec691a94b1 100644 --- a/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php +++ b/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php @@ -7,6 +7,7 @@ namespace Drupal\Core\EventSubscriber; +use Drupal\Core\Language\LanguageManager; use Symfony\Component\HttpKernel\Event\FilterResponseEvent; use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\EventDispatcher\EventSubscriberInterface; @@ -16,6 +17,23 @@ */ class FinishResponseSubscriber implements EventSubscriberInterface { + /** + * The LanguageManager object for retrieving the correct language code. + * + * @var LanguageManager + */ + protected $languageManager; + + /** + * Constructs a FinishResponseSubscriber object. + * + * @param LanguageManager $language_manager + * The LanguageManager object for retrieving the correct language code. + */ + public function __construct(LanguageManager $language_manager) { + $this->languageManager = $language_manager; + } + /** * Sets extra headers on successful responses. * @@ -30,10 +48,7 @@ public function onRespond(FilterResponseEvent $event) { $response->headers->set('X-UA-Compatible', 'IE=edge,chrome=1', false); // Set the Content-language header. - // @todo Receive the LanguageManager object as a constructor argument when - // the dependency injection container allows for it performantly: - // http://drupal.org/node/1706064. - $response->headers->set('Content-language', language(LANGUAGE_TYPE_INTERFACE)->langcode); + $response->headers->set('Content-language', $this->languageManager->getLanguage(LANGUAGE_TYPE_INTERFACE)->langcode); // Because pages are highly dynamic, set the last-modified time to now // since the page is in fact being regenerated right now. diff --git a/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php b/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php index dc6dae427390..9d151e06e138 100644 --- a/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php +++ b/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php @@ -911,7 +911,7 @@ protected function rebuildContainer() { // container in drupal_container(). Drupal\simpletest\TestBase::tearDown() // restores the original container. // @see Drupal\Core\DrupalKernel::initializeContainer() - $this->kernel = new DrupalKernel('testing', FALSE); + $this->kernel = new DrupalKernel('testing', FALSE, NULL); // Booting the kernel is necessary to initialize the new DIC. While // normally the kernel gets booted on demand in // Symfony\Component\HttpKernel\handle(), this kernel needs manual booting diff --git a/core/modules/system/lib/Drupal/system/Tests/DrupalKernel/DrupalKernelTest.php b/core/modules/system/lib/Drupal/system/Tests/DrupalKernel/DrupalKernelTest.php new file mode 100644 index 000000000000..41070c32bb59 --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/DrupalKernel/DrupalKernelTest.php @@ -0,0 +1,111 @@ +<?php + +/** + * @file + * Contains Drupal\system\Tests\DrupalKernel\DrupalKernelTest. + */ + +namespace Drupal\system\Tests\DrupalKernel; + +use Drupal\Core\Cache\MemoryBackend; +use Drupal\Core\DrupalKernel; +use Drupal\simpletest\UnitTestBase; +use ReflectionClass; + +/** + * Tests compilation of the DIC. + */ +class DrupalKernelTest extends UnitTestBase { + + public static function getInfo() { + return array( + 'name' => 'DrupalKernel tests', + 'description' => 'Tests DIC compilation to disk.', + 'group' => 'DrupalKernel', + ); + } + + /** + * Tests DIC compilation. + */ + function testCompileDIC() { + // Because we'll be instantiating a new kernel during this test, the + // container stored in drupal_container() will be updated as a side effect. + // We need to be able to restore it to the correct one at the end of this + // test. + $original_container = drupal_container(); + global $conf; + $conf['php_storage']['service_container'] = array( + 'class' => 'Drupal\Component\PhpStorage\MTimeProtectedFileStorage', + 'secret' => $GLOBALS['drupal_hash_salt'], + ); + $cache = new MemoryBackend('test'); + $module_enabled = array( + 'system' => 'system', + 'user' => 'user', + ); + $kernel = new DrupalKernel('testing', FALSE, $module_enabled, $cache); + $kernel->boot(); + // Instantiate it a second time and we should get the compiled Container + // class. + $kernel = new DrupalKernel('testing', FALSE, $module_enabled, $cache); + $kernel->boot(); + $container = $kernel->getContainer(); + $refClass = new ReflectionClass($container); + $is_compiled_container = + $refClass->getParentClass()->getName() == 'Symfony\Component\DependencyInjection\Container' && + !$refClass->isSubclassOf('Symfony\Component\DependencyInjection\ContainerBuilder'); + $this->assertTrue($is_compiled_container); + + // Reset the container. + drupal_container(NULL, TRUE); + + // Now use the read-only storage implementation, simulating a "production" + // environment. + drupal_static_reset('drupal_php_storage'); + $conf['php_storage']['service_container'] = array( + 'class' => 'Drupal\Component\PhpStorage\FileReadOnlyStorage', + ); + $kernel = new DrupalKernel('testing', FALSE, $module_enabled, $cache); + $kernel->boot(); + $container = $kernel->getContainer(); + $refClass = new ReflectionClass($container); + $is_compiled_container = + $refClass->getParentClass()->getName() == 'Symfony\Component\DependencyInjection\Container' && + !$refClass->isSubclassOf('Symfony\Component\DependencyInjection\ContainerBuilder'); + $this->assertTrue($is_compiled_container); + + // We make this assertion here purely to show that the new container below + // is functioning correctly, i.e. we get a brand new ContainerBuilder + // which has the required new services, after changing the list of enabled + // modules. + $this->assertFalse($container->has('bundle_test_class')); + + // Reset the container. + drupal_container(NULL, TRUE); + + // Add another module so that we can test that the new module's bundle is + // registered to the new container. + $module_enabled = array( + 'system' => 'system', + 'user' => 'user', + 'bundle_test' => 'bundle_test', + ); + $cache->flush(); + $kernel = new DrupalKernel('testing', FALSE, $module_enabled, $cache); + $kernel->boot(); + // Instantiate it a second time and we should still get a ContainerBuilder + // class because we are using the read-only PHP storage. + $kernel = new DrupalKernel('testing', FALSE, $module_enabled, $cache); + $kernel->boot(); + $container = $kernel->getContainer(); + $refClass = new ReflectionClass($container); + $is_container_builder = $refClass->isSubclassOf('Symfony\Component\DependencyInjection\ContainerBuilder'); + $this->assertTrue($is_container_builder); + // Assert that the new module's bundle was registered to the new container. + $this->assertTrue($container->has('bundle_test_class')); + + // Restore the original container. + drupal_container($original_container); + } +} 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 97b79c18afb6..c2db16a3fb41 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 @@ -23,9 +23,5 @@ public function build(ContainerBuilder $container) { // Override a default bundle used by core to a dummy class. $container->register('file.usage', 'Drupal\bundle_test\TestFileUsage'); - - // @todo Remove when the 'kernel.event_subscriber' tag above is made to - // work: http://drupal.org/node/1706064. - $container->get('dispatcher')->addSubscriber($container->get('bundle_test_class')); } } diff --git a/core/scripts/run-tests.sh b/core/scripts/run-tests.sh index 2395a846b18f..5aa8b22d0175 100755 --- a/core/scripts/run-tests.sh +++ b/core/scripts/run-tests.sh @@ -479,7 +479,7 @@ function simpletest_script_cleanup($test_id, $test_class, $exitcode) { // simpletest_clean_temporary_directories() cannot be used here, since it // would also delete file directories of other tests that are potentially // running concurrently. - file_unmanaged_delete_recursive($test_directory); + file_unmanaged_delete_recursive($test_directory, array('Drupal\simpletest\TestBase', 'filePreDeleteCallback')); $messages[] = "- Removed test files directory."; } diff --git a/index.php b/index.php index 38177ab2b5ba..a6eb61e2a804 100644 --- a/index.php +++ b/index.php @@ -28,7 +28,7 @@ drupal_bootstrap(DRUPAL_BOOTSTRAP_CODE); // @todo Figure out how best to handle the Kernel constructor parameters. -$kernel = new DrupalKernel('prod', FALSE); +$kernel = new DrupalKernel('prod', FALSE, NULL, cache('bootstrap')); // Create a request object from the HTTPFoundation. $request = Request::createFromGlobals(); -- GitLab