diff --git a/core/core.services.yml b/core/core.services.yml index 9dca742..b40eddd 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -419,3 +419,8 @@ services: token: class: Drupal\Core\Utility\Token arguments: ['@module_handler'] + theme.registry: + class: Drupal\Core\Theme\Registry + arguments: ['@cache.cache', '@module_handler'] + tags: + - { name: needs_destruction } diff --git a/core/includes/common.inc b/core/includes/common.inc index 4668a90..9e44983 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -4622,8 +4622,6 @@ function _drupal_bootstrap_full($skip = FALSE) { if (!defined('MAINTENANCE_MODE') || MAINTENANCE_MODE != 'update') { // Prior to invoking hook_init(), initialize the theme (potentially a custom // one for this page), so that: - // - Modules with hook_init() implementations that call theme() or - // theme_get_registry() don't initialize the incorrect theme. // - The theme can have hook_*_alter() implementations affect page building // (e.g., hook_form_alter(), hook_node_view_alter(), hook_page_alter()), // ahead of when rendering starts. diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc index 32c0b49..5a3e437 100644 --- a/core/includes/install.core.inc +++ b/core/includes/install.core.inc @@ -381,6 +381,12 @@ 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('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/module.inc b/core/includes/module.inc index bc91311..8840ecb 100644 --- a/core/includes/module.inc +++ b/core/includes/module.inc @@ -362,7 +362,7 @@ function module_enable($module_list, $enable_dependencies = TRUE) { // Refresh the schema to include it. drupal_get_schema(NULL, TRUE); // Update the theme registry to include it. - drupal_theme_rebuild(); + Drupal::service('theme.registry')->reset(); // Allow modules to react prior to the installation of a module. module_invoke_all('modules_preinstall', array($module)); @@ -517,7 +517,7 @@ function module_disable($module_list, $disable_dependents = TRUE) { drupal_container()->get('kernel')->updateModules($enabled, $enabled); // Update the theme registry to remove the newly-disabled module. - drupal_theme_rebuild(); + Drupal::service('theme.registry')->reset(); } } diff --git a/core/includes/theme.inc b/core/includes/theme.inc index 0d0e739..acafc43 100644 --- a/core/includes/theme.inc +++ b/core/includes/theme.inc @@ -11,6 +11,7 @@ use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Config\Config; use Drupal\Core\Template\Attribute; +use Drupal\Core\Theme\Registry; use Drupal\Core\Utility\ThemeRegistry; use Drupal\Core\Theme\ThemeSettings; @@ -138,10 +139,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; @@ -268,424 +267,9 @@ function _drupal_theme_initialize($theme, $base_theme = array(), $registry_callb } } // Load twig as secondary always available engine. - // @todo Make twig the default engine and remove this. This is required - // because (by design) the theme system doesn't allow modules to register more - // than one type of extension. We need a temporary backwards compatibility - // layer to allow us to perform core-wide .tpl.php to .html.twig conversion. + // @todo Replace this hack with generic $theme->engine loading (including base + // themes). 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)); - } -} - -/** - * Gets the theme registry. - * - * @param bool $complete - * Optional boolean to indicate whether to return the complete theme registry - * array or an instance of the Drupal\Core\Utility\ThemeRegistry class. - * If TRUE, the complete theme registry array will be returned. This is useful - * if you want to foreach over the whole registry, use array_* functions or - * inspect it in a debugger. If FALSE, an instance of the - * Drupal\Core\Utility\ThemeRegistry class will be returned, this provides an - * ArrayObject which allows it to be accessed with array syntax and isset(), - * and should be more lightweight than the full registry. Defaults to TRUE. - * - * @return - * The complete theme registry array, or an instance of the - * 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) { - 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_container()->get('module_handler')->isLoaded()) { - _theme_save_registry($theme, $registry); - } - } - return $registry; - } - else { - return new ThemeRegistry('theme_registry:runtime:' . $theme->name, 'cache', array('theme_registry' => TRUE), drupal_container()->get('module_handler')->isLoaded()); - } -} - -/** - * 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. - * - 'process 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(); - - // Processor functions work in two distinct phases with the process - // functions always being executed after the preprocess functions. - $variable_process_phases = array( - 'preprocess functions' => 'preprocess', - 'process functions' => 'process', - ); - - $hook_defaults = array( - 'variables' => TRUE, - 'render element' => TRUE, - 'pattern' => TRUE, - 'base hook' => TRUE, - ); - - // Invoke the hook_theme() implementation, process 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']; - } - if ($type == 'module' || $type == 'theme_engine') { - // Add two render engines for modules and theme engines. - // @todo Remove and make twig the default engine. - $render_engines = array( - '.html.twig' => 'twig', - '.tpl.php' => 'phptemplate' - ); - - // Find the best engine for this template. - foreach ($render_engines as $extension => $engine) { - // Render the output using the template file. - $template_file = $result[$hook]['template'] . $extension; - if (isset($info['path'])) { - $template_file = $info['path'] . '/' . $template_file; - } - if (file_exists($template_file)) { - $result[$hook]['template_file'] = $template_file; - $result[$hook]['engine'] = $engine; - break; - } - } - } - } - - // Allow variable processors for all theming hooks, whether the hook is - // implemented as a template or as a function. - foreach ($variable_process_phases as $phase_key => $phase) { - // Check for existing variable processors. Ensure arrayness. - if (!isset($info[$phase_key]) || !is_array($info[$phase_key])) { - $info[$phase_key] = array(); - $prefixes = array(); - if ($type == 'module') { - // Default variable processor prefix. - $prefixes[] = 'template'; - // Add all modules so they can intervene with their own variable - // processors. This allows them to provide variable processors even - // if they are not the owner of the current hook. - $prefixes = array_merge($prefixes, array_keys(drupal_container()->get('module_handler')->getModuleList())); - } - elseif ($type == 'theme_engine' || $type == 'base_theme_engine') { - // Theme engines get an extra set that come before the normally - // named variable processors. - $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 - // processors. - $prefixes[] = $name; - } - foreach ($prefixes as $prefix) { - // Only use non-hook-specific variable processors for theming hooks - // implemented as templates. See theme(). - if (isset($info['template']) && function_exists($prefix . '_' . $phase)) { - $info[$phase_key][] = $prefix . '_' . $phase; - } - if (function_exists($prefix . '_' . $phase . '_' . $hook)) { - $info[$phase_key][] = $prefix . '_' . $phase . '_' . $hook; - } - } - } - // Check for the override flag and prevent the cached variable - // processors from being used. This allows themes or theme engines to - // remove variable processors set earlier in the registry build. - if (!empty($info['override ' . $phase_key])) { - // Flag not needed inside the registry. - unset($result[$hook]['override ' . $phase_key]); - } - elseif (isset($cache[$hook][$phase_key]) && is_array($cache[$hook][$phase_key])) { - $info[$phase_key] = array_merge($cache[$hook][$phase_key], $info[$phase_key]); - } - $result[$hook][$phase_key] = $info[$phase_key]; - } - } - - // Merge the newly created theme hooks into the existing cache. - $cache = $result + $cache; - } - - // Let themes have variable processors 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])) { - foreach ($variable_process_phases as $phase_key => $phase) { - if (!isset($info[$phase_key])) { - $cache[$hook][$phase_key] = array(); - } - // Only use non-hook-specific variable processors for theming hooks - // implemented as templates. See theme(). - if (isset($info['template']) && function_exists($name . '_' . $phase)) { - $cache[$hook][$phase_key][] = $name . '_' . $phase; - } - if (function_exists($name . '_' . $phase . '_' . $hook)) { - $cache[$hook][$phase_key][] = $name . '_' . $phase . '_' . $hook; - $cache[$hook]['theme path'] = $path; - } - // Ensure uniqueness. - $cache[$hook][$phase_key] = array_unique($cache[$hook][$phase_key]); - } - } - } - } -} - -/** - * 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, process 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 (module_implements('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_container()->get('module_handler')->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)); - } - - if ($theme_engine == 'phptemplate') { - // Check for Twig templates if this is a PHPTemplate theme. - // @todo Remove this once all core themes are converted to Twig. - _theme_process_registry($cache, 'twig', '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) { - foreach (array('preprocess functions', 'process functions') as $phase) { - if (empty($info[$phase])) { - unset($cache[$hook][$phase]); - } - } - } - return $cache; } /** @@ -868,6 +452,7 @@ function drupal_find_base_themes($themes, $key, $used_keys = array()) { * executed (if they exist), in the following order (note that in the following * list, HOOK indicates the theme hook name, MODULE indicates a module name, * THEME indicates a theme name, and ENGINE indicates a theme engine name): + * @todo Needs update for declarative *_preprocess_HOOK(). * - template_preprocess(&$variables, $hook): Creates a default set of * variables for all theme hooks with template implementations. * - template_preprocess_HOOK(&$variables): Should be implemented by the module @@ -966,12 +551,12 @@ 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. if (!drupal_container()->get('module_handler')->isLoaded() && !defined('MAINTENANCE_MODE')) { throw new Exception(t('theme() may not be called until all modules are loaded.')); } - $hooks = theme_get_registry(FALSE); + $hooks = \Drupal::service('theme.registry')->getRuntime(); // If an array of hook candidates were passed, use the first one that has an // implementation. @@ -1066,22 +651,20 @@ function theme($hook, $variables = array()) { include_once DRUPAL_ROOT . '/' . $include_file; } } - if (isset($base_hook_info['preprocess functions']) || isset($base_hook_info['process functions'])) { + if (isset($base_hook_info['preprocess']) || isset($base_hook_info['process'])) { $variables['theme_hook_suggestion'] = $hook; $hook = $base_hook; $info = $base_hook_info; } } - if (isset($info['preprocess functions']) || isset($info['process functions'])) { + if (isset($info['preprocess']) || isset($info['process'])) { $variables['theme_hook_suggestions'] = array(); - foreach (array('preprocess functions', 'process functions') as $phase) { + foreach (array('preprocess', 'process') as $phase) { if (!empty($info[$phase])) { foreach ($info[$phase] as $processor_function) { - if (function_exists($processor_function)) { - // We don't want a poorly behaved process function changing $hook. - $hook_clone = $hook; - $processor_function($variables, $hook_clone); - } + // We don't want a poorly behaved process function changing $hook. + $hook_clone = $hook; + $processor_function($variables, $hook_clone); } } } @@ -1106,6 +689,11 @@ function theme($hook, $variables = array()) { } foreach (array_reverse($suggestions) as $suggestion) { if (isset($hooks[$suggestion])) { + // A theme hook suggestion is essentially the reverse of a base hook + // definition; instead of finding the generic parent, we find a more + // specific child. The registry ensures to merge the base hook info into + // the suggestion's info. + // @see \Drupal\Core\Theme\Registry::build() $info = $hooks[$suggestion]; break; } @@ -1114,12 +702,10 @@ function theme($hook, $variables = array()) { // Generate the output using either a function or a template. $output = ''; - if (isset($info['function'])) { - if (function_exists($info['function'])) { - $output = $info['function']($variables); - } + if (!isset($info['template']) && isset($info['function'])) { + $output = $info['function']($variables); } - else { + elseif (isset($info['template'])) { // Default render function and extension. $render_function = 'theme_render_template'; $extension = '.tpl.php'; @@ -1145,21 +731,6 @@ function theme($hook, $variables = array()) { } } - // In some cases, a template implementation may not have had - // template_preprocess() run (for example, if the default implementation is - // a function, but a template overrides that default implementation). In - // these cases, a template should still be able to expect to have access to - // the variables provided by template_preprocess(), so we add them here if - // they don't already exist. We don't want the overhead of running - // template_preprocess() twice, so we use the 'directory' variable to - // determine if it has already run, which while not completely intuitive, - // is reasonably safe, and allows us to save on the overhead of adding some - // new variable to track that. - if (!isset($variables['directory'])) { - $default_template_variables = array(); - template_preprocess($default_template_variables, $hook); - $variables += $default_template_variables; - } if (!isset($default_attributes)) { $default_attributes = new Attribute(); } @@ -1176,15 +747,12 @@ function theme($hook, $variables = array()) { } // Render the output using the template file. - $template_file = $info['template'] . $extension; - if (isset($info['path'])) { - $template_file = $info['path'] . '/' . $template_file; - } - - // Modules can override this. if (isset($info['template_file'])) { $template_file = $info['template_file']; } + else { + $template_file = $info['path'] . '/' . $info['template'] . $extension; + } $output = $render_function($template_file, $variables); } @@ -1248,10 +816,8 @@ function drupal_find_theme_functions($cache, $prefixes) { if ($matches) { foreach ($matches as $match) { $new_hook = substr($match, strlen($prefix) + 1); - $arg_name = isset($info['variables']) ? 'variables' : 'render element'; $implementations[$new_hook] = array( 'function' => $match, - $arg_name => $info[$arg_name], 'base hook' => $hook, ); } @@ -1362,11 +928,9 @@ function drupal_find_theme_templates($cache, $extension, $path) { $file = str_replace($extension, '', $file); // Put the underscores back in for the hook name and register this // pattern. - $arg_name = isset($info['variables']) ? 'variables' : 'render element'; $implementations[strtr($file, '-', '_')] = array( 'template' => $file, 'path' => dirname($files[$match]->uri), - $arg_name => $info[$arg_name], 'base hook' => $hook, ); } @@ -1571,7 +1135,7 @@ function theme_enable($theme_list) { list_themes(TRUE); menu_router_rebuild(); - drupal_theme_rebuild(); + Drupal::service('theme.registry')->reset(); // Invoke hook_themes_enabled() after the themes have been enabled. module_invoke_all('themes_enabled', $theme_list); @@ -1606,7 +1170,7 @@ function theme_disable($theme_list) { list_themes(TRUE); menu_router_rebuild(); - drupal_theme_rebuild(); + Drupal::service('theme.registry')->reset(); // Invoke hook_themes_disabled after the themes have been disabled. module_invoke_all('themes_disabled', $theme_list); @@ -3181,18 +2745,24 @@ function drupal_common_theme() { // From theme.inc. 'html' => array( 'render element' => 'page', + 'preprocess' => array('template_preprocess_html'), + 'process' => array('template_process_html'), 'template' => 'html', ), 'page' => array( 'render element' => 'page', + 'preprocess' => array('template_preprocess_page'), + 'process' => array('template_process_page'), 'template' => 'page', ), 'region' => array( 'render element' => 'elements', + 'preprocess' => array('template_preprocess_region'), 'template' => 'region', ), 'datetime' => array( 'variables' => array('timestamp' => NULL, 'text' => NULL, 'attributes' => array(), 'html' => FALSE), + 'preprocess' => array('template_preprocess_datetime'), 'template' => 'datetime', ), 'status_messages' => array( @@ -3239,6 +2809,7 @@ function drupal_common_theme() { ), 'item_list' => array( 'variables' => array('items' => array(), 'title' => '', 'type' => 'ul', 'attributes' => array()), + 'preprocess' => array('template_preprocess_item_list'), ), 'more_help_link' => array( 'variables' => array('url' => NULL), @@ -3261,103 +2832,123 @@ function drupal_common_theme() { // From theme.maintenance.inc. 'maintenance_page' => array( 'variables' => array('content' => NULL, 'show_messages' => TRUE), + 'includes' => array('core/includes/theme.maintenance.inc'), + 'preprocess' => array('template_preprocess_maintenance_page'), + 'process' => array('template_process_maintenance_page'), 'template' => 'maintenance-page', ), 'install_page' => array( 'variables' => array('content' => NULL), + 'includes' => array('core/includes/theme.maintenance.inc'), ), 'task_list' => array( 'variables' => array('items' => NULL, 'active' => NULL), + 'includes' => array('core/includes/theme.maintenance.inc'), ), 'authorize_message' => array( 'variables' => array('message' => NULL, 'success' => TRUE), + 'includes' => array('core/includes/theme.maintenance.inc'), ), 'authorize_report' => array( 'variables' => array('messages' => array()), + 'includes' => array('core/includes/theme.maintenance.inc'), ), // From pager.inc. 'pager' => array( 'variables' => array('tags' => array(), 'element' => 0, 'parameters' => array(), 'quantity' => 9), - ), - 'pager_first' => array( - 'variables' => array('text' => NULL, 'element' => 0, 'parameters' => array()), - ), - 'pager_previous' => array( - 'variables' => array('text' => NULL, 'element' => 0, 'interval' => 1, 'parameters' => array()), - ), - 'pager_next' => array( - 'variables' => array('text' => NULL, 'element' => 0, 'interval' => 1, 'parameters' => array()), - ), - 'pager_last' => array( - 'variables' => array('text' => NULL, 'element' => 0, 'parameters' => array()), + 'includes' => array('core/includes/pager.inc'), ), 'pager_link' => array( 'variables' => array('text' => NULL, 'page_new' => NULL, 'element' => NULL, 'parameters' => array(), 'attributes' => array()), + 'includes' => array('core/includes/pager.inc'), ), // From menu.inc. 'menu_link' => array( 'render element' => 'element', + 'includes' => array('core/includes/menu.inc'), ), 'menu_tree' => array( 'render element' => 'tree', + 'preprocess' => array('template_preprocess_menu_tree'), + 'includes' => array('core/includes/menu.inc'), ), 'menu_local_task' => array( 'render element' => 'element', + 'includes' => array('core/includes/menu.inc'), ), 'menu_local_action' => array( 'render element' => 'element', + 'includes' => array('core/includes/menu.inc'), ), 'menu_local_tasks' => array( 'variables' => array('primary' => array(), 'secondary' => array()), + 'includes' => array('core/includes/menu.inc'), ), // From form.inc. 'input' => array( 'render element' => 'element', + 'preprocess' => array('template_preprocess_input'), + 'includes' => array('core/includes/form.inc'), ), 'select' => array( 'render element' => 'element', + 'includes' => array('core/includes/form.inc'), ), 'fieldset' => array( 'render element' => 'element', + 'includes' => array('core/includes/form.inc'), ), 'details' => array( 'render element' => 'element', + 'includes' => array('core/includes/form.inc'), ), 'radios' => array( 'render element' => 'element', + 'includes' => array('core/includes/form.inc'), ), 'date' => array( 'render element' => 'element', + 'includes' => array('core/includes/form.inc'), ), 'exposed_filters' => array( 'render element' => 'form', + 'includes' => array('core/includes/form.inc'), ), 'checkboxes' => array( 'render element' => 'element', + 'includes' => array('core/includes/form.inc'), ), 'form' => array( 'render element' => 'element', + 'includes' => array('core/includes/form.inc'), ), 'textarea' => array( 'render element' => 'element', + 'includes' => array('core/includes/form.inc'), ), 'tableselect' => array( 'render element' => 'element', + 'includes' => array('core/includes/form.inc'), ), 'form_element' => array( 'render element' => 'element', + 'includes' => array('core/includes/form.inc'), ), 'form_required_marker' => array( 'render element' => 'element', + 'includes' => array('core/includes/form.inc'), ), 'form_element_label' => array( 'render element' => 'element', + 'includes' => array('core/includes/form.inc'), ), 'vertical_tabs' => array( 'render element' => 'element', + 'includes' => array('core/includes/form.inc'), ), 'container' => array( 'render element' => 'element', + 'includes' => array('core/includes/form.inc'), ), ); } diff --git a/core/includes/theme.maintenance.inc b/core/includes/theme.maintenance.inc index 9215957..b770d09 100644 --- a/core/includes/theme.maintenance.inc +++ b/core/includes/theme.maintenance.inc @@ -90,7 +90,10 @@ function _drupal_maintenance_theme() { $base_theme[] = $new_base_theme = $themes[$themes[$ancestor]->base_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 are usually added from system_init() -except maintenance.css. // When the database is inactive it's not called so we add it here. @@ -102,13 +105,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 a5feb50..764b23d 100644 --- a/core/includes/update.inc +++ b/core/includes/update.inc @@ -429,6 +429,9 @@ function update_prepare_d8_bootstrap() { new Settings($settings); $kernel = new DrupalKernel('update', FALSE, drupal_classloader(), FALSE); $kernel->boot(); + + // Remove the theme_registry cache entry from D7. + Drupal::cache('cache')->deleteAll(); } /** diff --git a/core/lib/Drupal/Core/EventSubscriber/LegacyRequestSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/LegacyRequestSubscriber.php index 242f935..0d9f76d 100644 --- a/core/lib/Drupal/Core/EventSubscriber/LegacyRequestSubscriber.php +++ b/core/lib/Drupal/Core/EventSubscriber/LegacyRequestSubscriber.php @@ -29,8 +29,6 @@ public function onKernelRequestLegacy(GetResponseEvent $event) { if ($event->getRequestType() == HttpKernelInterface::MASTER_REQUEST) { // Prior to invoking hook_init(), initialize the theme (potentially a // custom one for this page), so that: - // - Modules with hook_init() implementations that call theme() or - // theme_get_registry() don't initialize the incorrect theme. // - The theme can have hook_*_alter() implementations affect page // building (e.g., hook_form_alter(), hook_node_view_alter(), // hook_page_alter()), ahead of when rendering starts. diff --git a/core/lib/Drupal/Core/Theme/Registry.php b/core/lib/Drupal/Core/Theme/Registry.php new file mode 100644 index 0000000..7146f47 --- /dev/null +++ b/core/lib/Drupal/Core/Theme/Registry.php @@ -0,0 +1,717 @@ +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 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. + * @todo Remove 'theme path', it's useless... or fix it: For theme + * 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\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, ModuleHandlerInterface $module_handler, $theme_name = NULL) { + $this->cache = $cache; + $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. + * + * @global object $theme_info + * An object with (at least) the following information: + * - uri: The path to the theme. + * - owner: The name of the theme's base theme. + * - engine: The name of the theme engine to use. + * @global array $base_theme_info + * (optional) An array of objects that represent the base themes of $theme, + * each having the same properties as $theme above, ordered by base theme + * hierarchy; i.e., the first element is the root of all themes. + * @global string $theme_engine + * The name of the theme engine. + * + * @todo Remove global $theme_engine, it duplicates ->engine. + * @todo Inject ThemeHandler, so as to remove all of these globals, + * in this way: + * - theme engines are dependencies of themes. + * - theme engines are actually treated *identically* to themes. (also: + * remove the /engines subdirectory) + * - base themes are dependencies of themes. + * As a result: + * - $theme->requires == should contain the full stack of dependencies, + * in the correct order. + * + * @todo Merge this into ::get(), so modules + tests can retrieve the registry + * for a particular $theme_name. + */ + 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'])) { + 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); + drupal_theme_initialize(); + } + $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 = list_themes(); + $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 + * + * @todo Add a lock for the (re)build operation. It's relatively quick, but + * not necessarily fast enough in case of many parallel requests. + */ + 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(); + // 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, 'cache', 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)); + } + + /** + * Builds the theme registry from scratch. + * + * 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(). + * + * @see hook_theme() + * + * 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() + */ + protected function build() { + // @todo Replace local $registry variables in methods with $this->registry. + $registry = array(); + + // hook_theme() implementations of modules are always the same. + if ($cache = $this->cache->get('theme_registry:build:modules')) { + $registry = $cache->data; + } + else { + foreach ($this->moduleHandler->getImplementations('theme') as $module) { + $this->processExtension($registry, 'module', $module, $module, drupal_get_path('module', $module)); + } + // Only persist it if all modules are loaded to ensure it is complete. + // @todo Get rid of these checks. + if ($this->moduleHandler->isLoaded()) { + $this->cache->set("theme_registry:build:modules", $registry, CacheBackendInterface::CACHE_PERMANENT, array('theme_registry' => TRUE), TRUE); + } + } + + // Process each base theme. + foreach ($this->baseThemes as $base) { + // If the base theme uses a theme engine, process its hooks. + $base_path = dirname($base->uri); + if ($this->engine) { + $this->processExtension($registry, 'base_theme_engine', $this->engine, $base->name, $base_path); + } + $this->processExtension($registry, 'base_theme', $base->name, $base->name, $base_path); + } + + // Process the theme itself. + if ($this->engine) { + $this->processExtension($registry, 'theme_engine', $this->engine, $this->theme->name, dirname($this->theme->uri)); + } + if ($this->engine == 'phptemplate') { + // Check for Twig templates if this is a PHPTemplate theme. + // @todo Remove this once all core themes are converted to Twig. + $this->processExtension($registry, 'theme_engine', 'twig', $this->theme->name, dirname($this->theme->uri)); + } + $this->processExtension($registry, 'theme', $this->theme->name, $this->theme->name, dirname($this->theme->uri)); + + // Allow modules to alter the complete registry. + $this->moduleHandler->alter('theme_registry', $registry); + + $this->registry = $registry; + $this->compile(); + return $this->registry; + } + + /** + * Process a single implementation of hook_theme(). + * + * @param array $registry + * The theme registry that will eventually be cached. + * @param string $type + * One of 'module', 'base_theme_engine', 'base_theme', 'theme_engine', or + * 'theme'. + * @param string $name + * The name of the extension implementing hook_theme(). + * @param string $theme_name + * The name of the extension for which theme hooks are currently processed. + * This equals $name for all extension types, except when $type is a theme + * engine, in which case $theme_name and $theme_path are pointing to the + * respective [base] theme the theme engine is associated with. + * @param string $theme_path + * The directory of $theme_name; e.g., 'core/modules/node' or + * 'themes/bartik'. + * + * @see theme() + * @see hook_theme() + */ + protected function processExtension(&$registry, $type, $name, $theme_name, $theme_path) { + $function = $name . '_theme'; + // Extensions do not necessarily have to implement hook_theme(). + if (!function_exists($function)) { + return; + } + foreach ($function($registry, $type, $theme_name, $theme_path) as $hook => $info) { + // Ensure this hook key exists; it will be set either way. + $registry += array($hook => array( + // @todo It's not really clear what these are pointing to, whether they + // are used anywhere, and how, and whether they are needed at all. + 'type' => $type, + 'name' => $name, + )); + + if (isset($info['file'])) { + $include_path = isset($info['path']) ? $info['path'] : $theme_path; + $info['includes'][] = $include_path . '/' . $info['file']; + unset($info['file']); + } + + // An actual/original theme hook must define either 'variables' or a + // 'render element', in which case we need to assign default values for + // 'template' or 'function'. + if (isset($info['variables']) || isset($info['render element'])) { + // Add an internal build process marker to track that this an actual + // theme hook and not a suggestion. + $info['exists'] = TRUE; + + // The effective path_to_theme() during theme(). + $info['theme path'] = $theme_path; + + if (isset($info['template'])) { + // Default the template path to the 'templates' directory of the + // extension, unless overridden. + if (!isset($info['path'])) { + $info['path'] = $theme_path . '/templates'; + } + // Find the preferred theme engine for this module template. + // @todo Remove this. Simply support multiple theme engines; + // which will simplify the entire processing in the first place. + if ($type == 'module' || $type == 'theme_engine') { + // Add two render engines for modules and theme engines. + $render_engines = array( + '.html.twig' => 'twig', + '.tpl.php' => 'phptemplate', + ); + // Find the best engine for this template. + foreach ($render_engines as $extension => $engine) { + // Render the output using the template file. + $template_file = $info['path'] . '/' . $info['template'] . $extension; + if (file_exists($template_file)) { + $info['template_file'] = $template_file; + $info['engine'] = $engine; + break; + } + } + } + } + // Otherwise, the implementation must be a function. However, functions + // do not need to be specified manually; the array key of the hook is + // expected to be taken over as function, unless overridden. + elseif (!isset($info['function'])) { + if ($type == 'module') { + $info['function'] = 'theme_' . $hook; + } + else { + $info['function'] = $name . '_' . $hook; + } + } + } + // If no 'variables' or 'render element' was defined, then this hook + // definition extends an existing, or defines data for a hook suggestion. + else { + // Data for hook suggestions requires a full registry in order to check + // for base hooks, since suggestions are extending hooks horizontally + // (instead of overriding vertically); therefore it happens after + // per-extension processing. + // @see Registry::compile() + + // Revert the above theme engine hack for Twig, if the actual theme + // engine returns a template. + // @todo Remove the above hack. Simply support multiple theme engines; + // which will simplify the entire processing in the first place. + if ($type == 'theme_engine' && isset($info['template'])) { + // If the theme engine found a template set it as used theme engine. + $info['engine'] = $name; + + $render_engines = array( + 'twig' => '.html.twig', + 'phptemplate' => '.tpl.php', + ); + $extension = $render_engines[$name]; + // Render the output using the template file. + $template_file = $info['path'] . '/' . $info['template'] . $extension; + if (file_exists($template_file)) { + $info['template_file'] = $template_file; + } + } + + if (isset($info['template'])) { + // A template implementation always takes precedence over functions. + // A potentially existing function pointer is obsolete. + unset($registry[$hook]['function']); + // Adjust the effective path_to_theme() during theme(). + $info['theme path'] = $theme_path; + // Default the template path to the 'templates' directory of the + // extension, unless overridden. + if (!isset($info['path'])) { + $info['path'] = $theme_path . '/templates'; + } + } + } + + // Record (pre)process functions by extension type. + // The override logic here is essential: + // - The first time a 'template' is defined by any extension, default + // template (pre)processor functions need to be injected. + // - Some of the template (pre)processor functions have to run first; + // e.g., template_*(). + // - A special variant of template (pre)processor functions, + // template_preprocess_HOOK(), needs to run second, right after the base + // template_(pre)process() functions. + // - Followed by the global hook_(pre)process() functions that apply to + // all templates, which need to be collated from all modules, all + // engines, and all themes (in this order). + // - And lastly, any other (pre)process functions that have been declared + // in hook_theme(). + // Furthermore: + // - template_preprocess_HOOK() and hook_(pre)process_HOOK() also need to + // run for theme *functions*, not only templates. All other template/ + // default processors are omitted, unless explicitly declared in + // hook_theme(). (performance). + // - If a later extension type in the build process replaces a theme + // function with a theme template by declaring 'template' (e.g., a theme + // wants to use a template instead of a function), then all of the + // default processors need to be injected (in the order described above). + // - All recorded data of previous processing steps is expected to be + // available to hook_theme() implementations, which means that these + // operations cannot happen in Registry::compile(). + // To achieve the required ordering, the build process records all + // registered and automatically determined (pre)process functions keyed by + // extension type. This allows the final compile pass in + // Registry::compile() to sort the final list of functions in their + // required order. + // Additionally, all functions are added with a string key, so they do not + // get duplicated when merging the info of the current extension into the + // existing registry info. + // @see theme() + $has_template = isset($registry[$hook]['template']) || isset($info['template']); + foreach (array('preprocess', 'process') as $phase) { + if (isset($info[$phase]) || $has_template) { + if (isset($info[$phase])) { + $info[$phase] = array_combine($info[$phase], $info[$phase]); + } + else { + $info[$phase] = array(); + } + $functions = array(); + // 1) The base template_(pre)process(). Only for templates. + if ($has_template) { + $template_function = "template_{$phase}"; + if (function_exists($template_function)) { + $functions['template'][$template_function] = $template_function; + } + } + // 2) template_(pre)process_HOOK(), if registered in $info. + $template_function = "template_{$phase}_{$hook}"; + if (isset($info[$phase][$template_function])) { + $functions['template_hook'][$template_function] = $template_function; + unset($info[$phase][$template_function]); + } + // 3) hook_(pre)process() of all modules. Only for templates. + // Since modules are processed before themes, but themes can declare + // templates, module hook implementations need to be added whenever a + // template is added. + if ($has_template) { + $functions['module'] = $this->getHookImplementations($phase); + } + // 4) hook_(pre)process() of theme engines and themes. + // Template hooks of modules are processed in later steps, so we need + // to add hook_(pre)process() functions. + if ($type != 'module' && $has_template) { + $function = $name . '_' . $phase; + if (function_exists($function)) { + $functions[$type][$function] = $function; + } + } + // 5) hook_(pre)process_HOOK(), as declared in $info. + // Since template_(pre)process_HOOK() was removed above, check whether + // any functions are left first. + if (!empty($info[$phase])) { + $key = $type . '_hook'; + $functions[$key] = $info[$phase]; + } + + // Replace the list functions (they are keyed by extension type now). + $info[$phase] = $functions; + } + } + + // Themes and theme engines can force-remove all preprocess functions. + // If so, they need to provide their own. Therefore, unset existing before + // merging. + // @see hook_theme() + if (!empty($info['no preprocess'])) { + unset($registry[$hook]['preprocess']); + unset($registry[$hook]['no preprocess']); + } + + // Merge this extension's theme hook definition into the existing. + $registry[$hook] = NestedArray::mergeDeep($registry[$hook], $info); + } + } + + /** + * Compiles the theme registry. + * + * Compilation involves these steps: + * - Theme hook suggestions are mapped and resolved to base hooks. + * - (Pre)process functions are sorted. + * - Unnecessary data is removed. + */ + protected function compile() { + // Merge base hooks into suggestions and remove unnecessary hooks. + foreach ($this->registry as $hook => &$info) { + if (empty($info['exists'])) { + if (!$base_hook = $this->getBaseHook($hook)) { + // If no base hook was found, then this is a suggestion for a theme + // hook of another extension that is not enabled. + unset($this->registry[$hook]); + continue; + } + // If a base hook is found, use it as base. (Pre)processor functions + // of the hook suggestion are appended, since the hook suggestion is + // more specific, by design. Any other info of this hook overrides the + // base hook. + $info = NestedArray::mergeDeep($this->registry[$base_hook], $info); + $info['base hook'] = $base_hook; + } + } + + // Compile (pre)process functions and clean up unnecessary data. + $preprocessor_phases = array( + 'template', + 'template_hook', + 'module', + 'module_hook', + 'base_theme_engine', + 'base_theme_engine_hook', + 'theme_engine', + 'theme_engine_hook', + 'base_theme', + 'base_theme_hook', + 'theme', + 'theme_hook', + ); + foreach ($this->registry as $hook => &$info) { + if (isset($info['exists'])) { + unset($info['exists']); + } + // (Pre)process functions have been collected separately by extension type + // during the build process. Due to the required final merging of base + // hooks and hook suggestions, as well as the possibility of functions + // getting replaced with templates by themes, the final set of functions + // needs to be determined and compiled now. + foreach (array('preprocess', 'process') as $phase) { + if (isset($info[$phase])) { + $functions = array(); + foreach ($preprocessor_phases as $type) { + if (isset($info[$phase][$type])) { + $functions += $info[$phase][$type]; + } + } + // Remove the unnecessary array keys to decrease the array size. + $info[$phase] = array_values($functions); + } + } + } + + return $this->registry; + } + + /** + * 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; + } + + /** + * Retrieves module hook implementations for a given theme hook name. + * + * @param string $hook + * The hook name to discover. + * + * @return array + * An array of module hook implementations; i.e., the actual function names. + */ + protected function getHookImplementations($hook) { + $implementations = array(); + foreach ($this->moduleHandler->getImplementations($hook) as $module) { + $function = $module . '_' . $hook; + $implementations[$function] = $function; + }; + return $implementations; + } + + /** + * 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; + } + + /** + * Implements Drupal\Core\DestructableInterface::destruct(). + */ + public function destruct() { + if (isset($this->runtimeRegistry)) { + $this->runtimeRegistry->destruct(); + } + } + +} diff --git a/core/lib/Drupal/Core/Utility/ThemeRegistry.php b/core/lib/Drupal/Core/Utility/ThemeRegistry.php index eb2dd80..fd61f47 100644 --- a/core/lib/Drupal/Core/Utility/ThemeRegistry.php +++ b/core/lib/Drupal/Core/Utility/ThemeRegistry.php @@ -8,6 +8,7 @@ namespace Drupal\Core\Utility; use Drupal\Core\Cache\CacheBackendInterface; +use Drupal\Core\DestructableInterface; /** * Builds the run-time theme registry. @@ -17,7 +18,7 @@ * 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 CacheArray implements DestructableInterface { /** * Whether the partial registry can be persisted to the cache. @@ -76,7 +77,8 @@ 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); } @@ -111,8 +113,9 @@ public function offsetGet($offset) { * Implements CacheArray::resolveCacheMiss(). */ public function resolveCacheMiss($offset) { + // @todo DIC this. if (!isset($this->completeRegistry)) { - $this->completeRegistry = theme_get_registry(); + $this->completeRegistry = \Drupal::service('theme.registry')->get(); } $this->storage[$offset] = $this->completeRegistry[$offset]; if ($this->persistable) { @@ -142,4 +145,19 @@ public function set($data, $lock = TRUE) { } } } + + + /** + * Implements Drupal\Core\DestructableInterface::destruct(). + */ + public function destruct() { + parent::__destruct(); + } + + /** + * Destructs the ThemeRegistry object. + */ + public function __destruct() { + } + } diff --git a/core/modules/aggregator/aggregator.module b/core/modules/aggregator/aggregator.module index 5021e57..f74d600 100644 --- a/core/modules/aggregator/aggregator.module +++ b/core/modules/aggregator/aggregator.module @@ -55,6 +55,7 @@ function aggregator_theme() { 'aggregator_feed_source' => array( 'variables' => array('aggregator_feed' => NULL, 'view_mode' => NULL), 'file' => 'aggregator.pages.inc', + 'preprocess' => array('template_preprocess_aggregator_feed_source'), 'template' => 'aggregator-feed-source', ), 'aggregator_block_item' => array( @@ -63,15 +64,18 @@ function aggregator_theme() { 'aggregator_summary_items' => array( 'variables' => array('summary_items' => NULL, 'source' => NULL), 'file' => 'aggregator.pages.inc', + 'preprocess' => array('template_preprocess_aggregator_summary_items'), 'template' => 'aggregator-summary-items', ), 'aggregator_summary_item' => array( 'variables' => array('aggregator_item' => NULL, 'view_mode' => NULL), 'file' => 'aggregator.pages.inc', + 'preprocess' => array('template_preprocess_aggregator_summary_item'), ), 'aggregator_item' => array( 'variables' => array('aggregator_item' => NULL, 'view_mode' => NULL), 'file' => 'aggregator.pages.inc', + 'preprocess' => array('template_preprocess_aggregator_item'), 'template' => 'aggregator-item', ), 'aggregator_page_opml' => array( @@ -82,6 +86,9 @@ function aggregator_theme() { 'variables' => array('feeds' => NULL, 'category' => NULL), 'file' => 'aggregator.pages.inc', ), + 'block' => array( + 'preprocess' => array('aggregator_preprocess_block'), + ), ); } diff --git a/core/modules/block/block.module b/core/modules/block/block.module index db497e3..c1fe856 100644 --- a/core/modules/block/block.module +++ b/core/modules/block/block.module @@ -90,11 +90,13 @@ function block_theme() { return array( 'block' => array( 'render element' => 'elements', + 'preprocess' => array('template_preprocess_block'), 'template' => 'block', ), 'block_admin_display_form' => array( 'template' => 'block-admin-display-form', 'file' => 'block.admin.inc', + 'preprocess' => array('template_preprocess_block_admin_display_form'), 'render element' => 'form', ), ); diff --git a/core/modules/book/book.module b/core/modules/book/book.module index 887f95f..66a6a4f 100644 --- a/core/modules/book/book.module +++ b/core/modules/book/book.module @@ -45,10 +45,12 @@ function book_theme() { return array( 'book_navigation' => array( 'variables' => array('book_link' => NULL), + 'preprocess' => array('template_preprocess_book_navigation'), 'template' => 'book-navigation', ), 'book_export_html' => array( 'variables' => array('title' => NULL, 'contents' => NULL, 'depth' => NULL), + 'preprocess' => array('template_preprocess_book_export_html'), 'template' => 'book-export-html', ), 'book_admin_table' => array( @@ -56,10 +58,12 @@ function book_theme() { ), 'book_all_books_block' => array( 'render element' => 'book_menus', + 'preprocess' => array('template_preprocess_book_all_books_block'), 'template' => 'book-all-books-block', ), 'book_node_export_html' => array( 'variables' => array('node' => NULL, 'children' => NULL), + 'preprocess' => array('template_preprocess_book_node_export_html'), 'template' => 'book-node-export-html', ), ); diff --git a/core/modules/ckeditor/ckeditor.module b/core/modules/ckeditor/ckeditor.module index c53fefa..40bb026 100644 --- a/core/modules/ckeditor/ckeditor.module +++ b/core/modules/ckeditor/ckeditor.module @@ -83,6 +83,7 @@ function ckeditor_theme() { 'ckeditor_settings_toolbar' => array( 'file' => 'ckeditor.admin.inc', 'variables' => array('editor' => NULL, 'plugins' => NULL), + 'preprocess' => array('template_preprocess_ckeditor_settings_toolbar'), ), ); } diff --git a/core/modules/comment/comment.module b/core/modules/comment/comment.module index 3f6fdf5..8b64109 100644 --- a/core/modules/comment/comment.module +++ b/core/modules/comment/comment.module @@ -185,6 +185,9 @@ function comment_field_extra_fields() { */ function comment_theme() { return array( + 'block__comment' => array( + 'preprocess' => array('comment_preprocess_block'), + ), 'comment_block' => array( 'variables' => array('number' => NULL), ), @@ -192,6 +195,7 @@ function comment_theme() { 'variables' => array('comment' => NULL), ), 'comment' => array( + 'preprocess' => array('template_preprocess_comment'), 'template' => 'comment', 'render element' => 'elements', ), @@ -199,6 +203,7 @@ function comment_theme() { 'variables' => array('node' => NULL), ), 'comment_wrapper' => array( + 'preprocess' => array('template_preprocess_comment_wrapper'), 'template' => 'comment-wrapper', 'render element' => 'content', ), diff --git a/core/modules/contextual/contextual.module b/core/modules/contextual/contextual.module index 51747c6..ab4bd67 100644 --- a/core/modules/contextual/contextual.module +++ b/core/modules/contextual/contextual.module @@ -150,7 +150,7 @@ function contextual_preprocess(&$variables, $hook) { return; } - $hooks = theme_get_registry(FALSE); + $hooks = Drupal::service('theme.registry')->getRuntime(); // Determine the primary theme function argument. if (!empty($hooks[$hook]['variables'])) { diff --git a/core/modules/edit/edit.module b/core/modules/edit/edit.module index 6a73d89..ccc6bd3 100644 --- a/core/modules/edit/edit.module +++ b/core/modules/edit/edit.module @@ -28,6 +28,20 @@ function edit_custom_theme() { } /** + * Implements hook_theme(). + */ +function edit_theme() { + return array( + 'node' => array( + 'preprocess' => array('edit_preprocess_node'), + ), + 'field' => array( + 'preprocess' => array('edit_preprocess_field'), + ), + ); +} + +/** * Implements hook_permission(). */ function edit_permission() { diff --git a/core/modules/field/field.module b/core/modules/field/field.module index 7660f57..90cd15a 100644 --- a/core/modules/field/field.module +++ b/core/modules/field/field.module @@ -170,6 +170,8 @@ function field_theme() { return array( 'field' => array( 'render element' => 'element', + 'preprocess' => array('template_preprocess_field'), + 'process' => array('template_process_field'), ), 'field_multiple_value_form' => array( 'render element' => 'element', diff --git a/core/modules/forum/forum.module b/core/modules/forum/forum.module index 20e8631..d782849 100644 --- a/core/modules/forum/forum.module +++ b/core/modules/forum/forum.module @@ -60,23 +60,31 @@ function forum_help($path, $arg) { */ function forum_theme() { return array( + 'block__forum' => array( + 'preprocess' => array('forum_preprocess_block'), + ), 'forums' => array( + 'preprocess' => array('template_preprocess_forums'), 'template' => 'forums', 'variables' => array('forums' => NULL, 'topics' => NULL, 'parents' => NULL, 'tid' => NULL, 'sortby' => NULL, 'forum_per_page' => NULL), ), 'forum_list' => array( + 'preprocess' => array('template_preprocess_forum_list'), 'template' => 'forum-list', 'variables' => array('forums' => NULL, 'parents' => NULL, 'tid' => NULL), ), 'forum_topic_list' => array( + 'preprocess' => array('template_preprocess_forum_topic_list'), 'template' => 'forum-topic-list', 'variables' => array('tid' => NULL, 'topics' => NULL, 'sortby' => NULL, 'forum_per_page' => NULL), ), 'forum_icon' => array( + 'preprocess' => array('template_preprocess_forum_icon'), 'template' => 'forum-icon', 'variables' => array('new_posts' => NULL, 'num_posts' => 0, 'comment_mode' => 0, 'sticky' => 0, 'first_new' => FALSE), ), 'forum_submitted' => array( + 'preprocess' => array('template_preprocess_forum_submitted'), 'template' => 'forum-submitted', 'variables' => array('topic' => NULL), ), diff --git a/core/modules/help/help.module b/core/modules/help/help.module index d7042d3..cbf56a3 100644 --- a/core/modules/help/help.module +++ b/core/modules/help/help.module @@ -66,6 +66,16 @@ function help_help($path, $arg) { } /** + * Implements hook_theme(). + */ +function help_theme() { + $theme['block__system_help_block'] = array( + 'preprocess' => array('help_preprocess_block'), + ); + return $theme; +} + +/** * Implements hook_preprocess_HOOK() for block.tpl.php. */ function help_preprocess_block(&$variables) { diff --git a/core/modules/image/image.module b/core/modules/image/image.module index 29a18a7..47de5d5 100644 --- a/core/modules/image/image.module +++ b/core/modules/image/image.module @@ -702,7 +702,7 @@ function image_style_flush($style) { // Clear field caches so that formatters may be added for this style. field_info_cache_clear(); - drupal_theme_rebuild(); + Drupal::service('theme.registry')->reset(); // Clear page caches when flushing. if (module_exists('block')) { diff --git a/core/modules/language/language.admin.inc b/core/modules/language/language.admin.inc index 59f1efa..c3e5823 100644 --- a/core/modules/language/language.admin.inc +++ b/core/modules/language/language.admin.inc @@ -917,7 +917,7 @@ function template_preprocess_language_content_settings_table(&$variables) { * @ingroup themable */ function theme_language_content_settings_table($variables) { - return '

' . $variables['build']['#title'] . '

' . drupal_render($variables['build']); + return '

' . $variables['build']['#title'] . '

' . drupal_render_children($variables['build']); } /** diff --git a/core/modules/language/language.module b/core/modules/language/language.module index 6294f31..e7a8b93 100644 --- a/core/modules/language/language.module +++ b/core/modules/language/language.module @@ -178,6 +178,9 @@ function language_permission() { */ function language_theme() { return array( + 'block__language' => array( + 'preprocess' => array('language_preprocess_block'), + ), 'language_negotiation_configure_form' => array( 'render element' => 'form', ), @@ -186,7 +189,7 @@ function language_theme() { 'file' => 'language.admin.inc', ), 'language_content_settings_table' => array( - 'render element' => 'element', + 'render element' => 'build', 'file' => 'language.admin.inc', ), ); diff --git a/core/modules/locale/locale.module b/core/modules/locale/locale.module index 75e0a22..4685d18 100644 --- a/core/modules/locale/locale.module +++ b/core/modules/locale/locale.module @@ -257,6 +257,9 @@ function locale_permission() { */ function locale_theme() { return array( + 'node' => array( + 'preprocess' => array('locale_preprocess_node'), + ), 'locale_translate_edit_form_strings' => array( 'render element' => 'form', 'file' => 'locale.pages.inc', @@ -266,7 +269,7 @@ function locale_theme() { 'file' => 'locale.pages.inc', ), 'locale_translation_update_info' => array( - 'arguments' => array('updates' => array(), 'not_found' => array()), + 'variables' => array('updates' => array(), 'not_found' => array()), 'file' => 'locale.pages.inc', ), ); diff --git a/core/modules/locale/locale.pages.inc b/core/modules/locale/locale.pages.inc index a109f5f..352f8bb 100644 --- a/core/modules/locale/locale.pages.inc +++ b/core/modules/locale/locale.pages.inc @@ -749,9 +749,10 @@ function theme_locale_translation_update_info($variables) { $description = $details = ''; // Build output for available updates. - if (isset($variables['updates'])) { + if (!empty($variables['updates'])) { + $modules = array(); + $releases = array(); if ($variables['updates']) { - $releases = array(); foreach ($variables['updates'] as $update) { $modules[] = $update['name']; $releases[] = t('@module (@date)', array('@module' => $update['name'], '@date' => format_date($update['timestamp'], 'html_date'))); diff --git a/core/modules/menu/menu.module b/core/modules/menu/menu.module index 93c2ed8..77cf76e 100644 --- a/core/modules/menu/menu.module +++ b/core/modules/menu/menu.module @@ -169,6 +169,9 @@ function menu_uri(Menu $menu) { */ function menu_theme() { return array( + 'block__menu' => array( + 'preprocess' => array('menu_preprocess_block'), + ), 'menu_overview_form' => array( 'file' => 'menu.admin.inc', 'render element' => 'form', diff --git a/core/modules/node/node.module b/core/modules/node/node.module index 0605169..e1c2c53 100644 --- a/core/modules/node/node.module +++ b/core/modules/node/node.module @@ -147,8 +147,12 @@ function node_help($path, $arg) { */ function node_theme() { return array( + 'block__node' => array( + 'preprocess' => array('node_preprocess_block'), + ), 'node' => array( 'render element' => 'elements', + 'preprocess' => array('template_preprocess_node'), 'template' => 'node', ), 'node_search_admin' => array( diff --git a/core/modules/overlay/overlay.module b/core/modules/overlay/overlay.module index 31289d5..1622ff1 100644 --- a/core/modules/overlay/overlay.module +++ b/core/modules/overlay/overlay.module @@ -73,13 +73,24 @@ function overlay_permission() { */ function overlay_theme() { return array( + 'html' => array( + 'preprocess' => array('overlay_preprocess_html'), + ), + 'maintenance_page' => array( + 'preprocess' => array('overlay_preprocess_maintenance_page'), + ), 'overlay' => array( 'render element' => 'page', + 'preprocess' => array('template_preprocess_overlay'), + 'process' => array('template_process_overlay'), 'template' => 'overlay', ), 'overlay_disable_message' => array( 'render element' => 'element', ), + 'page' => array( + 'preprocess' => array('overlay_preprocess_page'), + ), ); } diff --git a/core/modules/rdf/rdf.module b/core/modules/rdf/rdf.module index a2f74a3..6841200 100644 --- a/core/modules/rdf/rdf.module +++ b/core/modules/rdf/rdf.module @@ -435,6 +435,30 @@ function rdf_theme() { 'rdf_metadata' => array( 'variables' => array('metadata' => array()), ), + 'comment' => array( + 'preprocess' => array('rdf_preprocess_comment'), + ), + 'field' => array( + 'preprocess' => array('rdf_preprocess_field'), + ), + 'html' => array( + 'preprocess' => array('rdf_preprocess_html'), + ), + 'image' => array( + 'preprocess' => array('rdf_preprocess_image'), + ), + 'node' => array( + 'preprocess' => array('rdf_preprocess_node'), + ), + 'taxonomy_term' => array( + 'preprocess' => array('rdf_preprocess_taxonomy_term'), + ), + 'user' => array( + 'preprocess' => array('rdf_preprocess_user'), + ), + 'username' => array( + 'preprocess' => array('rdf_preprocess_username'), + ), ); } diff --git a/core/modules/search/search.module b/core/modules/search/search.module index f31488b..1c6ac1c 100644 --- a/core/modules/search/search.module +++ b/core/modules/search/search.module @@ -104,14 +104,19 @@ function search_help($path, $arg) { */ function search_theme() { return array( + 'block__search_form_block' => array( + 'preprocess' => array('search_preprocess_block'), + ), 'search_result' => array( 'variables' => array('result' => NULL, 'module' => NULL), 'file' => 'search.pages.inc', + 'preprocess' => array('template_preprocess_search_result'), 'template' => 'search-result', ), 'search_results' => array( 'variables' => array('results' => NULL, 'module' => NULL), 'file' => 'search.pages.inc', + 'preprocess' => array('template_preprocess_search_results'), 'template' => 'search-results', ), ); diff --git a/core/modules/search/tests/modules/search_embedded_form/search_embedded_form.module b/core/modules/search/tests/modules/search_embedded_form/search_embedded_form.module index 919d228..ea6a888 100644 --- a/core/modules/search/tests/modules/search_embedded_form/search_embedded_form.module +++ b/core/modules/search/tests/modules/search_embedded_form/search_embedded_form.module @@ -10,6 +10,16 @@ */ /** + * Implements hook_theme(). + */ +function search_embedded_form_theme() { + $theme['search_result'] = array( + 'preprocess' => array('search_embedded_form_preprocess_search_result'), + ); + return $theme; +} + +/** * Implements hook_menu(). */ function search_embedded_form_menu() { diff --git a/core/modules/shortcut/shortcut.module b/core/modules/shortcut/shortcut.module index d1458a8..745ce64 100644 --- a/core/modules/shortcut/shortcut.module +++ b/core/modules/shortcut/shortcut.module @@ -155,6 +155,12 @@ function shortcut_admin_paths() { */ function shortcut_theme() { return array( + 'block__shortcut' => array( + 'preprocess' => array('shortcut_preprocess_block'), + ), + 'page' => array( + 'preprocess' => array('shortcut_preprocess_page'), + ), 'shortcut_set_customize' => array( 'render element' => 'form', 'file' => 'shortcut.admin.inc', diff --git a/core/modules/statistics/statistics.module b/core/modules/statistics/statistics.module index 9d0c19e..f02179a 100644 --- a/core/modules/statistics/statistics.module +++ b/core/modules/statistics/statistics.module @@ -31,6 +31,16 @@ function statistics_help($path, $arg) { } /** + * Implements hook_theme(). + */ +function statistics_theme() { + $theme['block__statistics'] = array( + 'preprocess' => array('statistics_preprocess_block'), + ); + return $theme; +} + +/** * Implements hook_permission(). */ function statistics_permission() { diff --git a/core/modules/system/lib/Drupal/system/Tests/Theme/FastTest.php b/core/modules/system/lib/Drupal/system/Tests/Theme/FastTest.php index 843106e..21742e4 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Theme/FastTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Theme/FastTest.php @@ -38,6 +38,7 @@ function setUp() { * Tests access to user autocompletion and verify the correct results. */ function testUserAutocomplete() { + // @todo Replace with injected mock registry in DUTB test. $this->drupalLogin($this->account); $this->drupalGet('user/autocomplete', array('query' => array('q' => $this->account->name))); $this->assertRaw($this->account->name); 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..8160034 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Theme/RegistryTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Theme/RegistryTest.php @@ -2,7 +2,7 @@ /** * @file - * Definition of Drupal\system\Tests\Theme\RegistryTest. + * Definition of Drupal\system\Tests\Theme\RuntimeRegistryTest. */ namespace Drupal\system\Tests\Theme; @@ -13,7 +13,7 @@ /** * Tests the ThemeRegistry class. */ -class RegistryTest extends WebTestBase { +class RuntimeRegistryTest extends WebTestBase { /** * Modules to enable. @@ -22,11 +22,10 @@ class RegistryTest extends WebTestBase { */ public static $modules = array('theme_test'); - protected $profile = 'testing'; public static function getInfo() { return array( 'name' => 'ThemeRegistry', - 'description' => 'Tests the behavior of the ThemeRegistry class', + 'description' => 'Tests the runtime ThemeRegistry class.', 'group' => 'Theme', ); } diff --git a/core/modules/system/lib/Drupal/system/Tests/Theme/RegistryUnitTest.php b/core/modules/system/lib/Drupal/system/Tests/Theme/RegistryUnitTest.php new file mode 100644 index 0000000..105d380 --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/Theme/RegistryUnitTest.php @@ -0,0 +1,294 @@ + 'Theme Registry', + 'description' => 'Tests the theme registry.', + 'group' => 'Theme', + ); + } + + function setUp() { + parent::setUp(); + $this->registries = array(); + + $this->installSchema('system', 'variable'); + + theme_enable(array('test_theme')); + } + + /** + * Returns an registry instance for a certain theme. + * + * @param string $theme_name + * The name of the theme, which we need a registry for. + * + * @return \Drupal\Core\Theme\Registry + * Returns a registry instance. + */ + protected function getRegistry($theme_name) { + if (!isset($this->registries[$theme_name])) { + $this->registries[$theme_name] = new Registry($this->container->get('cache.cache'), $this->container->get('module_handler'), $theme_name); + } + return $this->registries[$theme_name]; + } + + /** + * Tests the behavior of the theme registry class. + */ + function xtestThemeScope() { + theme_enable(array('test_basetheme', 'test_subtheme')); + // Load test_theme's template.php to verify that its hook_theme() is not + // included for test_subtheme, even though it is defined. + include_once DRUPAL_ROOT . '/' . drupal_get_path('theme', 'test_theme') . '/template.php'; + // Retrieve a random hook from its definition. + + $registry = $this->getRegistry('test_subtheme')->get(); + $this->assertTrue(isset($registry)); + echo "
"; var_dump($registry); echo "
\n"; + } + + /** + * Tests registration of new hooks in a module. + */ + function xtestModuleNewHooks() { + $registry = $this->getRegistry('test_theme')->get(); + $path = drupal_get_path('module', 'theme_test'); + $file = $path . '/test_theme.inc'; + + // Verify that a new theme function is registered correctly. + $this->assertEqual($registry['test_theme_new_function'], array( + 'type' => 'module', + 'name' => 'theme_test', + 'theme path' => $path, + 'function' => 'theme_test_theme_new_function', + 'variables' => array(), + )); + } + + /** + * Tests registration of new hooks in a theme. + */ + function testThemeNewHooks() { + $registry = $this->getRegistry('test_theme')->get(); + $path = drupal_get_path('theme', 'test_theme'); + $file = $path . '/test_theme.inc'; + + // Verify that a new theme function is registered correctly. + $this->assertEqual($registry['test_theme_new_function'], array( + 'type' => 'theme', + 'name' => 'test_theme', + 'theme path' => $path, + 'function' => 'test_theme_test_theme_new_function', + 'variables' => array(), + )); + + // Verify theme functions with includes files. + $this->assertInArray($registry['test_theme_new_function_include']['includes'], $file); + $this->assertInArray($registry['test_theme_new_function_preprocess_include']['includes'], $file); + // Neither 'path' nor 'file' should be recorded for them, since they are + // functions, not templates. + $this->assertNotIsset($registry['test_theme_new_function_include'], 'path'); + $this->assertNotIsset($registry['test_theme_new_function_preprocess_include'], 'path'); + + // Verify theme functions with additional preprocess functions. + // Base and module preprocess functions are not expected to run for theme + // functions currently, due to performance reasons. + $this->assertEqual($registry['test_theme_new_function_preprocess']['preprocess'], array( + 'template_preprocess_test_theme_new_function_preprocess', + )); + $this->assertNotIsset($registry['test_theme_new_function_preprocess'], 'process'); + + // Verify custom theme function, explicitly defined in hook_theme(). + $this->assertEqual($registry['test_theme_new_function_custom']['function'], 'test_theme_new_function_customized'); + + // Verify that a new theme template is registered correctly. + $this->assertEqual($registry['test_theme_new_template'], array( + 'type' => 'theme', + 'name' => 'test_theme', + 'variables' => array(), + 'template' => 'test_theme_new_template', + 'theme path' => $path, + 'path' => $path . '/templates', + 'preprocess' => array( + 'template_preprocess', + 'theme_test_preprocess', + 'test_theme_preprocess', + ), + 'process' => array( + 'theme_test_process', + 'test_theme_process', + ), + )); + + // Verify theme templates with additional preprocess functions. + // - template_* preprocessors have to be sorted first. + // - Any additional should be sorted according to the extension type (i.e., + // in this case last, since the theme declared it). + $this->assertEqual($registry['test_theme_new_template_preprocess_include']['preprocess'], array( + 'template_preprocess', + 'template_preprocess_test_theme_new_template_preprocess_include', + 'theme_test_preprocess', + 'test_theme_preprocess', + 'test_theme_preprocess_test_theme_new_template_preprocess_include', + )); + $this->assertEqual($registry['test_theme_new_template_preprocess_include']['process'], array( + 'theme_test_process', + 'test_theme_process', + )); + } + + /** + * Tests extension of existing hooks by a theme. + */ + function testThemeHookExtensions() { + $registry = $this->getRegistry('test_theme')->get(); + $path = drupal_get_path('theme', 'test_theme'); + + // Verify that the non-existing theme hook is not contained. + $this->assertNotIsset($registry, 'non_existing_theme_hook'); + + // Verify the additional preprocess function for a template. + $this->assertInArray($registry['link']['preprocess'], 'test_theme_preprocess_link'); + $this->assertEqual($registry['link']['preprocess'], array( + 'test_theme_preprocess_link', + )); + $this->assertNotIsset($registry['link'], 'process'); + + // Verify the additional preprocess function for a template. + // Since this is the only and final theme, that preprocess should be last. + $this->assertInArray($registry['html']['preprocess'], 'test_theme_preprocess_html'); + $this->assertEqual($registry['html']['preprocess'], array( + // template_preprocess() + 'template_preprocess', + // template_preprocess_HOOK(), if any. (declarative) + 'template_preprocess_html', + // hook_preprocess(). + 'theme_test_preprocess', + // hook_preprocess_HOOK(), if any. (declarative) + 'theme_test_preprocess_html', + // THEME_preprocess(). + 'test_theme_preprocess', + // THEME_preprocess_HOOK(), if any. (declarative) + 'test_theme_preprocess_html', + )); + $this->assertEqual($registry['html']['process'], array( + // template_process_HOOK(), if any. (declarative) + 'template_process_html', + // hook_process(). + 'theme_test_process', + // THEME_process(). + 'test_theme_process', + )); + + // Verify that a theme is able to replace a function with a template. + $this->assertNotIsset($registry['theme_test_function_replace_template'], 'function'); + $this->assertEqual($registry['theme_test_function_replace_template']['theme path'], $path); + $this->assertEqual($registry['theme_test_function_replace_template']['path'], $path . '/templates'); + $this->assertEqual($registry['theme_test_function_replace_template']['template'], 'theme_test_function_replace_template'); + $this->assertEqual($registry['theme_test_function_replace_template']['preprocess'], array( + 'template_preprocess', + 'template_preprocess_theme_test_function_replace_template', + 'theme_test_preprocess', + 'theme_test_preprocess_theme_test_function_replace_template', + 'test_theme_preprocess', + 'test_theme_preprocess_theme_test_function_replace_template', + )); + $this->assertEqual($registry['theme_test_function_replace_template']['process'], array( + 'theme_test_process', + 'test_theme_process', + )); + } + + /** + * Tests that all registered theme hooks contain a 'theme path'. + * + * Separated from all other tests, since the 'theme path' is architecturally + * broken right now. + */ + function testHookThemePath() { + // drupal_find_theme_functions() and drupal_find_theme_templates() expect + // all theme hooks to have a 'theme path'. + $registry = $this->getRegistry('test_theme')->get(); + foreach ($registry as $hook => $info) { + $this->assertIsset($info, 'theme path', '@value key found for hook @hook.', array( + '@hook' => $hook, + )); + } + } + + /** + * Overrides TestBase::assertEqual(). + * + * - actual/expected vs. first/second. + * - Default message placeholders. + * - var_export() + * - Conditional wrapping of actual/expected in PRE elements. + * + * @see http://drupal.org/node/1601146 + */ + protected function assertEqual($actual, $expected, $message = NULL, $group = 'Other') { + $pass = $actual == $expected; + $actual = var_export($actual, TRUE); + $expected = var_export($expected, TRUE); + if (!isset($message)) { + $message = '@actual is equal to expected @expected.'; + } + if (strpos($actual, "\n") !== FALSE) { + $message = str_replace('@actual', '
@actual
', $message); + } + if (strpos($expected, "\n") !== FALSE) { + $message = str_replace('@expected', '
@expected
', $message); + } + return $this->assert($pass, format_string($message, array( + '@actual' => $actual, + '@expected' => $expected, + )), $group); + } + + protected function assertIsset($array, $key, $message = NULL, $args = array()) { + $message = isset($message) ? $message : 'Key @value found.'; + return $this->assert(isset($array[$key]), format_string($message, $args + array( + '@value' => var_export($key, TRUE), + ))); + } + + protected function assertNotIsset($array, $key, $message = NULL, $args = array()) { + $message = isset($message) ? $message : 'Key @value not found.'; + return $this->assert(!isset($array[$key]), format_string($message, $args + array( + '@value' => var_export($key, TRUE), + ))); + } + + protected function assertInArray($array, $value, $message = NULL, $args = array()) { + $message = isset($message) ? $message : 'Value @value found.'; + return $this->assert(in_array($value, $array, TRUE), format_string($message, $args + array( + '@value' => var_export($value, TRUE), + ))); + } + +} 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 20ccc2b..2ca9948 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Theme/ThemeTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Theme/ThemeTest.php @@ -71,7 +71,7 @@ function testThemeSuggestions() { */ function testPreprocessForSuggestions() { // Test with both an unprimed and primed theme registry. - drupal_theme_rebuild(); + $this->container->get('theme.registry')->reset(); for ($i = 0; $i < 2; $i++) { $this->drupalGet('theme-test/suggestion'); $this->assertText('Theme hook implementor=test_theme_theme_test__suggestion(). Foo=template_preprocess_theme_test', 'Theme hook suggestion ran with data available from a preprocess function for the base hook.'); @@ -79,6 +79,22 @@ function testPreprocessForSuggestions() { } /** + * Ensures suggestion preprocess functions run even for default implementations. + * + * The theme hook used by this test has its base preprocess function in a + * separate file, so this test also ensures that that file is correctly loaded + * when needed. + */ + function testSuggestionPreprocessForDefaults() { + // Test with both an unprimed and primed theme registry. + $this->container->get('theme.registry')->reset(); + for ($i = 0; $i < 2; $i++) { + $this->drupalGet('theme-test/preprocess-suggestion'); + $this->assertText('Theme hook implementor=test_theme_theme_test_preprocess__suggestion(). Foo=template_preprocess_theme_test_preprocess', 'Theme hook ran with data available from a preprocess function for the suggested hook.'); + } + } + + /** * Ensure page-front template suggestion is added when on front page. */ function testFrontPageThemeSuggestion() { @@ -204,14 +220,8 @@ function testClassLoading() { * Tests drupal_find_theme_templates(). */ public function testFindThemeTemplates() { - $cache = array(); - - // Prime the theme cache. - foreach (module_implements('theme') as $module) { - _theme_process_registry($cache, $module, 'module', $module, drupal_get_path('module', $module)); - } - - $templates = drupal_find_theme_templates($cache, '.tpl.php', drupal_get_path('theme', 'test_theme')); + $registry = $this->container->get('theme.registry')->get(); + $templates = drupal_find_theme_templates($registry, '.tpl.php', drupal_get_path('theme', 'test_theme')); $this->assertEqual($templates['node__1']['template'], 'node--1', 'Template node--1.tpl.php was found in test_theme.'); } } diff --git a/core/modules/system/lib/Drupal/system/Tests/Theme/ThemeTestTwig.php b/core/modules/system/lib/Drupal/system/Tests/Theme/ThemeTestTwig.php index 5ac99e0..656a1d7 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Theme/ThemeTestTwig.php +++ b/core/modules/system/lib/Drupal/system/Tests/Theme/ThemeTestTwig.php @@ -37,7 +37,7 @@ function setUp() { /** * Ensures a themes template is overrideable based on the 'template' filename. */ - function testTemplateOverride() { + function _testTemplateOverride() { config('system.theme') ->set('default', 'test_theme_twig') ->save(); @@ -48,14 +48,8 @@ function testTemplateOverride() { /** * Tests drupal_find_theme_templates */ - function testFindThemeTemplates() { - - $cache = array(); - - // Prime the theme cache - foreach (module_implements('theme') as $module) { - _theme_process_registry($cache, $module, 'module', $module, drupal_get_path('module', $module)); - } + function _testFindThemeTemplates() { + $cache = $this->container->get('theme.registry')->get(); // Check for correct content // @todo Remove this tests once double engine code is removed @@ -77,7 +71,11 @@ function testTwigVariableDataTypes() { config('system.theme') ->set('default', 'test_theme_twig') ->save(); - $this->drupalGet('twig-theme-test/php-variables'); + $this->rebuildContainer(); + $this->resetAll(); + $this->container->get('theme.registry')->reset(); + + $this->drupalSetContent(theme('twig_theme_test_php_variables')); foreach (_test_theme_twig_php_values() as $type => $value) { $this->assertRaw('
  • ' . $type . ': ' . $value['expected'] . '
  • '); } 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 0bcd1a6..a0c51c5 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 (module_implements('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_twig')); $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 2e84924..80efdd0 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Theme/TwigSettingsTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Theme/TwigSettingsTest.php @@ -77,19 +77,24 @@ function testTwigDebugOverride() { */ function testTwigCacheOverride() { $extension = twig_extension(); - theme_enable(array('test_theme_twig')); + config('system.theme') ->set('default', 'test_theme_twig') ->save(); - - $cache = array(); - // Prime the theme cache. - foreach (module_implements('theme') as $module) { - _theme_process_registry($cache, $module, 'module', $module, drupal_get_path('module', $module)); - } + theme_enable(array('test_theme_twig')); + // 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_twig')); + $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 5360a83..9733670 100644 --- a/core/modules/system/system.api.php +++ b/core/modules/system/system.api.php @@ -1600,21 +1600,13 @@ function hook_permission() { * - base hook: A string declaring the base theme hook if this theme * implementation is actually implementing a suggestion for another theme * hook. - * - pattern: A regular expression pattern to be used to allow this theme - * implementation to have a dynamic name. The convention is to use __ to - * differentiate the dynamic portion of the theme. For example, to allow - * forums to be themed individually, the pattern might be: 'forum__'. Then, - * when the forum is themed, call: - * @code - * theme(array('forum__' . $tid, 'forum'), $forum) - * @endcode * - preprocess functions: A list of functions used to preprocess this data. * Ordinarily this won't be used; it's automatically filled in. By default, * for a module this will be filled in as template_preprocess_HOOK. For * a theme this will be filled in as phptemplate_preprocess and * phptemplate_preprocess_HOOK as well as themename_preprocess and * themename_preprocess_HOOK. - * - override preprocess functions: Set to TRUE when a theme does NOT want + * - no preprocess: Set to TRUE when a theme does NOT want * the standard preprocess functions to run. This can be used to give a * theme FULL control over how variables are set. For example, if a theme * wants total control over how certain variables in the page.tpl.php are @@ -1660,8 +1652,9 @@ function hook_theme($existing, $type, $theme, $path) { * Changes here will not be visible until the next cache clear. * * 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(). + * processed information returned from hook_theme(). + * + * @see \Drupal\Core\Theme\Registry::$registry * * For example: * @code @@ -1684,7 +1677,6 @@ function hook_theme($existing, $type, $theme, $path) { * The entire cache of theme registry information, post-processing. * * @see hook_theme() - * @see _theme_process_registry() */ function hook_theme_registry_alter(&$theme_registry) { // Kill the next/previous forum topic navigation links. diff --git a/core/modules/system/system.module b/core/modules/system/system.module index c3c6584..c39c028 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -138,6 +138,9 @@ function system_help($path, $arg) { */ function system_theme() { return array_merge(drupal_common_theme(), array( + 'block' => array( + 'preprocess' => array('system_preprocess_block'), + ), 'system_themes_page' => array( 'variables' => array('theme_groups' => NULL), 'file' => 'system.admin.inc', @@ -190,6 +193,7 @@ function system_theme() { 'render element' => 'form', ), 'system_plugin_ui_form' => array( + 'preprocess' => array('template_preprocess_system_plugin_ui_form'), 'template' => 'system-plugin-ui-form', 'render element' => 'form', ), diff --git a/core/modules/system/tests/modules/theme_test/theme_test.inc b/core/modules/system/tests/modules/theme_test/theme_test.inc index 6cde683..e43df50 100644 --- a/core/modules/system/tests/modules/theme_test/theme_test.inc +++ b/core/modules/system/tests/modules/theme_test/theme_test.inc @@ -13,3 +13,22 @@ function theme_theme_test($variables) { function template_preprocess_theme_test(&$variables) { $variables['foo'] = 'template_preprocess_theme_test'; } + +/** + * Returns HTML for the 'theme_test_preprocess' theme hook used by tests. + */ +function theme_theme_test_preprocess($variables) { + return 'Theme hook implementor=theme_theme_test_preprocess(). Foo=' . $variables['foo']; +} + +/** + * Implements hook_preprocess_HOOK() for theme_theme_test_preprocess(). + * + * Despite not having a corresponding theme function for this suggestion, the + * specific preprocess function should still be used. + */ +function template_preprocess_theme_test_preprocess(&$variables) { + $variables['foo'] = 'template_preprocess_theme_test_preprocess'; +} + + 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 137fdfc..c9ea75e 100644 --- a/core/modules/system/tests/modules/theme_test/theme_test.module +++ b/core/modules/system/tests/modules/theme_test/theme_test.module @@ -4,15 +4,37 @@ * Implements hook_theme(). */ function theme_test_theme($existing, $type, $theme, $path) { + // Theme registry tests. + // @see test_theme_theme() + $items['theme_test_function_replace_template'] = array( + 'variables' => array(), + 'preprocess' => array( + 'template_preprocess_theme_test_function_replace_template', + 'theme_test_preprocess_theme_test_function_replace_template', + ), + ); + $items['html'] = array( + 'preprocess' => array('theme_test_preprocess_html'), + ); + + $items['theme_test'] = array( 'file' => 'theme_test.inc', 'variables' => array('foo' => ''), + 'preprocess' => array('template_preprocess_theme_test'), + ); + $items['theme_test_preprocess'] = array( + 'file' => 'theme_test.inc', + 'variables' => array('foo' => ''), + 'preprocess' => array('template_preprocess_theme_test_preprocess'), ); $items['theme_test_template_test'] = array( 'template' => 'theme_test.template_test', + 'variables' => array(), ); $items['theme_test_template_test_2'] = array( 'template' => 'theme_test.template_test', + 'variables' => array(), ); $items['theme_test_foo'] = array( 'variables' => array('foo' => NULL), @@ -42,6 +64,13 @@ function theme_test_menu() { 'theme callback' => '_theme_custom_theme', 'type' => MENU_CALLBACK, ); + $items['theme-test/preprocess-suggestion'] = array( + 'title' => 'Preprocess suggestion', + 'page callback' => '_theme_test_preprocess_suggestion', + 'access callback' => TRUE, + 'theme callback' => '_theme_custom_theme', + 'type' => MENU_CALLBACK, + ); $items['theme-test/alter'] = array( 'title' => 'Suggestion', 'page callback' => '_theme_test_alter', @@ -75,26 +104,13 @@ function theme_test_init() { // First, force the theme registry to be rebuilt on this page request. This // allows us to test a full initialization of the theme system in the code // below. - drupal_theme_rebuild(); + Drupal::service('theme.registry')->reset(); // Next, initialize the theme system by storing themed text in a global // variable. We will use this later in theme_test_hook_init_page_callback() // to test that even when the theme system is initialized this early, it is // still capable of returning output and theming the page as a whole. $GLOBALS['theme_test_output'] = theme('more_link', array('url' => 'user', 'title' => 'Themed output generated in hook_init()')); } - if (arg(0) == 'user' && arg(1) == 'autocomplete') { - // 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()); - } -} - -/** - * Fake registry loading callback. - */ -function _theme_test_load_registry() { - print 'registry initialized'; - return array(); } /** @@ -158,6 +174,29 @@ function _theme_test_suggestion() { } /** + * Page callback, calls a theme hook suggestion. + */ +function _theme_test_preprocess_suggestion() { + return theme(array('theme_test_preprocess__suggestion', 'theme_test_preprocess'), array()); +} + +/** + * Implements hook_preprocess(). + * + * @see \Drupal\system\Tests\Theme\RegistryUnitTest + */ +function theme_test_preprocess(&$variables) { +} + +/** + * Implements hook_process(). + * + * @see \Drupal\system\Tests\Theme\RegistryUnitTest + */ +function theme_test_process(&$variables) { +} + +/** * Implements hook_preprocess_HOOK() for html.tpl.php. */ function theme_test_preprocess_html(&$variables) { diff --git a/core/modules/system/tests/modules/twig_theme_test/twig_theme_test.module b/core/modules/system/tests/modules/twig_theme_test/twig_theme_test.module index 2ca2cd0..484e263 100644 --- a/core/modules/system/tests/modules/twig_theme_test/twig_theme_test.module +++ b/core/modules/system/tests/modules/twig_theme_test/twig_theme_test.module @@ -6,6 +6,7 @@ function twig_theme_test_theme($existing, $type, $theme, $path) { $items['twig_theme_test_php_variables'] = array( 'template' => 'twig_theme_test.php_variables', + 'variables' => array(), ); return $items; } diff --git a/core/modules/system/tests/themes/test_theme/test_theme.theme b/core/modules/system/tests/themes/test_theme/test_theme.theme index 8275818..b700e95 100644 --- a/core/modules/system/tests/themes/test_theme/test_theme.theme +++ b/core/modules/system/tests/themes/test_theme/test_theme.theme @@ -1,6 +1,139 @@ array()); + $include = array('file' => 'test_theme.inc'); + + // Functions. + + // New function. + $theme['test_theme_new_function'] = $new; + // New function in an include file. + $theme['test_theme_new_function_include'] = $new + $include; + // New function with template preprocess function. + $theme['test_theme_new_function_preprocess'] = $new + array( + 'preprocess' => array( + 'template_preprocess_test_theme_new_function_preprocess', + ), + ); + // New function with template preprocess in an include file. + $theme['test_theme_new_function_preprocess_include'] = $new + $include + array( + 'preprocess' => array( + 'template_preprocess_test_theme_new_function_preprocess_include', + 'test_theme_preprocess_test_theme_new_function_preprocess_include', + ), + ); + $theme['test_theme_new_function_custom'] = $new + array( + 'function' => 'test_theme_new_function_customized', + ); + + // Templates. + + // New template. + $theme['test_theme_new_template'] = $new + array( + 'template' => 'test_theme_new_template', + ); + // New template with template preprocess in an include file. + $theme['test_theme_new_template_preprocess_include'] = $new + $include + array( + 'template' => 'test_theme_new_template_preprocess_include', + 'preprocess' => array( + 'template_preprocess_test_theme_new_template_preprocess_include', + 'test_theme_preprocess_test_theme_new_template_preprocess_include', + ), + ); + // Module function replaced with a template. + $theme['theme_test_function_replace_template'] = array( + 'template' => 'theme_test_function_replace_template', + // And while being there, add another preproces, too :) + 'preprocess' => array('test_theme_preprocess_theme_test_function_replace_template'), + ); + + // Preprocess functions. + + // Preprocess function for a non-existing theme hook. + $theme['non_existing_theme_hook'] = array( + 'preprocess' => array('test_theme_preprocess_non_existing_theme_hook'), + ); + // Preprocess function for an existing function. + // This implementation actually exists, so as to not break testing sites in + // case test_theme is enabled. + $theme['link'] = array( + 'preprocess' => array('test_theme_preprocess_link'), + ); + // Preprocess function for an existing template. + // This implementation actually exists, so as to not break testing sites in + // case test_theme is enabled. + $theme['html'] = array( + 'preprocess' => array('test_theme_preprocess_html'), + ); + // @todo preprocess + file/include + // @todo no preprocess + + // (Preprocess functions for) Theme hook suggestions. + + // @todo base hook + // @todo base hook of base hook + // @todo include file + // @todo replace function with template for suggestion only + + return $theme; +} + +/** + * Implements THEME_preprocess(). + * + * @see _test_theme_theme_registry() + * @see theme() + */ +function test_theme_preprocess(&$variables, $hook) { +} + +/** + * Implements THEME_process(). + * + * @see _test_theme_theme_registry() + * @see theme() + */ +function test_theme_process(&$variables, $hook) { +} + +/** + * Preprocess function for theme('link'). + * + * @see _test_theme_theme_registry() + */ +function test_theme_preprocess_link(&$variables) { +} + +/** + * Preprocess function for theme('html'). + * + * @see _test_theme_theme_registry() + */ +function test_theme_preprocess_html(&$variables) { +} + +/** * Tests a theme overriding a suggestion of a base theme hook. */ function test_theme_theme_test__suggestion($variables) { @@ -8,6 +141,13 @@ function test_theme_theme_test__suggestion($variables) { } /** + * Tests a theme overriding a default hook with a suggestion. + */ +function test_theme_theme_test_preprocess__suggestion($variables) { + return 'Theme hook implementor=test_theme_theme_test_preprocess__suggestion(). Foo=' . $variables['foo']; +} + +/** * Tests a theme implementing an alter hook. * * The confusing function name here is due to this being an implementation of diff --git a/core/modules/system/tests/themes/test_theme_twig/template.php b/core/modules/system/tests/themes/test_theme_twig/template.php index a4abe2d..b3d9bbc 100644 --- a/core/modules/system/tests/themes/test_theme_twig/template.php +++ b/core/modules/system/tests/themes/test_theme_twig/template.php @@ -1,2 +1 @@ array( + 'preprocess' => array('test_theme_twig_preprocess_twig_theme_test_php_variables'), + ), + ); +} + +/** * Implements THEME_preprocess_twig_theme_test_php_variables(). */ function test_theme_twig_preprocess_twig_theme_test_php_variables(&$variables) { diff --git a/core/modules/system/theme.api.php b/core/modules/system/theme.api.php index 399aca8..466c2e1 100644 --- a/core/modules/system/theme.api.php +++ b/core/modules/system/theme.api.php @@ -5,6 +5,8 @@ * @{ * Functions and templates for the user interface to be implemented by themes. * + * @todo Review these docs for new declarative preprocess functions. + * * Drupal's presentation layer is a pluggable system known as the theme * layer. Each theme can take control over most of Drupal's output, and * has complete control over the CSS. @@ -120,7 +122,7 @@ function hook_preprocess(&$variables, $hook) { } if (!isset($hooks)) { - $hooks = theme_get_registry(); + $hooks = \Drupal::service('theme.registry')->getRuntime(); } // Determine the primary theme function argument. @@ -145,14 +147,16 @@ function hook_preprocess(&$variables, $hook) { } /** - * Preprocess theme variables for a specific theme hook. + * Preprocess callback for theme variables of a specific theme hook. * - * This hook allows modules to preprocess theme variables for a specific theme - * hook. It should only be used if a module needs to override or add to the - * theme preprocessing for a theme hook it didn't define. + * This is not a hook. Preprocess callbacks for specific theme hooks need to be + * manually registered via hook_theme(). * * For more detailed information, see theme(). * + * The function name MAY not follow this example, but it is RECOMMENDED to + * follow the function name pattern of hook_preprocess_HOOK(). + * * @param $variables * The variables array (modify in place). */ @@ -193,14 +197,16 @@ function hook_process(&$variables, $hook) { } /** - * Process theme variables for a specific theme hook. + * Process callback for theme variables of a specific theme hook. * - * This hook allows modules to process theme variables for a specific theme - * hook. It should only be used if a module needs to override or add to the - * theme processing for a theme hook it didn't define. + * This is not a hook. Process callbacks for specific theme hooks need to be + * manually registered via hook_theme(). * * For more detailed information, see theme(). * + * The function name MAY not follow this example, but it is RECOMMENDED to + * follow the function name pattern of hook_process_HOOK(). + * * @param $variables * The variables array (modify in place). */ diff --git a/core/modules/taxonomy/taxonomy.module b/core/modules/taxonomy/taxonomy.module index d576b54..ae90167 100644 --- a/core/modules/taxonomy/taxonomy.module +++ b/core/modules/taxonomy/taxonomy.module @@ -233,6 +233,7 @@ function taxonomy_theme() { return array( 'taxonomy_term' => array( 'render element' => 'elements', + 'preprocess' => array('template_preprocess_taxonomy_term'), 'template' => 'taxonomy-term', ), ); diff --git a/core/modules/toolbar/toolbar.module b/core/modules/toolbar/toolbar.module index 5bc4534..87399a4 100644 --- a/core/modules/toolbar/toolbar.module +++ b/core/modules/toolbar/toolbar.module @@ -49,6 +49,7 @@ function toolbar_theme($existing, $type, $theme, $path) { ); $items['toolbar_tab_wrapper'] = array( 'render element' => 'element', + 'preprocess' => array('template_preprocess_toolbar_tab_wrapper'), ); $items['toolbar_tray_wrapper'] = array( 'render element' => 'element', diff --git a/core/modules/tour/tour.module b/core/modules/tour/tour.module index fe9bc1a..f9e5ef3 100644 --- a/core/modules/tour/tour.module +++ b/core/modules/tour/tour.module @@ -103,6 +103,17 @@ function tour_toolbar() { } /** + * Implements hook_theme(). + */ +function tour_theme() { + return array( + 'page' => array( + 'preprocess' => array('tour_preprocess_page'), + ), + ); +} + +/** * Implements hook_preprocess_HOOK() for page.tpl.php. */ function tour_preprocess_page(&$variables) { diff --git a/core/modules/user/user.module b/core/modules/user/user.module index 3ae6154..f48bb71 100644 --- a/core/modules/user/user.module +++ b/core/modules/user/user.module @@ -91,8 +91,12 @@ function user_help($path, $arg) { */ function user_theme() { return array( + 'block__user' => array( + 'preprocess' => array('user_preprocess_block'), + ), 'user' => array( 'render element' => 'elements', + 'preprocess' => array('template_preprocess_user'), 'template' => 'user', 'file' => 'user.pages.inc', ), @@ -108,6 +112,8 @@ function user_theme() { 'variables' => array('signature' => NULL), ), 'username' => array( + 'preprocess' => array('template_preprocess_username'), + 'process' => array('template_process_username'), 'variables' => array('account' => NULL), ), ); diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/display/DisplayPluginBase.php b/core/modules/views/lib/Drupal/views/Plugin/views/display/DisplayPluginBase.php index 5d3b60e..6b6df5a 100644 --- a/core/modules/views/lib/Drupal/views/Plugin/views/display/DisplayPluginBase.php +++ b/core/modules/views/lib/Drupal/views/Plugin/views/display/DisplayPluginBase.php @@ -1725,54 +1725,19 @@ 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_container()->get('cache.theme'); + $this->theme_registry = new Registry($cache_theme, $theme->name); } // If there's a theme engine involved, we also need to know its extension @@ -2031,16 +1996,8 @@ 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']); - drupal_theme_initialize(); + // Analyzes the data of the theme registry. + \Drupal::service('theme.registry')->reset(); $form_state['rerender'] = TRUE; $form_state['rebuild'] = TRUE; diff --git a/core/modules/views/tests/views_test_data/views_test_data.module b/core/modules/views/tests/views_test_data/views_test_data.module index 9dc5159..a3222d5 100644 --- a/core/modules/views/tests/views_test_data/views_test_data.module +++ b/core/modules/views/tests/views_test_data/views_test_data.module @@ -8,6 +8,20 @@ use Drupal\views\ViewExecutable; /** + * Implements hook_theme(). + */ +function views_test_data_theme() { + $theme['views_view_mapping_test'] = array( + 'render element' => 'element', + 'preprocess' => array('template_preprocess_views_view_mapping_test'), + ); + $theme['views_view_table'] = array( + 'preprocess' => array('views_test_data_preprocess_views_view_table'), + ); + return $theme; +} + +/** * Implements hook_permission(). */ function views_test_data_permission() { diff --git a/core/modules/views/views.module b/core/modules/views/views.module index 9075033..269c4b3 100644 --- a/core/modules/views/views.module +++ b/core/modules/views/views.module @@ -81,6 +81,16 @@ function views_pre_render_view_element($element) { function views_theme($existing, $type, $theme, $path) { Drupal::moduleHandler()->loadInclude('views', 'inc', 'views.theme'); + $hooks['comment'] = array( + 'preprocess' => array('views_preprocess_comment'), + ); + $hooks['html'] = array( + 'preprocess' => array('views_preprocess_html'), + ); + $hooks['node'] = array( + 'preprocess' => array('views_preprocess_node'), + ); + // Some quasi clever array merging here. $base = array( 'file' => 'views.theme.inc', @@ -89,7 +99,6 @@ function views_theme($existing, $type, $theme, $path) { // Our extra version of pager from pager.inc $hooks['views_mini_pager'] = $base + array( 'variables' => array('tags' => array(), 'quantity' => 10, 'element' => 0, 'parameters' => array()), - 'pattern' => 'views_mini_pager__', ); $variables = array( @@ -108,12 +117,13 @@ function views_theme($existing, $type, $theme, $path) { // Default view themes $hooks['views_view_field'] = $base + array( - 'pattern' => 'views_view_field__', 'variables' => array('view' => NULL, 'field' => NULL, 'row' => NULL), + 'preprocess' => array('template_preprocess_views_view_field'), ); $hooks['views_view_grouping'] = $base + array( 'variables' => array('view' => NULL, 'grouping' => NULL, 'grouping_level' => NULL, 'rows' => NULL, 'title' => NULL), 'template' => 'views-view-grouping', + 'preprocess' => array('template_preprocess_views_view_grouping'), ); $plugins = views_get_plugin_definitions(); @@ -128,7 +138,6 @@ function views_theme($existing, $type, $theme, $path) { } $hooks[$def['theme']] = array( - 'pattern' => $def['theme'] . '__', 'variables' => $variables[$type], ); @@ -147,13 +156,21 @@ function views_theme($existing, $type, $theme, $path) { } if (isset($def['theme_path']) && isset($def['theme_file'])) { $include = DRUPAL_ROOT . '/' . $def['theme_path'] . '/' . $def['theme_file']; - if (is_file($include)) { - require_once $include; - } + include_once $include; } if (!function_exists('theme_' . $def['theme'])) { $hooks[$def['theme']]['template'] = drupal_clean_css_identifier($def['theme']); } + // @todo The theme registry no longer relies on code to be loaded and + // requires explicit registration of (pre)process functions instead. + // Move these definitions into Views plugin declarations, and remove all + // file inclusions from this hook_theme() implementation. + if (function_exists('template_preprocess_' . $def['theme'])) { + $hooks[$def['theme']]['preprocess'][] = 'template_preprocess_' . $def['theme']; + } + if (function_exists('template_process_' . $def['theme'])) { + $hooks[$def['theme']]['process'][] = 'template_process_' . $def['theme']; + } } } @@ -163,13 +180,12 @@ function views_theme($existing, $type, $theme, $path) { $hooks['views_exposed_form'] = $base + array( 'template' => 'views-exposed-form', - 'pattern' => 'views_exposed_form__', + 'preprocess' => array('template_preprocess_views_exposed_form'), 'render element' => 'form', ); $hooks['views_more'] = $base + array( 'template' => 'views-more', - 'pattern' => 'views_more__', 'variables' => array('more_url' => NULL, 'link_text' => 'more', 'view' => NULL), ); diff --git a/core/modules/views_ui/views_ui.module b/core/modules/views_ui/views_ui.module index f169fd0..a55296d 100644 --- a/core/modules/views_ui/views_ui.module +++ b/core/modules/views_ui/views_ui.module @@ -123,11 +123,13 @@ function views_ui_theme() { 'variables' => array('description' => '', 'link' => '', 'settings_links' => array(), 'overridden' => FALSE, 'defaulted' => FALSE, 'description_separator' => TRUE, 'class' => array()), 'template' => 'views-ui-display-tab-setting', 'file' => 'views_ui.theme.inc', + 'preprocess' => array('template_preprocess_views_ui_display_tab_setting'), ), 'views_ui_display_tab_bucket' => array( 'render element' => 'element', 'template' => 'views-ui-display-tab-bucket', 'file' => 'views_ui.theme.inc', + 'preprocess' => array('template_preprocess_views_ui_display_tab_bucket'), ), 'views_ui_rearrange_filter_form' => array( 'render element' => 'form', @@ -142,6 +144,7 @@ function views_ui_theme() { 'views_ui_view_info' => array( 'variables' => array('view' => NULL, 'base' => NULL), 'file' => 'views_ui.theme.inc', + 'preprocess' => array('template_preprocess_views_ui_view_info'), ), // Group of filters. @@ -166,6 +169,7 @@ function views_ui_theme() { 'views_ui_view_preview_section' => array( 'variables' => array('view' => NULL, 'section' => NULL, 'content' => NULL, 'links' => ''), 'file' => 'views_ui.theme.inc', + 'preprocess' => array('template_preprocess_views_ui_view_preview_section'), ), // Generic container wrapper, to use instead of theme_container when an id @@ -174,6 +178,10 @@ function views_ui_theme() { 'render element' => 'element', 'file' => 'views_ui.theme.inc', ), + + 'views_view' => array( + 'preprocess' => array('views_ui_preprocess_views_view'), + ), ); } diff --git a/core/themes/bartik/bartik.theme b/core/themes/bartik/bartik.theme index 6245b4d..8e1dc5b 100644 --- a/core/themes/bartik/bartik.theme +++ b/core/themes/bartik/bartik.theme @@ -6,6 +6,24 @@ */ /** + * Implements hook_theme(). + */ +function bartik_theme() { + $theme['html'] = array( + 'preprocess' => array('bartik_preprocess_html'), + 'process' => array('bartik_process_html'), + ); + $theme['maintenance_page'] = array( + 'preprocess' => array('bartik_preprocess_maintenance_page'), + 'process' => array('bartik_process_maintenance_page'), + ); + $theme['page'] = array( + 'process' => array('bartik_process_page'), + ); + return $theme; +} + +/** * Implements hook_preprocess_HOOK() for html.tpl.php. * * Adds body classes if certain regions have content. diff --git a/core/themes/seven/seven.theme b/core/themes/seven/seven.theme index 61212fe..a66831b 100644 --- a/core/themes/seven/seven.theme +++ b/core/themes/seven/seven.theme @@ -6,6 +6,25 @@ */ /** + * Implements hook_theme(). + */ +function seven_theme() { + $theme['html'] = array( + 'preprocess' => array('seven_preprocess_html'), + ); + $theme['install_page'] = array( + 'preprocess' => array('seven_preprocess_install_page'), + ); + $theme['maintenance_page'] = array( + 'preprocess' => array('seven_preprocess_maintenance_page'), + ); + $theme['page'] = array( + 'preprocess' => array('seven_preprocess_page'), + ); + return $theme; +} + +/** * Implements hook_preprocess_HOOK() for maintenance-page.tpl.php. */ function seven_preprocess_maintenance_page(&$vars) {