diff --git a/core/core.services.yml b/core/core.services.yml index 12f3a3c..0a2ff93 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -626,6 +626,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: diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc index 9159f71..36b6b14 100644 --- a/core/includes/install.core.inc +++ b/core/includes/install.core.inc @@ -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'); diff --git a/core/includes/theme.inc b/core/includes/theme.inc index 9a0d713..dd8e9be 100644 --- a/core/includes/theme.inc +++ b/core/includes/theme.inc @@ -143,10 +143,8 @@ function drupal_theme_initialize() { * the same information as the $theme object. It should be in * 'oldest first' order, meaning the top level of the chain will * be first. - * @param $registry_callback - * The callback to invoke to set the theme registry. */ -function _drupal_theme_initialize($theme, $base_theme = array(), $registry_callback = '_theme_load_registry') { +function _drupal_theme_initialize($theme, $base_theme = array()) { global $theme_info, $base_theme_info, $theme_engine, $theme_path; $theme_info = $theme; $base_theme_info = $base_theme; @@ -275,10 +273,6 @@ function _drupal_theme_initialize($theme, $base_theme = array(), $registry_callb // Always include Twig as the default theme engine. include_once DRUPAL_ROOT . '/core/themes/engines/twig/twig.engine'; - - if (isset($registry_callback)) { - _theme_registry_callback($registry_callback, array($theme, $base_theme, $theme_engine)); - } } /** @@ -299,354 +293,23 @@ function _drupal_theme_initialize($theme, $base_theme = array(), $registry_callb * Drupal\Core\Utility\ThemeRegistry class. */ function theme_get_registry($complete = TRUE) { - // Use the advanced drupal_static() pattern, since this is called very often. - static $drupal_static_fast; - if (!isset($drupal_static_fast)) { - $drupal_static_fast['registry'] = &drupal_static('theme_get_registry'); - } - $theme_registry = &$drupal_static_fast['registry']; - - // Initialize the theme, if this is called early in the bootstrap, or after - // static variables have been reset. - if (!is_array($theme_registry)) { - drupal_theme_initialize(); - $theme_registry = array(); - } - - $key = (int) $complete; - - if (!isset($theme_registry[$key])) { - list($callback, $arguments) = _theme_registry_callback(); - if (!$complete) { - $arguments[] = FALSE; - } - $theme_registry[$key] = call_user_func_array($callback, $arguments); - } - - return $theme_registry[$key]; -} - -/** - * Sets the callback that will be used by theme_get_registry(). - * - * @param $callback - * The name of the callback function. - * @param $arguments - * The arguments to pass to the function. - */ -function _theme_registry_callback($callback = NULL, array $arguments = array()) { - static $stored; - if (isset($callback)) { - $stored = array($callback, $arguments); - } - return $stored; -} - -/** - * Gets the theme_registry cache; if it doesn't exist, builds it. - * - * @param $theme - * The loaded $theme object as returned by list_themes(). - * @param $base_theme - * An array of loaded $theme objects representing the ancestor themes in - * oldest first order. - * @param $theme_engine - * The name of the theme engine. - * @param $complete - * Whether to load the complete theme registry or an instance of the - * Drupal\Core\Utility\ThemeRegistry class. - * - * @return - * The theme registry array, or an instance of the - * Drupal\Core\Utility\ThemeRegistry class. - */ -function _theme_load_registry($theme, $base_theme = NULL, $theme_engine = NULL, $complete = TRUE) { + $theme_registry = \Drupal::service('theme.registry'); if ($complete) { - // Check the theme registry cache; if it exists, use it. - $cached = cache()->get("theme_registry:$theme->name"); - if (isset($cached->data)) { - $registry = $cached->data; - } - else { - // If not, build one and cache it. - $registry = _theme_build_registry($theme, $base_theme, $theme_engine); - // Only persist this registry if all modules are loaded. This assures a - // complete set of theme hooks. - if (\Drupal::moduleHandler()->isLoaded()) { - _theme_save_registry($theme, $registry); - } - } - return $registry; + return $theme_registry->get(); } else { - return new ThemeRegistry('theme_registry:runtime:' . $theme->name, 'cache', array('theme_registry' => TRUE), \Drupal::moduleHandler()->isLoaded()); + return $theme_registry->getRuntime(); } } /** - * Writes the theme_registry cache into the database. - */ -function _theme_save_registry($theme, $registry) { - cache()->set("theme_registry:$theme->name", $registry, CacheBackendInterface::CACHE_PERMANENT, array('theme_registry' => TRUE)); -} - -/** * Forces the system to rebuild the theme registry. * * This function should be called when modules are added to the system, or when * a dynamic system needs to add more theme hooks. */ function drupal_theme_rebuild() { - drupal_static_reset('theme_get_registry'); - cache()->invalidateTags(array('theme_registry' => TRUE)); -} - -/** - * Process a single implementation of hook_theme(). - * - * @param $cache - * The theme registry that will eventually be cached; It is an associative - * array keyed by theme hooks, whose values are associative arrays describing - * the hook: - * - 'type': The passed-in $type. - * - 'theme path': The passed-in $path. - * - 'function': The name of the function generating output for this theme - * hook. Either defined explicitly in hook_theme() or, if neither - * 'function' nor 'template' is defined, then the default theme function - * name is used. The default theme function name is the theme hook prefixed - * by either 'theme_' for modules or '$name_' for everything else. If - * 'function' is defined, 'template' is not used. - * - 'template': The filename of the template generating output for this - * theme hook. The template is in the directory defined by the 'path' key of - * hook_theme() or defaults to "$path/templates". - * - 'variables': The variables for this theme hook as defined in - * hook_theme(). If there is more than one implementation and 'variables' - * is not specified in a later one, then the previous definition is kept. - * - 'render element': The renderable element for this theme hook as defined - * in hook_theme(). If there is more than one implementation and - * 'render element' is not specified in a later one, then the previous - * definition is kept. - * - 'preprocess functions': See theme() for detailed documentation. - * @param $name - * The name of the module, theme engine, base theme engine, theme or base - * theme implementing hook_theme(). - * @param $type - * One of 'module', 'theme_engine', 'base_theme_engine', 'theme', or - * 'base_theme'. Unlike regular hooks that can only be implemented by modules, - * each of these can implement hook_theme(). _theme_process_registry() is - * called in aforementioned order and new entries override older ones. For - * example, if a theme hook is both defined by a module and a theme, then the - * definition in the theme will be used. - * @param $theme - * The loaded $theme object as returned from list_themes(). - * @param $path - * The directory where $name is. For example, modules/system or - * themes/bartik. - * - * @see theme() - * @see hook_theme() - * @see list_themes() - */ -function _theme_process_registry(&$cache, $name, $type, $theme, $path) { - $result = array(); - - $hook_defaults = array( - 'variables' => TRUE, - 'render element' => TRUE, - 'pattern' => TRUE, - 'base hook' => TRUE, - ); - - $module_list = array_keys(\Drupal::moduleHandler()->getModuleList()); - - // Invoke the hook_theme() implementation, preprocess what is returned, and - // merge it into $cache. - $function = $name . '_theme'; - if (function_exists($function)) { - $result = $function($cache, $type, $theme, $path); - foreach ($result as $hook => $info) { - // When a theme or engine overrides a module's theme function - // $result[$hook] will only contain key/value pairs for information being - // overridden. Pull the rest of the information from what was defined by - // an earlier hook. - - // Fill in the type and path of the module, theme, or engine that - // implements this theme function. - $result[$hook]['type'] = $type; - $result[$hook]['theme path'] = $path; - - // If function and file are omitted, default to standard naming - // conventions. - if (!isset($info['template']) && !isset($info['function'])) { - $result[$hook]['function'] = ($type == 'module' ? 'theme_' : $name . '_') . $hook; - } - - if (isset($cache[$hook]['includes'])) { - $result[$hook]['includes'] = $cache[$hook]['includes']; - } - - // If the theme implementation defines a file, then also use the path - // that it defined. Otherwise use the default path. This allows - // system.module to declare theme functions on behalf of core .include - // files. - if (isset($info['file'])) { - $include_file = isset($info['path']) ? $info['path'] : $path; - $include_file .= '/' . $info['file']; - include_once DRUPAL_ROOT . '/' . $include_file; - $result[$hook]['includes'][] = $include_file; - } - - // If the default keys are not set, use the default values registered - // by the module. - if (isset($cache[$hook])) { - $result[$hook] += array_intersect_key($cache[$hook], $hook_defaults); - } - - // The following apply only to theming hooks implemented as templates. - if (isset($info['template'])) { - // Prepend the current theming path when none is set. - if (!isset($info['path'])) { - $result[$hook]['template'] = $path . '/templates/' . $info['template']; - } - } - - // Preprocess variables for all theming hooks, whether the hook is - // implemented as a template or as a function. Ensure they are arrays. - if (!isset($info['preprocess functions']) || !is_array($info['preprocess functions'])) { - $info['preprocess functions'] = array(); - $prefixes = array(); - if ($type == 'module') { - // Default variable preprocessor prefix. - $prefixes[] = 'template'; - // Add all modules so they can intervene with their own variable - // preprocessors. This allows them to provide variable preprocessors - // even if they are not the owner of the current hook. - $prefixes = array_merge($prefixes, $module_list); - } - elseif ($type == 'theme_engine' || $type == 'base_theme_engine') { - // Theme engines get an extra set that come before the normally - // named variable preprocessors. - $prefixes[] = $name . '_engine'; - // The theme engine registers on behalf of the theme using the - // theme's name. - $prefixes[] = $theme; - } - else { - // This applies when the theme manually registers their own variable - // preprocessors. - $prefixes[] = $name; - } - foreach ($prefixes as $prefix) { - // Only use non-hook-specific variable preprocessors for theming - // hooks implemented as templates. See theme(). - if (isset($info['template']) && function_exists($prefix . '_preprocess')) { - $info['preprocess functions'][] = $prefix . '_preprocess'; - } - if (function_exists($prefix . '_preprocess_' . $hook)) { - $info['preprocess functions'][] = $prefix . '_preprocess_' . $hook; - } - } - } - // Check for the override flag and prevent the cached variable - // preprocessors from being used. This allows themes or theme engines - // to remove variable preprocessors set earlier in the registry build. - if (!empty($info['override preprocess functions'])) { - // Flag not needed inside the registry. - unset($result[$hook]['override preprocess functions']); - } - elseif (isset($cache[$hook]['preprocess functions']) && is_array($cache[$hook]['preprocess functions'])) { - $info['preprocess functions'] = array_merge($cache[$hook]['preprocess functions'], $info['preprocess functions']); - } - $result[$hook]['preprocess functions'] = $info['preprocess functions']; - } - - // Merge the newly created theme hooks into the existing cache. - $cache = $result + $cache; - } - - // Let themes have variable preprocessors even if they didn't register a - // template. - if ($type == 'theme' || $type == 'base_theme') { - foreach ($cache as $hook => $info) { - // Check only if not registered by the theme or engine. - if (empty($result[$hook])) { - if (!isset($info['preprocess functions'])) { - $cache[$hook]['preprocess functions'] = array(); - } - // Only use non-hook-specific variable preprocessors for theme hooks - // implemented as templates. See theme(). - if (isset($info['template']) && function_exists($name . '_preprocess')) { - $cache[$hook]['preprocess functions'][] = $name . '_preprocess'; - } - if (function_exists($name . '_preprocess_' . $hook)) { - $cache[$hook]['preprocess functions'][] = $name . '_preprocess_' . $hook; - $cache[$hook]['theme path'] = $path; - } - // Ensure uniqueness. - $cache[$hook]['preprocess functions'] = array_unique($cache[$hook]['preprocess functions']); - } - } - } -} - -/** - * Builds the theme registry cache. - * - * @param $theme - * The loaded $theme object as returned by list_themes(). - * @param $base_theme - * An array of loaded $theme objects representing the ancestor themes in - * oldest first order. - * @param $theme_engine - * The name of the theme engine. - */ -function _theme_build_registry($theme, $base_theme, $theme_engine) { - $cache = array(); - // First, preprocess the theme hooks advertised by modules. This will - // serve as the basic registry. Since the list of enabled modules is the same - // regardless of the theme used, this is cached in its own entry to save - // building it for every theme. - if ($cached = cache()->get('theme_registry:build:modules')) { - $cache = $cached->data; - } - else { - foreach (\Drupal::moduleHandler()->getImplementations('theme') as $module) { - _theme_process_registry($cache, $module, 'module', $module, drupal_get_path('module', $module)); - } - // Only cache this registry if all modules are loaded. - if (\Drupal::moduleHandler()->isLoaded()) { - cache()->set("theme_registry:build:modules", $cache, CacheBackendInterface::CACHE_PERMANENT, array('theme_registry' => TRUE)); - } - } - - // Process each base theme. - foreach ($base_theme as $base) { - // If the base theme uses a theme engine, process its hooks. - $base_path = dirname($base->filename); - if ($theme_engine) { - _theme_process_registry($cache, $theme_engine, 'base_theme_engine', $base->name, $base_path); - } - _theme_process_registry($cache, $base->name, 'base_theme', $base->name, $base_path); - } - - // And then the same thing, but for the theme. - if ($theme_engine) { - _theme_process_registry($cache, $theme_engine, 'theme_engine', $theme->name, dirname($theme->filename)); - } - - // Finally, hooks provided by the theme itself. - _theme_process_registry($cache, $theme->name, 'theme', $theme->name, dirname($theme->filename)); - - // Let modules alter the registry. - drupal_alter('theme_registry', $cache); - - // Optimize the registry to not have empty arrays for functions. - foreach ($cache as $hook => $info) { - if (empty($info['preprocess functions'])) { - unset($cache[$hook]['preprocess functions']); - } - } - return $cache; + \Drupal::service('theme.registry')->reset(); } /** @@ -912,18 +575,19 @@ function theme($hook, $variables = array()) { static $default_attributes; // If called before all modules are loaded, we do not necessarily have a full // theme registry to work with, and therefore cannot process the theme - // request properly. See also _theme_load_registry(). + // request properly. See also \Drupal\Core\Theme\Registry::get(). if (!\Drupal::moduleHandler()->isLoaded() && !defined('MAINTENANCE_MODE')) { throw new Exception(t('theme() may not be called until all modules are loaded.')); } - $hooks = theme_get_registry(FALSE); + /** @var \Drupal\Core\Utility\ThemeRegistry $hooks */ + $hooks = \Drupal::service('theme.registry')->getRuntime(); // If an array of hook candidates were passed, use the first one that has an // implementation. if (is_array($hook)) { foreach ($hook as $candidate) { - if (isset($hooks[$candidate])) { + if ($hooks->has($candidate)) { break; } } @@ -935,16 +599,16 @@ function theme($hook, $variables = array()) { // If there's no implementation, check for more generic fallbacks. If there's // still no implementation, log an error and return an empty string. - if (!isset($hooks[$hook])) { + if (!$hooks->has($hook)) { // Iteratively strip everything after the last '__' delimiter, until an // implementation is found. while ($pos = strrpos($hook, '__')) { $hook = substr($hook, 0, $pos); - if (isset($hooks[$hook])) { + if ($hooks->has($hook)) { break; } } - if (!isset($hooks[$hook])) { + if (!$hooks->has($hook)) { // Only log a message when not trying theme suggestions ($hook being an // array). if (!isset($candidate)) { @@ -957,7 +621,7 @@ function theme($hook, $variables = array()) { } } - $info = $hooks[$hook]; + $info = $hooks->get($hook); global $theme_path; $temp = $theme_path; // point path_to_theme() to the currently used theme path: @@ -1030,8 +694,8 @@ function theme($hook, $variables = array()) { // 'node__article' as a suggestion via hook_theme_suggestions_HOOK_alter(), // enabling a theme to have an alternate template file for article nodes. foreach (array_reverse($suggestions) as $suggestion) { - if (isset($hooks[$suggestion])) { - $info = $hooks[$suggestion]; + if ($hooks->has($suggestion)) { + $info = $hooks->get($suggestion); break; } } @@ -1039,7 +703,7 @@ function theme($hook, $variables = array()) { // Invoke the variable preprocessors, if any. if (isset($info['base hook'])) { $base_hook = $info['base hook']; - $base_hook_info = $hooks[$base_hook]; + $base_hook_info = $hooks->get($base_hook); // Include files required by the base hook, since its variable preprocessors // might reside there. if (!empty($base_hook_info['includes'])) { diff --git a/core/includes/theme.maintenance.inc b/core/includes/theme.maintenance.inc index b86a25d..807566c 100644 --- a/core/includes/theme.maintenance.inc +++ b/core/includes/theme.maintenance.inc @@ -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 @@ -99,13 +103,6 @@ function _drupal_maintenance_theme() { } /** - * 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. * * @param $variables diff --git a/core/includes/update.inc b/core/includes/update.inc index 186255d..c616e08 100644 --- a/core/includes/update.inc +++ b/core/includes/update.inc @@ -458,6 +458,9 @@ function update_prepare_d8_bootstrap() { new Settings($settings); $kernel = new DrupalKernel('update', drupal_classloader(), FALSE); $kernel->boot(); + + // Remove the theme_registry cache entry from D7. + Drupal::cache('cache')->deleteAll(); } /** diff --git a/core/lib/Drupal/Core/Path/AliasWhitelist.php b/core/lib/Drupal/Core/Path/AliasWhitelist.php index 9e3a6a1..d43f83e 100644 --- a/core/lib/Drupal/Core/Path/AliasWhitelist.php +++ b/core/lib/Drupal/Core/Path/AliasWhitelist.php @@ -88,20 +88,20 @@ protected function loadMenuPathRoots() { /** * {@inheritdoc} */ - public function get($offset) { + public function get($key) { $this->lazyLoadCache(); // url() may be called with paths that are not represented by menu router // items such as paths that will be rewritten by hook_url_outbound_alter(). // Therefore internally TRUE is used to indicate whitelisted paths. FALSE is // used to indicate paths that have already been checked but are not // whitelisted, and NULL indicates paths that have not been checked yet. - if (isset($this->storage[$offset])) { - if ($this->storage[$offset]) { + if (isset($this->storage[$key])) { + if ($this->storage[$key]) { return TRUE; } } - elseif (array_key_exists($offset, $this->storage)) { - return $this->resolveCacheMiss($offset); + elseif (array_key_exists($key, $this->storage)) { + return $this->resolveCacheMiss($key); } } diff --git a/core/lib/Drupal/Core/Theme/Registry.php b/core/lib/Drupal/Core/Theme/Registry.php new file mode 100644 index 0000000..3893338 --- /dev/null +++ b/core/lib/Drupal/Core/Theme/Registry.php @@ -0,0 +1,599 @@ +registry. + */ +class Registry implements DestructableInterface { + + /** + * The theme object representing the active theme for this registry. + * + * @var object + */ + protected $theme; + + /** + * An array of base theme objects. + * + * @var array + */ + protected $baseThemes; + + /** + * The name of the theme engine of $theme. + * + * @var string + */ + protected $engine; + + /** + * The lock backend that should be used. + * + * @var \Drupal\Core\Lock\LockBackendInterface + */ + protected $lock; + + /** + * The complete theme registry. + * + * @var array + * An associative array keyed by theme hook names, whose values are + * associative arrays containing the aggregated hook definition: + * - type: The type of the extension the original theme hook originates + * from; e.g., 'module' for theme hook 'node' of Node module. + * - name: The name of the extension the original theme hook originates + * from; e.g., 'node' for theme hook 'node' of Node module. + * - theme path: The effective path_to_theme() during theme(), available as + * 'directory' variable in templates. + * functions, it should point to the respective theme. For templates, + * it should point to the directory that contains the template. + * - includes: (optional) An array of include files to load when the theme + * hook is executed by theme(). + * - file: (optional) A filename to add to 'includes', either prefixed with + * the value of 'path', or the path of the extension implementing + * hook_theme(). + * In case of a theme base hook, one of the following: + * - variables: An associative array whose keys are variable names and whose + * values are default values of the variables to use for this theme hook. + * - render element: A string denoting the name of the variable name, in + * which the render element for this theme hook is provided. + * In case of a theme template file: + * - path: The path to the template file to use. Defaults to the + * subdirectory 'templates' of the path of the extension implementing + * hook_theme(); e.g., 'core/modules/node/templates' for Node module. + * - template: The basename of the template file to use, without extension + * (as the extension is specific to the theme engine). The template file + * is in the directory defined by 'path'. + * - template_file: A full path and file name to a template file to use. + * Allows any extension to override the effective template file. + * - engine: The theme engine to use for the template file. + * In case of a theme function: + * - function: The function name to call to generate the output. + * For any registered theme hook, including theme hook suggestions: + * - preprocess: An array of theme variable preprocess callbacks to invoke + * before invoking final theme variable processors. + * - process: An array of theme variable process callbacks to invoke + * before invoking the actual theme function or template. + */ + protected $registry; + + /** + * The cache backend to use for the complete theme registry data. + * + * @var \Drupal\Core\Cache\CacheBackendInterface + */ + protected $cache; + + /** + * The module handler to use to load modules. + * + * @var \Drupal\Core\Extension\ModuleHandlerInterface + */ + protected $moduleHandler; + + /** + * The incomplete, runtime theme registry. + * + * @var \Drupal\Core\Utility\ThemeRegistry + */ + protected $runtimeRegistry; + + /** + * Constructs a \Drupal\Core\\Theme\Registry object. + * + * @param \Drupal\Core\Cache\CacheBackendInterface $cache + * The cache backend interface to use for the complete theme registry data. + * @param \Drupal\Core\Lock\LockBackendInterface $lock + * The lock backend. + * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler + * The module handler to use to load modules. + * @param string $theme_name + * (optional) The name of the theme for which to construct the registry. + */ + public function __construct(CacheBackendInterface $cache, LockBackendInterface $lock, ModuleHandlerInterface $module_handler, $theme_name = NULL) { + $this->cache = $cache; + $this->lock = $lock; + $this->moduleHandler = $module_handler; + $this->init($theme_name); + } + + /** + * Initializes a theme with a certain name. + * + * This function does to much magic, so it should be replaced by another + * services which holds the current active theme information. + * + * @param string $theme_name + * (optional) The name of the theme for which to construct the registry.+ + */ + protected function init($theme_name = NULL) { + // Unless instantiated for a specific theme, use globals. + if (!isset($theme_name)) { + // #1: The theme registry might get instantiated before the theme was + // initialized. Cope with that. + if (!isset($GLOBALS['theme_info']) || !isset($GLOBALS['theme'])) { + unset($this->runtimeRegistry); + unset($this->registry); + drupal_theme_initialize(); + } + // #2: The testing framework only cares for the global $theme variable at + // this point. Cope with that. + if ($GLOBALS['theme'] != $GLOBALS['theme_info']->name) { + unset($this->runtimeRegistry); + unset($this->registry); + $this->initializeTheme(); + } + $this->theme = $GLOBALS['theme_info']; + $this->baseThemes = $GLOBALS['base_theme_info']; + $this->engine = $GLOBALS['theme_engine']; + } + // Instead of the global theme, a specific theme was requested. + else { + // @see drupal_theme_initialize() + $themes = $this->listThemes(); + $this->theme = $themes[$theme_name]; + + // Find all base themes. + $this->baseThemes = array(); + $ancestor = $theme_name; + while ($ancestor && isset($themes[$ancestor]->base_theme)) { + $ancestor = $themes[$ancestor]->base_theme; + $this->baseThemes[] = $themes[$ancestor]; + if (!empty($themes[$ancestor]->owner)) { + include_once DRUPAL_ROOT . '/' . $themes[$ancestor]->owner; + } + } + $this->baseThemes = array_reverse($this->baseThemes); + + // @see _drupal_theme_initialize() + if (isset($this->theme->engine)) { + $this->engine = $this->theme->engine; + include_once DRUPAL_ROOT . '/' . $this->theme->owner; + if (function_exists($this->theme->engine . '_init')) { + foreach ($this->baseThemes as $base) { + call_user_func($this->theme->engine . '_init', $base); + } + call_user_func($this->theme->engine . '_init', $this->theme); + } + } + } + } + + /** + * Returns the complete theme registry from cache or rebuilds it. + * + * @return array + * The complete theme registry data array. + * + * @see Registry::$registry + */ + public function get() { + if (isset($this->registry)) { + return $this->registry; + } + if ($cache = $this->cache->get('theme_registry:' . $this->theme->name)) { + $this->registry = $cache->data; + } + else { + $this->registry = $this->build($this->theme, $this->baseThemes, $this->engine); + // Only persist it if all modules are loaded to ensure it is complete. + if ($this->moduleHandler->isLoaded()) { + $this->setCache(); + } + } + return $this->registry; + } + + /** + * Returns the incomplete, runtime theme registry. + * + * @return \Drupal\Core\Utility\ThemeRegistry + * A shared instance of the ThemeRegistry class, provides an ArrayObject + * that allows it to be accessed with array syntax and isset(), and is more + * lightweight than the full registry. + */ + public function getRuntime() { + if (!isset($this->runtimeRegistry)) { + $this->runtimeRegistry = new ThemeRegistry('theme_registry:runtime:' . $this->theme->name, $this->cache, $this->lock, array('theme_registry' => TRUE), $this->moduleHandler->isLoaded()); + } + return $this->runtimeRegistry; + } + + /** + * Persists the theme registry in the cache backend. + */ + protected function setCache() { + $this->cache->set('theme_registry:' . $this->theme->name, $this->registry, CacheBackendInterface::CACHE_PERMANENT, array('theme_registry' => TRUE)); + } + + /** + * Returns the base hook for a given hook suggestion. + * + * @param string $hook + * The name of a theme hook whose base hook to find. + * + * @return string|false + * The name of the base hook or FALSE. + */ + public function getBaseHook($hook) { + $base_hook = $hook; + // Iteratively strip everything after the last '__' delimiter, until a + // base hook definition is found. Recursive base hooks of base hooks are + // not supported, so the base hook must be an original implementation that + // points to a theme function or template. + while ($pos = strrpos($base_hook, '__')) { + $base_hook = substr($base_hook, 0, $pos); + if (isset($this->registry[$base_hook]['exists'])) { + break; + } + } + if ($pos !== FALSE && $base_hook !== $hook) { + return $base_hook; + } + return FALSE; + } + + /** + * Builds the theme registry cache. + * + * Theme hook definitions are collected in the following order: + * - Modules + * - Base theme engines + * - Base themes + * - Theme engine + * - Theme + * + * All theme hook definitions are essentially just collated and merged in the + * above order. However, various extension-specific default values and + * customizations are required; e.g., to record the effective file path for + * theme template. Therefore, this method first collects all extensions per + * type, and then dispatches the processing for each extension to + * processExtension(). + * + * After completing the collection, modules are allowed to alter it. Lastly, + * any derived and incomplete theme hook definitions that are hook suggestions + * for base hooks (e.g., 'block__node' for the base hook 'block') need to be + * determined based on the full registry and classified as 'base hook'. + * + * @see theme() + * @see hook_theme_registry_alter() + * + * @param string $theme + * The loaded $theme object as returned by list_themes(). + * @param array $base_theme + * An array of loaded $theme objects representing the ancestor themes in + * oldest first order. + * @param string $theme_engine + * The name of the theme engine. + * + * @return \Drupal\Core\Utility\ThemeRegistry + * The build theme registry. + */ + protected function build($theme, array $base_theme, $theme_engine) { + $cache = array(); + // First, preprocess the theme hooks advertised by modules. This will + // serve as the basic registry. Since the list of enabled modules is the + // same regardless of the theme used, this is cached in its own entry to + // save building it for every theme. + if ($cached = $this->cache->get('theme_registry:build:modules')) { + $cache = $cached->data; + } + else { + foreach ($this->moduleHandler->getImplementations('theme') as $module) { + $this->processExtension($cache, $module, 'module', $module, $this->getPath($module)); + } + // Only cache this registry if all modules are loaded. + if ($this->moduleHandler->isLoaded()) { + $this->cache->set("theme_registry:build:modules", $cache, CacheBackendInterface::CACHE_PERMANENT, array('theme_registry' => TRUE)); + } + } + + // Process each base theme. + foreach ($base_theme as $base) { + // If the base theme uses a theme engine, process its hooks. + $base_path = dirname($base->filename); + if ($theme_engine) { + $this->processExtension($cache, $theme_engine, 'base_theme_engine', $base->name, $base_path); + } + $this->processExtension($cache, $base->name, 'base_theme', $base->name, $base_path); + } + + // And then the same thing, but for the theme. + if ($theme_engine) { + $this->processExtension($cache, $theme_engine, 'theme_engine', $theme->name, dirname($theme->filename)); + } + + // Finally, hooks provided by the theme itself. + $this->processExtension($cache, $theme->name, 'theme', $theme->name, dirname($theme->filename)); + + // Let modules alter the registry. + $this->moduleHandler->alter('theme_registry', $cache); + + // @todo Implement more reduction of the theme registry entry. + // Optimize the registry to not have empty arrays for functions. + foreach ($cache as $hook => $info) { + if (empty($info['preprocess functions'])) { + unset($cache[$hook]['preprocess functions']); + } + } + $this->registry = $cache; + + return $this->registry; + } + + /** + * Process a single implementation of hook_theme(). + * + * @param $cache + * The theme registry that will eventually be cached; It is an associative + * array keyed by theme hooks, whose values are associative arrays + * describing the hook: + * - 'type': The passed-in $type. + * - 'theme path': The passed-in $path. + * - 'function': The name of the function generating output for this theme + * hook. Either defined explicitly in hook_theme() or, if neither + * 'function' nor 'template' is defined, then the default theme function + * name is used. The default theme function name is the theme hook + * prefixed by either 'theme_' for modules or '$name_' for everything + * else. If 'function' is defined, 'template' is not used. + * - 'template': The filename of the template generating output for this + * theme hook. The template is in the directory defined by the 'path' key + * of hook_theme() or defaults to "$path/templates". + * - 'variables': The variables for this theme hook as defined in + * hook_theme(). If there is more than one implementation and 'variables' + * is not specified in a later one, then the previous definition is kept. + * - 'render element': The renderable element for this theme hook as defined + * in hook_theme(). If there is more than one implementation and + * 'render element' is not specified in a later one, then the previous + * definition is kept. + * - 'preprocess functions': See theme() for detailed documentation. + * @param string $name + * The name of the module, theme engine, base theme engine, theme or base + * theme implementing hook_theme(). + * @param string $type + * One of 'module', 'theme_engine', 'base_theme_engine', 'theme', or + * 'base_theme'. Unlike regular hooks that can only be implemented by + * modules, each of these can implement hook_theme(). + * _theme_process_registry() is called in aforementioned order and new + * entries override older ones. For example, if a theme hook is both defined + * by a module and a theme, then the definition in the theme will be used. + * @param \stdClass $theme + * The loaded $theme object as returned from list_themes(). + * @param string $path + * The directory where $name is. For example, modules/system or + * themes/bartik. + * + * @see theme() + * @see hook_theme() + * @see list_themes() + */ + protected function processExtension(&$cache, $name, $type, $theme, $path) { + $result = array(); + + $hook_defaults = array( + 'variables' => TRUE, + 'render element' => TRUE, + 'pattern' => TRUE, + 'base hook' => TRUE, + ); + + $module_list = array_keys((array) $this->moduleHandler->getModuleList()); + + // Invoke the hook_theme() implementation, preprocess what is returned, and + // merge it into $cache. + $function = $name . '_theme'; + if (function_exists($function)) { + $result = $function($cache, $type, $theme, $path); + foreach ($result as $hook => $info) { + // When a theme or engine overrides a module's theme function + // $result[$hook] will only contain key/value pairs for information being + // overridden. Pull the rest of the information from what was defined by + // an earlier hook. + + // Fill in the type and path of the module, theme, or engine that + // implements this theme function. + $result[$hook]['type'] = $type; + $result[$hook]['theme path'] = $path; + + // If function and file are omitted, default to standard naming + // conventions. + if (!isset($info['template']) && !isset($info['function'])) { + $result[$hook]['function'] = ($type == 'module' ? 'theme_' : $name . '_') . $hook; + } + + if (isset($cache[$hook]['includes'])) { + $result[$hook]['includes'] = $cache[$hook]['includes']; + } + + // If the theme implementation defines a file, then also use the path + // that it defined. Otherwise use the default path. This allows + // system.module to declare theme functions on behalf of core .include + // files. + if (isset($info['file'])) { + $include_file = isset($info['path']) ? $info['path'] : $path; + $include_file .= '/' . $info['file']; + include_once DRUPAL_ROOT . '/' . $include_file; + $result[$hook]['includes'][] = $include_file; + } + + // If the default keys are not set, use the default values registered + // by the module. + if (isset($cache[$hook])) { + $result[$hook] += array_intersect_key($cache[$hook], $hook_defaults); + } + + // The following apply only to theming hooks implemented as templates. + if (isset($info['template'])) { + // Prepend the current theming path when none is set. + if (!isset($info['path'])) { + $result[$hook]['template'] = $path . '/templates/' . $info['template']; + } + } + + // Preprocess variables for all theming hooks, whether the hook is + // implemented as a template or as a function. Ensure they are arrays. + if (!isset($info['preprocess functions']) || !is_array($info['preprocess functions'])) { + $info['preprocess functions'] = array(); + $prefixes = array(); + if ($type == 'module') { + // Default variable preprocessor prefix. + $prefixes[] = 'template'; + // Add all modules so they can intervene with their own variable + // preprocessors. This allows them to provide variable preprocessors + // even if they are not the owner of the current hook. + $prefixes = array_merge($prefixes, $module_list); + } + elseif ($type == 'theme_engine' || $type == 'base_theme_engine') { + // Theme engines get an extra set that come before the normally + // named variable preprocessors. + $prefixes[] = $name . '_engine'; + // The theme engine registers on behalf of the theme using the + // theme's name. + $prefixes[] = $theme; + } + else { + // This applies when the theme manually registers their own variable + // preprocessors. + $prefixes[] = $name; + } + foreach ($prefixes as $prefix) { + // Only use non-hook-specific variable preprocessors for theming + // hooks implemented as templates. See theme(). + if (isset($info['template']) && function_exists($prefix . '_preprocess')) { + $info['preprocess functions'][] = $prefix . '_preprocess'; + } + if (function_exists($prefix . '_preprocess_' . $hook)) { + $info['preprocess functions'][] = $prefix . '_preprocess_' . $hook; + } + } + } + // Check for the override flag and prevent the cached variable + // preprocessors from being used. This allows themes or theme engines + // to remove variable preprocessors set earlier in the registry build. + if (!empty($info['override preprocess functions'])) { + // Flag not needed inside the registry. + unset($result[$hook]['override preprocess functions']); + } + elseif (isset($cache[$hook]['preprocess functions']) && is_array($cache[$hook]['preprocess functions'])) { + $info['preprocess functions'] = array_merge($cache[$hook]['preprocess functions'], $info['preprocess functions']); + } + $result[$hook]['preprocess functions'] = $info['preprocess functions']; + } + + // Merge the newly created theme hooks into the existing cache. + $cache = $result + $cache; + } + + // Let themes have variable preprocessors even if they didn't register a + // template. + if ($type == 'theme' || $type == 'base_theme') { + foreach ($cache as $hook => $info) { + // Check only if not registered by the theme or engine. + if (empty($result[$hook])) { + if (!isset($info['preprocess functions'])) { + $cache[$hook]['preprocess functions'] = array(); + } + // Only use non-hook-specific variable preprocessors for theme hooks + // implemented as templates. See theme(). + if (isset($info['template']) && function_exists($name . '_preprocess')) { + $cache[$hook]['preprocess functions'][] = $name . '_preprocess'; + } + if (function_exists($name . '_preprocess_' . $hook)) { + $cache[$hook]['preprocess functions'][] = $name . '_preprocess_' . $hook; + $cache[$hook]['theme path'] = $path; + } + // Ensure uniqueness. + $cache[$hook]['preprocess functions'] = array_unique($cache[$hook]['preprocess functions']); + } + } + } + } + + /** + * Invalidates theme registry caches. + * + * To be called when the list of enabled extensions is changed. + */ + public function reset() { + + // Reset the runtime registry. + if (isset($this->runtimeRegistry) && $this->runtimeRegistry instanceof ThemeRegistry) { + $this->runtimeRegistry->clear(); + } + $this->runtimeRegistry = NULL; + + $this->registry = NULL; + $this->cache->invalidateTags(array('theme_registry' => TRUE)); + return $this; + } + + /** + * {@inheritdoc} + */ + public function destruct() { + if (isset($this->runtimeRegistry)) { + $this->runtimeRegistry->destruct(); + } + } + + /** + * Wraps drupal_get_path(). + * + * @return string + */ + protected function getPath($module) { + return drupal_get_path('module', $module); + } + + /** + * Wraps list_themes(). + * + * @return array + */ + protected function listThemes() { + return list_themes(); + } + + /** + * Wraps drupal_theme_initialize(). + */ + protected function initializeTheme() { + return drupal_theme_initialize(); + } + +} diff --git a/core/lib/Drupal/Core/Utility/ThemeRegistry.php b/core/lib/Drupal/Core/Utility/ThemeRegistry.php index 9263e51..e09ed65 100644 --- a/core/lib/Drupal/Core/Utility/ThemeRegistry.php +++ b/core/lib/Drupal/Core/Utility/ThemeRegistry.php @@ -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,22 +41,28 @@ 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)) { + // @todo: Implement lazyload. + $this->cacheLoaded = TRUE; + + if ($this->persistable && $cached = $this->cache->get($this->cid)) { + $this->storage = $cached->data; $data = $cached->data; } else { @@ -61,12 +70,14 @@ function __construct($cid, $bin, $tags, $modules_loaded = FALSE) { // 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 +88,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 +163,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); } } } + } diff --git a/core/modules/locale/lib/Drupal/locale/LocaleLookup.php b/core/modules/locale/lib/Drupal/locale/LocaleLookup.php index 2003e4e..54b3495 100644 --- a/core/modules/locale/lib/Drupal/locale/LocaleLookup.php +++ b/core/modules/locale/lib/Drupal/locale/LocaleLookup.php @@ -82,10 +82,10 @@ public function __construct($langcode, $context, StringStorageInterface $string_ /** * {@inheritdoc} */ - protected function resolveCacheMiss($offset) { + protected function resolveCacheMiss($key) { $translation = $this->stringStorage->findTranslation(array( 'language' => $this->langcode, - 'source' => $offset, + 'source' => $key, 'context' => $this->context, )); @@ -96,19 +96,19 @@ protected function resolveCacheMiss($offset) { // We don't have the source string, update the {locales_source} table to // indicate the string is not translated. $this->stringStorage->createString(array( - 'source' => $offset, + 'source' => $key, 'context' => $this->context, 'version' => \Drupal::VERSION ))->addLocation('path', request_uri())->save(); $value = TRUE; } - $this->storage[$offset] = $value; + $this->storage[$key] = $value; // Disabling the usage of string caching allows a module to watch for // the exact list of strings used on a page. From a performance // perspective that is a really bad idea, so we have no user // interface for this. Be careful when turning this option off! if (\Drupal::config('locale.settings')->get('cache_strings')) { - $this->persist($offset); + $this->persist($key); } return $value; } diff --git a/core/modules/system/lib/Drupal/system/Tests/Theme/RegistryTest.php b/core/modules/system/lib/Drupal/system/Tests/Theme/RegistryTest.php index 5ab43ca..3e4d6af 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Theme/RegistryTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Theme/RegistryTest.php @@ -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'); } } diff --git a/core/modules/system/lib/Drupal/system/Tests/Theme/ThemeTest.php b/core/modules/system/lib/Drupal/system/Tests/Theme/ThemeTest.php index 9c21795..069960c 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Theme/ThemeTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Theme/ThemeTest.php @@ -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.'); } /** diff --git a/core/modules/system/lib/Drupal/system/Tests/Theme/TwigDebugMarkupTest.php b/core/modules/system/lib/Drupal/system/Tests/Theme/TwigDebugMarkupTest.php index 53d6a0b..3a26efe 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Theme/TwigDebugMarkupTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Theme/TwigDebugMarkupTest.php @@ -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')); diff --git a/core/modules/system/lib/Drupal/system/Tests/Theme/TwigSettingsTest.php b/core/modules/system/lib/Drupal/system/Tests/Theme/TwigSettingsTest.php index 958128c..fe65c8f 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Theme/TwigSettingsTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Theme/TwigSettingsTest.php @@ -82,14 +82,19 @@ 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::init() fires + // drupal_theme_initialize, which fills up the global variables properly and + // choosed 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. diff --git a/core/modules/system/system.api.php b/core/modules/system/system.api.php index 47665c5..ad29ada 100644 --- a/core/modules/system/system.api.php +++ b/core/modules/system/system.api.php @@ -1348,7 +1348,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::_theme_process_registry(). * * For example: * @code @@ -1371,7 +1371,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. diff --git a/core/modules/system/tests/modules/theme_test/lib/Drupal/theme_test/EventSubscriber/ThemeTestSubscriber.php b/core/modules/system/tests/modules/theme_test/lib/Drupal/theme_test/EventSubscriber/ThemeTestSubscriber.php index 56cbdaf..41129cc 100644 --- a/core/modules/system/tests/modules/theme_test/lib/Drupal/theme_test/EventSubscriber/ThemeTestSubscriber.php +++ b/core/modules/system/tests/modules/theme_test/lib/Drupal/theme_test/EventSubscriber/ThemeTestSubscriber.php @@ -7,6 +7,9 @@ namespace Drupal\theme_test\EventSubscriber; +use Drupal\Core\DependencyInjection\ContainerInjectionInterface; +use Symfony\Component\DependencyInjection\ContainerAware; +use Symfony\Component\DependencyInjection\IntrospectableContainerInterface; use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\HttpKernel\Event\GetResponseEvent; use Symfony\Component\EventDispatcher\EventSubscriberInterface; @@ -14,7 +17,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 +46,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 +66,7 @@ public function onRequest(GetResponseEvent $event) { */ static function getSubscribedEvents() { $events[KernelEvents::REQUEST][] = array('onRequest'); + $events[KernelEvents::VIEW][] = array('onView', -1000); return $events; } diff --git a/core/modules/system/tests/modules/theme_test/theme_test.module b/core/modules/system/tests/modules/theme_test/theme_test.module index badea81..87e26b8 100644 --- a/core/modules/system/tests/modules/theme_test/theme_test.module +++ b/core/modules/system/tests/modules/theme_test/theme_test.module @@ -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. diff --git a/core/tests/Drupal/Tests/Core/Theme/RegistryTest.php b/core/tests/Drupal/Tests/Core/Theme/RegistryTest.php new file mode 100644 index 0000000..8445150 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Theme/RegistryTest.php @@ -0,0 +1,141 @@ + 'Theme Registry', + 'description' => 'Tests the theme registry.', + 'group' => 'Theme', + ); + } + + 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(); + } + + 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__))))); +}