diff --git a/core/core.services.yml b/core/core.services.yml index c56c778..4926c36 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -1332,7 +1332,7 @@ services: arguments: ['@app.root', '@theme_handler', '@cache.bootstrap', '@module_handler'] theme.registry: class: Drupal\Core\Theme\Registry - arguments: ['@app.root', '@cache.default', '@lock', '@module_handler', '@theme_handler', '@theme.initialization'] + arguments: ['@app.root', '@cache.default', '@lock', '@module_handler', '@theme_handler', '@theme.initialization', '@request_stack'] tags: - { name: needs_destruction } calls: diff --git a/core/lib/Drupal/Core/Theme/Registry.php b/core/lib/Drupal/Core/Theme/Registry.php index 0167a74..a5ce34c 100644 --- a/core/lib/Drupal/Core/Theme/Registry.php +++ b/core/lib/Drupal/Core/Theme/Registry.php @@ -14,6 +14,7 @@ use Drupal\Core\Extension\ThemeHandlerInterface; use Drupal\Core\Lock\LockBackendInterface; use Drupal\Core\Utility\ThemeRegistry; +use Symfony\Component\HttpFoundation\RequestStack; /** * Defines the theme registry service. @@ -138,6 +139,13 @@ class Registry implements DestructableInterface { protected $themeManager; /** + * The related request stack. + * + * @var \Symfony\Component\HttpFoundation\RequestStack + */ + protected $requestStack; + + /** * Constructs a \Drupal\Core\Theme\Registry object. * * @param string $root @@ -152,10 +160,12 @@ class Registry implements DestructableInterface { * The theme handler. * @param \Drupal\Core\Theme\ThemeInitializationInterface $theme_initialization * The theme initialization. + * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack + * The request stack. * @param string $theme_name * (optional) The name of the theme for which to construct the registry. */ - public function __construct($root, CacheBackendInterface $cache, LockBackendInterface $lock, ModuleHandlerInterface $module_handler, ThemeHandlerInterface $theme_handler, ThemeInitializationInterface $theme_initialization, $theme_name = NULL) { + public function __construct($root, CacheBackendInterface $cache, LockBackendInterface $lock, ModuleHandlerInterface $module_handler, ThemeHandlerInterface $theme_handler, ThemeInitializationInterface $theme_initialization, RequestStack $request_stack, $theme_name = NULL) { $this->root = $root; $this->cache = $cache; $this->lock = $lock; @@ -163,6 +173,7 @@ public function __construct($root, CacheBackendInterface $cache, LockBackendInte $this->themeName = $theme_name; $this->themeHandler = $theme_handler; $this->themeInitialization = $theme_initialization; + $this->requestStack = $request_stack; } /** @@ -236,7 +247,7 @@ public function get() { public function getRuntime() { $this->init($this->themeName); if (!isset($this->runtimeRegistry)) { - $this->runtimeRegistry = new ThemeRegistry('theme_registry:runtime:' . $this->theme->getName(), $this->cache, $this->lock, array('theme_registry'), $this->moduleHandler->isLoaded()); + $this->runtimeRegistry = new ThemeRegistry('theme_registry:runtime:' . $this->theme->getName(), $this->cache, $this->lock, $this->requestStack, array('theme_registry'), $this->moduleHandler->isLoaded()); } return $this->runtimeRegistry; } diff --git a/core/lib/Drupal/Core/Utility/ThemeRegistry.php b/core/lib/Drupal/Core/Utility/ThemeRegistry.php index c5e6055..6a51000 100644 --- a/core/lib/Drupal/Core/Utility/ThemeRegistry.php +++ b/core/lib/Drupal/Core/Utility/ThemeRegistry.php @@ -12,6 +12,7 @@ use Drupal\Core\Cache\CacheCollector; use Drupal\Core\DestructableInterface; use Drupal\Core\Lock\LockBackendInterface; +use Symfony\Component\HttpFoundation\RequestStack; /** * Builds the run-time theme registry. @@ -29,15 +30,33 @@ class ThemeRegistry extends CacheCollector implements DestructableInterface { * This is only allowed if all modules and the request method is GET. _theme() * should be very rarely called on POST requests and this avoids polluting * the runtime cache. + * + * @var bool */ - protected $persistable; + protected $persistable = NULL; /** * The complete theme registry array. + * + * @var array */ protected $completeRegistry; /** + * Whether all modules have already been loaded. + * + * @var bool + */ + protected $modulesLoaded; + + /** + * The theme object representing the active theme for this registry. + * + * @var \Drupal\Core\Theme\ActiveTheme + */ + protected $theme; + + /** * Constructs a ThemeRegistry object. * * @param string $cid @@ -46,43 +65,67 @@ class ThemeRegistry extends CacheCollector implements DestructableInterface { * The cache backend. * @param \Drupal\Core\Lock\LockBackendInterface $lock * The lock backend. + * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack + * The request stack. * @param array $tags * (optional) The tags to specify for the cache item. * @param bool $modules_loaded * Whether all modules have already been loaded. */ - function __construct($cid, CacheBackendInterface $cache, LockBackendInterface $lock, $tags = array(), $modules_loaded = FALSE) { - $this->cid = $cid; + function __construct($cid, CacheBackendInterface $cache, LockBackendInterface $lock, RequestStack $request_stack, $tags = array(), $modules_loaded = FALSE) { $this->cache = $cache; $this->lock = $lock; $this->tags = $tags; - $this->persistable = $modules_loaded && \Drupal::hasRequest() && \Drupal::request()->isMethod('GET'); - - // @todo: Implement lazyload. - $this->cacheLoaded = TRUE; + $this->cid = $cid; + $this->modulesLoaded = $modules_loaded; + $this->requestStack = $request_stack; + } - if ($this->persistable && $cached = $this->cache->get($this->cid)) { - $this->storage = $cached->data; + /** + * Whether the partial registry can be persisted to the cache. + * + * @return bool + */ + protected function isPersistable() { + if ($this->persistable === NULL) { + $this->persistable = $this->modulesLoaded && $this->requestStack->getCurrentRequest() !== NULL && $this->requestStack->getCurrentRequest()->isMethod('GET'); } - else { - // If there is no runtime cache stored, fetch the full theme registry, - // but then initialize each value to NULL. This allows offsetExists() - // to function correctly on non-registered theme hooks without triggering - // a call to resolveCacheMiss(). - $this->storage = $this->initializeRegistry(); - foreach (array_keys($this->storage) as $key) { - $this->persist($key); + + return $this->persistable; + } + + /** + * Gets the storage. + * + * @return array + */ + protected function getStorage() { + if (!$this->storage) { + if ($this->isPersistable() && $cached = $this->cache->get($this->getCid())) { + $this->storage = $cached->data; + } + else { + // If there is no runtime cache stored, fetch the full theme registry, + // but then initialize each value to NULL. This allows offsetExists() + // to function correctly on non-registered theme hooks without + // triggering a call to resolveCacheMiss(). + $this->storage = $this->initializeRegistry(); + foreach (array_keys($this->storage) as $key) { + $this->persist($key); + } + // RegistryTest::testRaceCondition() ensures that the cache entry is + // written when the theme registry is actually used. + $this->updateCache(); } - // RegistryTest::testRaceCondition() ensures that the cache entry is - // written on the initial construction of the theme registry. - $this->updateCache(); } + + return $this->storage; } /** * Initializes the full theme registry. * - * @return + * @return mixed[] * An array with the keys of the full theme registry, but the values * initialized to NULL. */ @@ -101,13 +144,18 @@ public function has($key) { // are not registered, just check the existence of the key in the registry. // Use array_key_exists() here since a NULL value indicates that the theme // hook exists but has not yet been requested. - return array_key_exists($key, $this->storage); + return array_key_exists($key, $this->getStorage()); } /** * {@inheritdoc} */ public function get($key) { + // When storage is not set get the storage. + if (!$this->storage) { + $this->getStorage(); + } + // If the offset is set but empty, it is a registered theme hook that has // not yet been requested. Offsets that do not exist at all were not // registered in hook_theme(). @@ -126,34 +174,35 @@ public function resolveCacheMiss($key) { if (!isset($this->completeRegistry)) { $this->completeRegistry = \Drupal::service('theme.registry')->get(); } - $this->storage[$key] = $this->completeRegistry[$key]; - if ($this->persistable) { + $storage = $this->getStorage(); + $storage[$key] = $this->completeRegistry[$key]; + if ($this->isPersistable()) { $this->persist($key); } - return $this->storage[$key]; + return $storage[$key]; } /** * {@inheritdoc} */ protected function updateCache($lock = TRUE) { - if (!$this->persistable) { + if (!$this->isPersistable()) { return; } // @todo: Is the custom implementation necessary? $data = array(); foreach ($this->keysToPersist as $offset => $persist) { if ($persist) { - $data[$offset] = $this->storage[$offset]; + $data[$offset] = $this->getStorage()[$offset]; } } if (empty($data)) { return; } - $lock_name = $this->cid . ':' . __CLASS__; + $lock_name = $this->getCid() . ':' . __CLASS__; if (!$lock || $this->lock->acquire($lock_name)) { - if ($cached = $this->cache->get($this->cid)) { + if ($cached = $this->cache->get($this->getCid())) { // Use array merge instead of union so that filled in values in $data // overwrite empty values in the current cache. $data = array_merge($cached->data, $data); @@ -162,7 +211,7 @@ protected function updateCache($lock = TRUE) { $registry = $this->initializeRegistry(); $data = array_merge($registry, $data); } - $this->cache->set($this->cid, $data, Cache::PERMANENT, $this->tags); + $this->cache->set($this->getCid(), $data, Cache::PERMANENT, $this->tags); if ($lock) { $this->lock->release($lock_name); } diff --git a/core/modules/system/src/Tests/Theme/RegistryTest.php b/core/modules/system/src/Tests/Theme/RegistryTest.php index 92d52b4..41953f2 100644 --- a/core/modules/system/src/Tests/Theme/RegistryTest.php +++ b/core/modules/system/src/Tests/Theme/RegistryTest.php @@ -40,12 +40,14 @@ function testRaceCondition() { // entry to be written in __construct(). $cache = \Drupal::cache(); $lock_backend = \Drupal::lock(); - $registry = new ThemeRegistry($cid, $cache, $lock_backend, array('theme_registry'), $this->container->get('module_handler')->isLoaded()); - - $this->assertTrue(\Drupal::cache()->get($cid), 'Cache entry was created.'); + $registry = new ThemeRegistry($cid, $cache, $lock_backend, \Drupal::service('request_stack'), array('theme_registry'), $this->container->get('module_handler')->isLoaded()); + // Test that lazy loading works. + $this->assertFalse(\Drupal::cache()->get($cid), 'Cache entry was not created by default.'); // Trigger a cache miss for an offset. $this->assertTrue($registry->get('theme_test_template_test'), 'Offset was returned correctly from the theme registry.'); + // After registry has been actually used we should have a cache entry. + $this->assertTrue(\Drupal::cache()->get($cid), 'Cache entry was created.'); // This will cause the ThemeRegistry class to write an updated version of // the cache entry when it is destroyed, usually at the end of the request. // Before that happens, manually delete the cache entry we created earlier @@ -60,7 +62,7 @@ function testRaceCondition() { // Create a new instance of the class. Confirm that both the offset // requested previously, and one that has not yet been requested are both // available. - $registry = new ThemeRegistry($cid, $cache, $lock_backend, array('theme_registry'), $this->container->get('module_handler')->isLoaded()); + $registry = new ThemeRegistry($cid, $cache, $lock_backend, \Drupal::service('request_stack'), array('theme_registry'), $this->container->get('module_handler')->isLoaded()); $this->assertTrue($registry->get('theme_test_template_test'), 'Offset was returned correctly from the theme registry'); $this->assertTrue($registry->get('theme_test_template_test_2'), 'Offset was returned correctly from the theme registry'); } @@ -71,12 +73,13 @@ function testRaceCondition() { public function testMultipleSubThemes() { $theme_handler = \Drupal::service('theme_handler'); $theme_handler->install(['test_basetheme', 'test_subtheme', 'test_subsubtheme']); + $request_stack = \Drupal::service('request_stack'); - $registry_subsub_theme = new Registry(\Drupal::root(), \Drupal::cache(), \Drupal::lock(), \Drupal::moduleHandler(), $theme_handler, \Drupal::service('theme.initialization'), 'test_subsubtheme'); + $registry_subsub_theme = new Registry(\Drupal::root(), \Drupal::cache(), \Drupal::lock(), \Drupal::moduleHandler(), $theme_handler, \Drupal::service('theme.initialization'), $request_stack, 'test_subsubtheme'); $registry_subsub_theme->setThemeManager(\Drupal::theme()); - $registry_sub_theme = new Registry(\Drupal::root(), \Drupal::cache(), \Drupal::lock(), \Drupal::moduleHandler(), $theme_handler, \Drupal::service('theme.initialization'), 'test_subtheme'); + $registry_sub_theme = new Registry(\Drupal::root(), \Drupal::cache(), \Drupal::lock(), \Drupal::moduleHandler(), $theme_handler, \Drupal::service('theme.initialization'), $request_stack, 'test_subtheme'); $registry_sub_theme->setThemeManager(\Drupal::theme()); - $registry_base_theme = new Registry(\Drupal::root(), \Drupal::cache(), \Drupal::lock(), \Drupal::moduleHandler(), $theme_handler, \Drupal::service('theme.initialization'), 'test_basetheme'); + $registry_base_theme = new Registry(\Drupal::root(), \Drupal::cache(), \Drupal::lock(), \Drupal::moduleHandler(), $theme_handler, \Drupal::service('theme.initialization'), $request_stack, 'test_basetheme'); $registry_base_theme->setThemeManager(\Drupal::theme()); $preprocess_functions = $registry_subsub_theme->get()['theme_test_template_test']['preprocess functions']; @@ -157,7 +160,7 @@ public function testThemeRegistryAlterByTheme() { $theme_handler->install(['test_theme']); $theme_handler->setDefault('test_theme'); - $registry = new Registry(\Drupal::root(), \Drupal::cache(), \Drupal::lock(), \Drupal::moduleHandler(), $theme_handler, \Drupal::service('theme.initialization'), 'test_theme'); + $registry = new Registry(\Drupal::root(), \Drupal::cache(), \Drupal::lock(), \Drupal::moduleHandler(), $theme_handler, \Drupal::service('theme.initialization'), \Drupal::service('request_stack'), 'test_theme'); $registry->setThemeManager(\Drupal::theme()); $this->assertEqual('value', $registry->get()['theme_test_template_test']['variables']['additional']); } diff --git a/core/tests/Drupal/Tests/Core/Theme/RegistryTest.php b/core/tests/Drupal/Tests/Core/Theme/RegistryTest.php index 0cb5a25..b7beebd 100644 --- a/core/tests/Drupal/Tests/Core/Theme/RegistryTest.php +++ b/core/tests/Drupal/Tests/Core/Theme/RegistryTest.php @@ -67,6 +67,13 @@ class RegistryTest extends UnitTestCase { protected $themeManager; /** + * The request stack. + * + * @var \Symfony\Component\HttpFoundation\RequestStack|\PHPUnit_Framework_MockObject_MockObject + */ + protected $requestStack; + + /** * {@inheritdoc} */ protected function setUp() { @@ -78,6 +85,7 @@ protected function setUp() { $this->themeHandler = $this->getMock('Drupal\Core\Extension\ThemeHandlerInterface'); $this->themeInitialization = $this->getMock('Drupal\Core\Theme\ThemeInitializationInterface'); $this->themeManager = $this->getMock('Drupal\Core\Theme\ThemeManagerInterface'); + $this->requestStack = $this->getMock('Symfony\Component\HttpFoundation\RequestStack'); $this->setupTheme(); } @@ -134,7 +142,7 @@ public function testGetRegistryForModule() { } protected function setupTheme($theme_name = NULL) { - $this->registry = new TestRegistry($this->root, $this->cache, $this->lock, $this->moduleHandler, $this->themeHandler, $this->themeInitialization, $theme_name); + $this->registry = new TestRegistry($this->root, $this->cache, $this->lock, $this->moduleHandler, $this->themeHandler, $this->themeInitialization, $this->requestStack, $theme_name); $this->registry->setThemeManager($this->themeManager); }