Commit 3cda8309 authored by alexpott's avatar alexpott

Issue #1886448 by dawehner, sun, Berdir, ParisLiakos: Rewrite the theme...

Issue #1886448 by dawehner, sun, Berdir, ParisLiakos: Rewrite the theme registry into a proper service.
parent ac495597
......@@ -641,6 +641,11 @@ services:
class: Zend\Feed\Writer\Extension\Threading\Renderer\Entry
feed.writer.wellformedwebrendererentry:
class: Zend\Feed\Writer\Extension\WellFormedWeb\Renderer\Entry
theme.registry:
class: Drupal\Core\Theme\Registry
arguments: ['@cache.cache', '@lock', '@module_handler']
tags:
- { name: needs_destruction }
authentication:
class: Drupal\Core\Authentication\AuthenticationManager
authentication.cookie:
......
......@@ -410,6 +410,13 @@ function install_begin_request(&$install_state) {
// implementation here.
$container->register('lock', 'Drupal\Core\Lock\NullLockBackend');
$container
->register('theme.registry', 'Drupal\Core\Theme\Registry')
->addArgument(new Reference('cache.cache'))
->addArgument(new Reference('lock'))
->addArgument(new Reference('module_handler'))
->addTag('needs_destruction');
// Register a module handler for managing enabled modules.
$container
->register('module_handler', 'Drupal\Core\Extension\ModuleHandler');
......
This diff is collapsed.
......@@ -91,6 +91,10 @@ function _drupal_maintenance_theme() {
$ancestor = $themes[$ancestor]->base_theme;
}
_drupal_theme_initialize($themes[$theme], array_reverse($base_theme), '_theme_load_offline_registry');
_drupal_theme_initialize($themes[$theme], array_reverse($base_theme));
// Prime the theme registry.
// @todo Remove global theme variables.
Drupal::service('theme.registry');
// These CSS files are normally added by system_page_build(), except
// system.maintenance.css. When the database is inactive, it's not called so
......@@ -98,13 +102,6 @@ function _drupal_maintenance_theme() {
drupal_add_library('system', 'normalize');
}
/**
* Builds the registry when the site needs to bypass any database calls.
*/
function _theme_load_offline_registry($theme, $base_theme = NULL, $theme_engine = NULL) {
return _theme_build_registry($theme, $base_theme, $theme_engine);
}
/**
* Returns HTML for a list of maintenance tasks to perform.
*
......
......@@ -458,6 +458,10 @@ function update_prepare_d8_bootstrap() {
new Settings($settings);
$kernel = new DrupalKernel('update', drupal_classloader(), FALSE);
$kernel->boot();
// Clear the D7 caches, to ensure that for example the theme_registry does not
// take part in the upgrade process.
Drupal::cache('cache')->deleteAll();
}
/**
......
This diff is collapsed.
......@@ -8,16 +8,19 @@
namespace Drupal\Core\Utility;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Cache\CacheCollector;
use Drupal\Core\DestructableInterface;
use Drupal\Core\Lock\LockBackendInterface;
/**
* Builds the run-time theme registry.
*
* Extends CacheArray to allow the theme registry to be accessed as a
* A cache collector to allow the theme registry to be accessed as a
* complete registry, while internally caching only the parts of the registry
* that are actually in use on the site. On cache misses the complete
* theme registry is loaded and used to update the run-time cache.
*/
class ThemeRegistry extends CacheArray {
class ThemeRegistry extends CacheCollector implements DestructableInterface {
/**
* Whether the partial registry can be persisted to the cache.
......@@ -38,35 +41,42 @@ class ThemeRegistry extends CacheArray {
*
* @param string $cid
* The cid for the array being cached.
* @param string $bin
* The bin to cache the array.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache
* The cache backend.
* @param \Drupal\Core\Lock\LockBackendInterface $lock
* The lock backend.
* @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, $bin, $tags, $modules_loaded = FALSE) {
function __construct($cid, CacheBackendInterface $cache, LockBackendInterface $lock, $tags = array(), $modules_loaded = FALSE) {
$this->cid = $cid;
$this->bin = $bin;
$this->cache = $cache;
$this->lock = $lock;
$this->tags = $tags;
$request = \Drupal::request();
$this->persistable = $modules_loaded && $request->isMethod('GET');
if ($this->persistable && $cached = cache($this->bin)->get($this->cid)) {
$data = $cached->data;
// @todo: Implement lazyload.
$this->cacheLoaded = TRUE;
if ($this->persistable && $cached = $this->cache->get($this->cid)) {
$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().
$data = $this->initializeRegistry();
if ($this->persistable) {
$this->set($data);
$this->storage = $this->initializeRegistry();
foreach (array_keys($this->storage) as $key) {
$this->persist($key);
}
// RegistryTest::testRaceCondition() ensures that the cache entry is
// written on the initial construction of the theme registry.
$this->updateCache();
}
$this->storage = $data;
}
/**
......@@ -77,58 +87,73 @@ function __construct($cid, $bin, $tags, $modules_loaded = FALSE) {
* initialized to NULL.
*/
function initializeRegistry() {
$this->completeRegistry = theme_get_registry();
// @todo DIC this.
$this->completeRegistry = \Drupal::service('theme.registry')->get();
return array_fill_keys(array_keys($this->completeRegistry), NULL);
}
/**
* Overrides CacheArray::offsetExists().
* {@inheritdoc}
*/
public function offsetExists($offset) {
public function has($key) {
// Since the theme registry allows for theme hooks to be requested that
// 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($offset, $this->storage);
return array_key_exists($key, $this->storage);
}
/**
* Overrides CacheArray::offsetGet().
* {@inheritdoc}
*/
public function offsetGet($offset) {
public function get($key) {
// 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().
if (isset($this->storage[$offset])) {
return $this->storage[$offset];
if (isset($this->storage[$key])) {
return $this->storage[$key];
}
elseif (array_key_exists($offset, $this->storage)) {
return $this->resolveCacheMiss($offset);
elseif (array_key_exists($key, $this->storage)) {
return $this->resolveCacheMiss($key);
}
}
/**
* Implements CacheArray::resolveCacheMiss().
* {@inheritdoc}
*/
public function resolveCacheMiss($offset) {
public function resolveCacheMiss($key) {
if (!isset($this->completeRegistry)) {
$this->completeRegistry = theme_get_registry();
$this->completeRegistry = \Drupal::service('theme.registry')->get();
}
$this->storage[$offset] = $this->completeRegistry[$offset];
$this->storage[$key] = $this->completeRegistry[$key];
if ($this->persistable) {
$this->persist($offset);
$this->persist($key);
}
return $this->storage[$offset];
return $this->storage[$key];
}
/**
* Overrides CacheArray::set().
* {@inheritdoc}
*/
public function set($data, $lock = TRUE) {
$lock_name = $this->cid . ':' . $this->bin;
if (!$lock || lock()->acquire($lock_name)) {
if ($cached = cache($this->bin)->get($this->cid)) {
protected function updateCache($lock = TRUE) {
if (!$this->persistable) {
return;
}
// @todo: Is the custom implementation necessary?
$data = array();
foreach ($this->keysToPersist as $offset => $persist) {
if ($persist) {
$data[$offset] = $this->storage[$offset];
}
}
if (empty($data)) {
return;
}
$lock_name = $this->cid . ':' . __CLASS__;
if (!$lock || $this->lock->acquire($lock_name)) {
if ($cached = $this->cache->get($this->cid)) {
// 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);
......@@ -137,10 +162,11 @@ public function set($data, $lock = TRUE) {
$registry = $this->initializeRegistry();
$data = array_merge($registry, $data);
}
cache($this->bin)->set($this->cid, $data, CacheBackendInterface::CACHE_PERMANENT, $this->tags);
$this->cache->set($this->cid, $data, CacheBackendInterface::CACHE_PERMANENT, $this->tags);
if ($lock) {
lock()->release($lock_name);
$this->lock->release($lock_name);
}
}
}
}
......@@ -35,17 +35,19 @@ public static function getInfo() {
* Tests the behavior of the theme registry class.
*/
function testRaceCondition() {
$_SERVER['REQUEST_METHOD'] = 'GET';
\Drupal::request()->setMethod('GET');
$cid = 'test_theme_registry';
// Directly instantiate the theme registry, this will cause a base cache
// entry to be written in __construct().
$registry = new ThemeRegistry($cid, 'cache', array('theme_registry' => TRUE), $this->container->get('module_handler')->isLoaded());
$cache = \Drupal::cache('cache');
$lock_backend = \Drupal::lock();
$registry = new ThemeRegistry($cid, $cache, $lock_backend, array('theme_registry' => TRUE), $this->container->get('module_handler')->isLoaded());
$this->assertTrue(cache()->get($cid), 'Cache entry was created.');
// Trigger a cache miss for an offset.
$this->assertTrue($registry['theme_test_template_test'], 'Offset was returned correctly from the theme registry.');
$this->assertTrue($registry->get('theme_test_template_test'), 'Offset was returned correctly from the theme registry.');
// 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
......@@ -53,15 +55,15 @@ function testRaceCondition() {
cache()->delete($cid);
// Destroy the class so that it triggers a cache write for the offset.
unset($registry);
$registry->destruct();
$this->assertTrue(cache()->get($cid), 'Cache entry was created.');
// 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', array('theme_registry' => TRUE));
$this->assertTrue($registry['theme_test_template_test'], 'Offset was returned correctly from the theme registry');
$this->assertTrue($registry['theme_test_template_test_2'], 'Offset was returned correctly from the theme registry');
$registry = new ThemeRegistry($cid, $cache, $lock_backend, array('theme_registry' => TRUE), $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');
}
}
......@@ -250,15 +250,9 @@ function testClassLoading() {
* Tests drupal_find_theme_templates().
*/
public function testFindThemeTemplates() {
$cache = array();
// Prime the theme cache.
foreach (\Drupal::moduleHandler()->getImplementations('theme') as $module) {
_theme_process_registry($cache, $module, 'module', $module, drupal_get_path('module', $module));
}
$templates = drupal_find_theme_templates($cache, '.html.twig', drupal_get_path('theme', 'test_theme'));
$this->assertEqual($templates['node__1']['template'], 'node--1', 'Template node--1.html.twig was found in test_theme.');
$registry = $this->container->get('theme.registry')->get();
$templates = drupal_find_theme_templates($registry, '.html.twig', drupal_get_path('theme', 'test_theme'));
$this->assertEqual($templates['node__1']['template'], 'node--1', 'Template node--1.tpl.twig was found in test_theme.');
}
/**
......
......@@ -41,11 +41,7 @@ function testTwigDebugMarkup() {
$this->rebuildContainer();
$this->resetAll();
$cache = array();
// Prime the theme cache.
foreach (\Drupal::moduleHandler()->getImplementations('theme') as $module) {
_theme_process_registry($cache, $module, 'module', $module, drupal_get_path('module', $module));
}
$cache = $this->container->get('theme.registry')->get();
// Create array of Twig templates.
$templates = drupal_find_theme_templates($cache, $extension, drupal_get_path('theme', 'test_theme'));
$templates += drupal_find_theme_templates($cache, $extension, drupal_get_path('module', 'node'));
......
......@@ -82,18 +82,24 @@ function testTwigCacheOverride() {
->set('default', 'test_theme')
->save();
$cache = array();
// Prime the theme cache.
foreach (\Drupal::moduleHandler()->getImplementations('theme') as $module) {
_theme_process_registry($cache, $module, 'module', $module, drupal_get_path('module', $module));
}
// Unset the global variables, so \Drupal\Core\Theme\Registry::init() fires
// drupal_theme_initialize, which fills up the global variables properly
// and chosen the current active theme.
unset($GLOBALS['theme_info']);
unset($GLOBALS['theme']);
// Reset the theme registry, so that the new theme is used.
$this->container->set('theme.registry', NULL);
// Load array of Twig templates.
$templates = drupal_find_theme_templates($cache, $extension, drupal_get_path('theme', 'test_theme'));
$registry = $this->container->get('theme.registry');
$registry->reset();
$templates = $registry->getRuntime();
// Get the template filename and the cache filename for
// theme_test.template_test.html.twig.
$template_filename = $templates['theme_test_template_test']['path'] . '/' . $templates['theme_test_template_test']['template'] . $extension;
$info = $templates->get('theme_test_template_test');
$template_filename = $info['path'] . '/' . $info['template'] . $extension;
$cache_filename = $this->container->get('twig')->getCacheFilename($template_filename);
// Navigate to the page and make sure the template gets cached.
......
......@@ -1362,7 +1362,7 @@ function hook_theme($existing, $type, $theme, $path) {
*
* The $theme_registry array is keyed by theme hook name, and contains the
* information returned from hook_theme(), as well as additional properties
* added by _theme_process_registry().
* added by \Drupal\Core\Theme\Registry::processExtension().
*
* For example:
* @code
......@@ -1385,7 +1385,7 @@ function hook_theme($existing, $type, $theme, $path) {
* The entire cache of theme registry information, post-processing.
*
* @see hook_theme()
* @see _theme_process_registry()
* @see \Drupal\Core\Theme\Registry::processExtension()
*/
function hook_theme_registry_alter(&$theme_registry) {
// Kill the next/previous forum topic navigation links.
......
......@@ -7,6 +7,7 @@
namespace Drupal\theme_test\EventSubscriber;
use Symfony\Component\DependencyInjection\ContainerAware;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
......@@ -14,7 +15,14 @@
/**
* Theme test subscriber for controller requests.
*/
class ThemeTestSubscriber implements EventSubscriberInterface {
class ThemeTestSubscriber extends ContainerAware implements EventSubscriberInterface {
/**
* The used container.
*
* @var \Symfony\Component\DependencyInjection\IntrospectableContainerInterface
*/
protected $container;
/**
* Generates themed output early in a page request.
......@@ -36,10 +44,18 @@ public function onRequest(GetResponseEvent $event) {
// returning output and theming the page as a whole.
$GLOBALS['theme_test_output'] = theme('more_link', array('url' => 'user', 'title' => 'Themed output generated in a KernelEvents::REQUEST listener'));
}
}
/**
* Ensures that the theme registry was not initialized.
*/
public function onView(GetResponseEvent $event) {
$request = $event->getRequest();
$current_path = $request->attributes->get('_system_path');
if (strpos($current_path, 'user/autocomplete') === 0) {
// Register a fake registry loading callback. If it gets called by
// theme_get_registry(), the registry has not been initialized yet.
_theme_registry_callback('_theme_test_load_registry', array());
if ($this->container->initialized('theme.registry')) {
throw new \Exception('registry initialized');
}
}
}
......@@ -48,6 +64,7 @@ public function onRequest(GetResponseEvent $event) {
*/
static function getSubscribedEvents() {
$events[KernelEvents::REQUEST][] = array('onRequest');
$events[KernelEvents::VIEW][] = array('onView', -1000);
return $events;
}
......
......@@ -42,6 +42,9 @@ function theme_test_theme($existing, $type, $theme, $path) {
$items['theme_test_function_template_override'] = array(
'variables' => array(),
);
$info['test_theme_not_existing_function'] = array(
'function' => 'test_theme_not_existing_function',
);
return $items;
}
......@@ -76,13 +79,6 @@ function theme_test_menu() {
);
return $items;
}
/**
* Fake registry loading callback.
*/
function _theme_test_load_registry() {
print 'registry initialized';
return array();
}
/**
* Custom theme callback.
......
......@@ -9,6 +9,7 @@
use Drupal\Component\Utility\String;
use Drupal\Core\Language\Language;
use Drupal\Core\Theme\Registry;
use Drupal\views\Plugin\views\area\AreaPluginBase;
use Drupal\views\ViewExecutable;
use Drupal\views\Plugin\views\PluginBase;
......@@ -1756,54 +1757,21 @@ public function buildOptionsForm(&$form, &$form_state) {
}
if (isset($GLOBALS['theme']) && $GLOBALS['theme'] == $this->theme) {
$this->theme_registry = theme_get_registry();
$this->theme_registry = \Drupal::service('theme.registry')->get();
$theme_engine = $GLOBALS['theme_engine'];
}
else {
$themes = list_themes();
$theme = $themes[$this->theme];
// Find all our ancestor themes and put them in an array.
$base_theme = array();
$ancestor = $this->theme;
while ($ancestor && isset($themes[$ancestor]->base_theme)) {
$ancestor = $themes[$ancestor]->base_theme;
$base_theme[] = $themes[$ancestor];
}
// The base themes should be initialized in the right order.
$base_theme = array_reverse($base_theme);
// This code is copied directly from _drupal_theme_initialize()
// @see _drupal_theme_initialize()
$theme_engine = NULL;
// Initialize the theme.
if (isset($theme->engine)) {
// Include the engine.
include_once DRUPAL_ROOT . '/' . $theme->owner;
$theme_engine = $theme->engine;
if (function_exists($theme_engine . '_init')) {
foreach ($base_theme as $base) {
call_user_func($theme_engine . '_init', $base);
}
call_user_func($theme_engine . '_init', $theme);
}
}
else {
// include non-engine theme files
foreach ($base_theme as $base) {
// Include the theme file or the engine.
if (!empty($base->owner)) {
include_once DRUPAL_ROOT . '/' . $base->owner;
}
}
// and our theme gets one too.
if (!empty($theme->owner)) {
include_once DRUPAL_ROOT . '/' . $theme->owner;
}
}
$this->theme_registry = _theme_load_registry($theme, $base_theme, $theme_engine);
$cache_theme = \Drupal::service('cache.theme');
$this->theme_registry = new Registry($cache_theme, \Drupal::lock(), \Drupal::moduleHandler(), $theme->name);
}
// If there's a theme engine involved, we also need to know its extension
......@@ -2071,15 +2039,9 @@ public function buildOptionsForm(&$form, &$form_state) {
* a templates rescan).
*/
public function rescanThemes($form, &$form_state) {
drupal_theme_rebuild();
// The 'Theme: Information' page is about to be shown again. That page
// analyzes the output of theme_get_registry(). However, this latter
// function uses an internal cache (which was initialized before we
// called drupal_theme_rebuild()) so it won't reflect the
// current state of our theme registry. The only way to clear that cache
// is to re-initialize the theme system:
unset($GLOBALS['theme']);
// Analyzes the data of the theme registry.
\Drupal::service('theme.registry')->reset();
drupal_theme_initialize();
$form_state['rerender'] = TRUE;
......
<?php
/**
* @file
* Contains \Drupal\Tests\Core\Theme\RegistryTest.
*/
namespace Drupal\Tests\Core\Theme;
use Drupal\Core\Theme\Registry;
use Drupal\Tests\UnitTestCase;
/**
* Tests the theme registry service.
*
* @group Drupal
* @group ${group}
*
* @see \Drupal\Core\Theme\Registry
*/
class RegistryTest extends UnitTestCase {
/**
* The tested theme registry.
*
* @var \Drupal\Tests\Core\Theme\TestRegistry
*/
protected $registry;
/**
* The mocked cache backend.
*
* @var \Drupal\Core\Cache\CacheBackendInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $cache;
/**
* The mocked lock backend.
*
* @var \Drupal\Core\Lock\LockBackendInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $lock;
/**
* The mocked module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $moduleHandler;
/**
* {@inheritdoc}
*/
public static function getInfo() {
return array(
'name' => 'Theme Registry',
'description' => 'Tests the theme registry.',
'group' => 'Theme',
);
}
/**
* {@inheritdoc}
*/
protected function setUp() {
$this->cache = $this->getMock('Drupal\Core\Cache\CacheBackendInterface');
$this->lock = $this->getMock('Drupal\Core\Lock\LockBackendInterface');
$this->moduleHandler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface');
$this->setupTheme();
}
/**
* Tests getting the theme registry defined by a module.
*/
public function testGetRegistryForModule() {
$this->setupTheme('test_theme');
$this->registry->setTheme((object) array(
'name' => 'test_theme',
'filename' => 'core/modules/system/tests/themes/test_theme/test_theme.theme',
));
$this->registry->setBaseThemes(array());
// Include the module so that hook_theme can be called.
include_once DRUPAL_ROOT . '/core/modules/system/tests/modules/theme_test/theme_test.module';
$this->moduleHandler->expects($this->once())
->method('getImplementations')
->with('theme')
->will($this->returnValue(array('theme_test')));
$registry = $this->registry->get();
// Ensure that the registry entries from the module are found.
$this->assertArrayHasKey('theme_test', $registry);
$this->assertArrayHasKey('theme_test_template_test', $registry);
$this->assertArrayHasKey('theme_test_template_test_2', $registry);
$this->assertArrayHasKey('theme_test_suggestion_provided', $registry);
$this->assertArrayHasKey('theme_test_specific_suggestions', $registry);
$this->assertArrayHasKey('theme_test_suggestions', $registry);
$this->assertArrayHasKey('theme_test_function_suggestions', $registry);
$this->assertArrayHasKey('theme_test_foo', $registry);
$this->assertArrayHasKey('theme_test_render_element', $registry);
$this->assertArrayHasKey('theme_test_render_element_children', $registry);
$this->assertArrayHasKey('theme_test_function_template_override', $registry);
$this->assertArrayNotHasKey('test_theme_not_existing_function', $registry);
$info = $registry['theme_test_function_suggestions'];
$this->assertEquals('module', $info['type']);
$this->assertEquals('core/modules/system/tests/modules/theme_test', $info['theme path']);
$this->assertEquals('theme_theme_test_function_suggestions', $info['function']);
$this->assertEquals(array(), $info['variables']);
}
protected function setupTheme($theme_name = NULL) {
$this->registry = new TestRegistry($this->cache, $this->lock, $this->moduleHandler, $theme_name);
}
}
class TestRegistry extends Registry {
public function setTheme(\stdClass $theme) {
$this->theme = $theme;
}
public function setBaseThemes(array $base_themes) {
$this->baseThemes = $base_themes;
}
protected function init($theme_name = NULL) {
}
protected function getPath($module) {
if ($module == 'theme_test') {
return 'core/modules/system/tests/modules/theme_test';
}
}
protected function listThemes() {
}
protected function initializeTheme() {
}
}
if (!defined('DRUPAL_ROOT')) {
define('DRUPAL_ROOT', dirname(dirname(substr(__DIR__, 0, -strlen(__NAMESPACE__)))));
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment