diff --git a/core/lib/Drupal/Core/Render/theme.api.php b/core/lib/Drupal/Core/Render/theme.api.php index 44fcdd0..5d20ba9 100644 --- a/core/lib/Drupal/Core/Render/theme.api.php +++ b/core/lib/Drupal/Core/Render/theme.api.php @@ -1092,87 +1092,16 @@ function hook_page_bottom(array &$page_bottom) { * The directory path of the theme or module, so that it doesn't need to be * looked up. * - * @return array + * @return \Drupal\Core\Theme\ThemeHook[] * An associative array of information about theme implementations. The keys * on the outer array are known as "theme hooks". For theme suggestions, * instead of the array key being the base theme hook, the key is a theme * suggestion name with the format 'base_hook_name__sub_hook_name'. * For render elements, the key is the machine name of the render element. - * The array values are themselves arrays containing information about the - * theme hook and its implementation. Each information array must contain + * The array values are value objects containing information about the + * theme hook and its implementation. Each theme hook object must specify * either a 'variables' element (for using a #theme element) or a * 'render element' element (for render elements), but not both. - * The following elements may be part of each information array: - * - variables: Only used for #theme in render array: an array of variables, - * where the array keys are the names of the variables, and the array - * values are the default values if they are not given in the render array. - * Template implementations receive each array key as a variable in the - * template file (so they must be legal PHP/Twig variable names). Function - * implementations are passed the variables in a single $variables function - * argument. If you are using these variables in a render array, prefix the - * variable names defined here with a #. - * - render element: Used for render element items only: the name of the - * renderable element or element tree to pass to the theme function. This - * name is used as the name of the variable that holds the renderable - * element or tree in preprocess and process functions. - * - file: The file the implementation resides in. This file will be included - * prior to the theme being rendered, to make sure that the function or - * preprocess function (as needed) is actually loaded. - * - path: Override the path of the file to be used. Ordinarily the module or - * theme path will be used, but if the file will not be in the default - * path, include it here. This path should be relative to the Drupal root - * directory. - * - template: If specified, the theme implementation is a template file, and - * this is the template name. Do not add 'html.twig' on the end of the - * template name. The extension will be added automatically by the default - * rendering engine (which is Twig.) If 'path' is specified, 'template' - * should also be specified. If neither 'template' nor 'function' are - * specified, a default template name will be assumed. For example, if a - * module registers the 'search_result' theme hook, 'search-result' will be - * assigned as its template name. - * - function: (deprecated in Drupal 8.0.x, will be removed in Drupal 9.0.x) - * If specified, this will be the function name to invoke for this - * implementation. If neither 'template' nor 'function' are specified, a - * default template name will be assumed. See above for more details. - * - base hook: Used for theme suggestions only: the base theme hook name. - * Instead of this suggestion's implementation being used directly, the base - * hook will be invoked with this implementation as its first suggestion. - * The base hook's files will be included and the base hook's preprocess - * functions will be called in addition to any suggestion's preprocess - * functions. If an implementation of hook_theme_suggestions_HOOK() (where - * HOOK is the base hook) changes the suggestion order, a different - * suggestion may be used in place of this suggestion. If after - * hook_theme_suggestions_HOOK() this suggestion remains the first - * suggestion, then this suggestion's function or template will be used to - * generate the rendered output. - * - 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 rendered, following render array can be used: - * @code - * $render_array = array( - * '#theme' => array('forum__' . $tid, 'forum'), - * '#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 twig_preprocess and - * twig_preprocess_HOOK as well as themename_preprocess and - * themename_preprocess_HOOK. - * - override preprocess functions: 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.html.twig are - * set, this can be set to true. Please keep in mind that when this is used - * by a theme, that theme becomes responsible for making sure necessary - * variables are set. - * - type: (automatically derived) Where the theme hook is defined: - * 'module', 'theme_engine', or 'theme'. - * - theme path: (automatically derived) The directory path of the theme or - * module, so that it doesn't need to be looked up. * * @see themeable * @see hook_theme_registry_alter() diff --git a/core/lib/Drupal/Core/Theme/Registry.php b/core/lib/Drupal/Core/Theme/Registry.php index a0af702..226f1bd 100644 --- a/core/lib/Drupal/Core/Theme/Registry.php +++ b/core/lib/Drupal/Core/Theme/Registry.php @@ -215,7 +215,7 @@ protected function init($theme_name = NULL) { /** * Returns the complete theme registry from cache or rebuilds it. * - * @return array + * @return \Drupal\Core\Theme\ThemeHook[] * The complete theme registry data array. * * @see Registry::$registry @@ -343,6 +343,7 @@ protected function build() { // and preprocess functions comes first. foreach (array_reverse($this->theme->getBaseThemes()) as $base) { // If the base theme uses a theme engine, process its hooks. + /** @var \Drupal\Core\Theme\ActiveTheme $base */ $base_path = $base->getPath(); if ($this->theme->getEngine()) { $this->processExtension($cache, $this->theme->getEngine(), 'base_theme_engine', $base->getName(), $base_path); @@ -366,12 +367,6 @@ protected function build() { $this->themeManager->alterForTheme($this->theme, 'theme_registry', $cache); // @todo Implement more reduction of the theme registry entry. - // Optimize the registry to not have empty arrays for functions. - foreach ($cache as $hook => $info) { - if (empty($info['preprocess functions'])) { - unset($cache[$hook]['preprocess functions']); - } - } $this->registry[$this->theme->getName()] = $cache; return $this->registry[$this->theme->getName()]; @@ -380,28 +375,10 @@ protected function build() { /** * Process a single implementation of hook_theme(). * - * @param array $cache + * @param \Drupal\Core\Theme\ThemeHook[] $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. + * array keyed by theme hooks, whose values are \Drupal\Core\Theme\ThemeHook + * value objects. * - See the @link themeable Theme system overview topic @endlink for * detailed documentation. * @param string $name @@ -430,13 +407,6 @@ protected function build() { protected function processExtension(array &$cache, $name, $type, $theme, $path) { $result = []; - $hook_defaults = [ - 'variables' => TRUE, - 'render element' => TRUE, - 'pattern' => TRUE, - 'base hook' => TRUE, - ]; - $module_list = array_keys($this->moduleHandler->getModuleList()); // Invoke the hook_theme() implementation, preprocess what is returned, and @@ -445,53 +415,63 @@ protected function processExtension(array &$cache, $name, $type, $theme, $path) if (function_exists($function)) { $result = $function($cache, $type, $theme, $path); foreach ($result as $hook => $info) { + // If there was no string $hook provided retrieve it from the ThemeHook. + if ($info instanceof ThemeHook && is_int($hook)) { + $hook = $info->getHook(); + } + + // @deprecated + if (is_array($info)) { + $info = ThemeHook::createFromLegacy($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. + $cached_info = isset($cache[$hook]) ? $cache[$hook] : NULL; // 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; + $info->setType($type); + $info->setThemePath($path); // If a theme hook has a base hook, mark its preprocess functions always // incomplete in order to inherit the base hook's preprocess functions. - if (!empty($result[$hook]['base hook'])) { - $result[$hook]['incomplete preprocess functions'] = TRUE; + if ($info->getBaseHook()) { + $info->markIncomplete(); } - if (isset($cache[$hook]['includes'])) { - $result[$hook]['includes'] = $cache[$hook]['includes']; - } - - // Load the includes, as they may contain preprocess functions. - if (isset($info['includes'])) { - foreach ($info['includes'] as $include_file) { - include_once $this->root . '/' . $include_file; - } + if ($cached_info && $cached_info->getIncludes()) { + $info->setIncludes($cached_info->getIncludes()); } // 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 $this->root . '/' . $include_file; - $result[$hook]['includes'][] = $include_file; + if ($info->getFile()) { + $include_file = $info->getPath() ?: $info->getThemePath(); + $include_file .= '/' . $info->getFile(); + $info->addInclude($include_file); + } + + // Load the includes, as they may contain preprocess functions. + if ($info->hasIncludes()) { + foreach ($info->getIncludes() as $include_file) { + include_once $this->root . '/' . $include_file; + } } // A template file is the default implementation for a theme hook, but // if the theme hook specifies a function callback instead, check to // ensure the function actually exists. - if (isset($info['function'])) { - if (!function_exists($info['function'])) { + if ($function = $info->getFunction()) { + if (!function_exists($function)) { throw new \BadFunctionCallException(sprintf( 'Theme hook "%s" refers to a theme function callback that does not exist: "%s"', $hook, - $info['function'] + $function )); } } @@ -499,29 +479,27 @@ protected function processExtension(array &$cache, $name, $type, $theme, $path) // hook used. If the template does not exist, the theme engine used // should throw an exception at runtime when attempting to include // the template file. - elseif (!isset($info['template'])) { - $info['template'] = strtr($hook, '_', '-'); - $result[$hook]['template'] = $info['template']; + elseif (!$info->getTemplate()) { + $info->setTemplate(strtr($hook, '_', '-')); } // Prepend the current theming path when none is set. This is required // for the default theme engine to know where the template lives. - if (isset($result[$hook]['template']) && !isset($info['path'])) { - $result[$hook]['path'] = $path . '/templates'; + if ($info->getTemplate() && !$info->getPath()) { + $info->setPath($info->getThemePath() . '/templates'); } // 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); + if ($cached_info) { + $info->mergeDefaults($cached_info); } // Preprocess variables for all theming hooks, whether the hook is // implemented as a template or as a function. Ensure they are arrays. - if (!isset($info['preprocess functions']) || !is_array($info['preprocess functions'])) { - $info['preprocess functions'] = []; + if (!$info->getPreprocessFunctions()) { $prefixes = []; - if ($type == 'module') { + if ($info->getType() == 'module') { // Default variable preprocessor prefix. $prefixes[] = 'template'; // Add all modules so they can intervene with their own variable @@ -529,7 +507,7 @@ protected function processExtension(array &$cache, $name, $type, $theme, $path) // even if they are not the owner of the current hook. $prefixes = array_merge($prefixes, $module_list); } - elseif ($type == 'theme_engine' || $type == 'base_theme_engine') { + elseif ($info->getType() == 'theme_engine' || $info->getType() == 'base_theme_engine') { // Theme engines get an extra set that come before the normally // named variable preprocessors. $prefixes[] = $name . '_engine'; @@ -546,29 +524,23 @@ protected function processExtension(array &$cache, $name, $type, $theme, $path) // Only use non-hook-specific variable preprocessors for theming // hooks implemented as templates. See the @defgroup themeable // topic. - if (isset($info['template']) && function_exists($prefix . '_preprocess')) { - $info['preprocess functions'][] = $prefix . '_preprocess'; + if ($info->getTemplate() && function_exists($prefix . '_preprocess')) { + $info->addPreprocessFunction($prefix . '_preprocess'); } if (function_exists($prefix . '_preprocess_' . $hook)) { - $info['preprocess functions'][] = $prefix . '_preprocess_' . $hook; + $info->addPreprocessFunction($prefix . '_preprocess_' . $hook); } } } - // Check for the override flag and prevent the cached variable - // preprocessors from being used. This allows themes or theme engines - // to remove variable preprocessors set earlier in the registry build. - if (!empty($info['override preprocess functions'])) { - // Flag not needed inside the registry. - unset($result[$hook]['override preprocess functions']); - } - elseif (isset($cache[$hook]['preprocess functions']) && is_array($cache[$hook]['preprocess functions'])) { - $info['preprocess functions'] = array_merge($cache[$hook]['preprocess functions'], $info['preprocess functions']); - } - $result[$hook]['preprocess functions'] = $info['preprocess functions']; - } - // Merge the newly created theme hooks into the existing cache. - $cache = $result + $cache; + // Merge the newly created theme hooks into the existing cache. + if ($cached_info) { + $cache[$hook] = $info->merge($cache[$hook]); + } + else { + $cache[$hook] = $info; + } + } } // Let themes have variable preprocessors even if they didn't register a @@ -577,17 +549,14 @@ protected function processExtension(array &$cache, $name, $type, $theme, $path) foreach ($cache as $hook => $info) { // Check only if not registered by the theme or engine. if (empty($result[$hook])) { - if (!isset($info['preprocess functions'])) { - $cache[$hook]['preprocess functions'] = []; - } // Only use non-hook-specific variable preprocessors for theme hooks // implemented as templates. See the @defgroup themeable topic. - if (isset($info['template']) && function_exists($name . '_preprocess')) { - $cache[$hook]['preprocess functions'][] = $name . '_preprocess'; + if ($info->getTemplate() && function_exists($name . '_preprocess')) { + $info->addPreprocessFunction($name . '_preprocess'); } if (function_exists($name . '_preprocess_' . $hook)) { - $cache[$hook]['preprocess functions'][] = $name . '_preprocess_' . $hook; - $cache[$hook]['theme path'] = $path; + $info->addPreprocessFunction($name . '_preprocess_' . $hook); + $info->setThemePath($path); } } } @@ -599,34 +568,37 @@ protected function processExtension(array &$cache, $name, $type, $theme, $path) * * @param string $hook * The name of the suggestion hook to complete. - * @param array $cache + * @param \Drupal\Core\Theme\ThemeHook[] $cache * The theme registry, as documented in * \Drupal\Core\Theme\Registry::processExtension(). */ protected function completeSuggestion($hook, array &$cache) { $previous_hook = $hook; - $incomplete_previous_hook = []; + $incomplete_previous_hook = NULL; // Continue looping if the candidate hook doesn't exist or if the candidate // hook has incomplete preprocess functions, and if the candidate hook is a // suggestion (has a double underscore). - while ((!isset($cache[$previous_hook]) || isset($cache[$previous_hook]['incomplete preprocess functions'])) + while ((!isset($cache[$previous_hook]) || !$cache[$previous_hook]->isComplete()) && $pos = strrpos($previous_hook, '__')) { // Find the first existing candidate hook that has incomplete preprocess // functions. - if (isset($cache[$previous_hook]) && !$incomplete_previous_hook && isset($cache[$previous_hook]['incomplete preprocess functions'])) { - $incomplete_previous_hook = $cache[$previous_hook]; - unset($incomplete_previous_hook['incomplete preprocess functions']); + if (isset($cache[$previous_hook]) && !$incomplete_previous_hook && !$cache[$previous_hook]->isComplete()) { + $incomplete_previous_hook = clone $cache[$previous_hook]; + $incomplete_previous_hook->markComplete(); } $previous_hook = substr($previous_hook, 0, $pos); - $this->mergePreprocessFunctions($hook, $previous_hook, $incomplete_previous_hook, $cache); + $this->mergePreprocessFunctions($hook, $previous_hook, $cache, $incomplete_previous_hook); } - // In addition to processing suggestions, include base hooks. - if (isset($cache[$hook]['base hook'])) { - // In order to retain the additions from above, pass in the current hook - // as the parent hook, otherwise it will be overwritten. - $this->mergePreprocessFunctions($hook, $cache[$hook]['base hook'], $cache[$hook], $cache); + // If a theme hook specifies a base hook, and that base hook is its own + // theme hook and has a complete list of preprocess functions, merge it into + // the current hook. + $base_hook = $cache[$hook]->getBaseHook(); + if ($base_hook && isset($cache[$base_hook]) && $cache[$base_hook]->isComplete()) { + $cache[$hook] = $cache[$hook]->merge($cache[$base_hook]); } + + $cache[$hook]->markComplete(); } /** @@ -636,26 +608,32 @@ protected function completeSuggestion($hook, array &$cache) { * The name of the hook to merge preprocess functions to. * @param string $source_hook_name * The name of the hook to merge preprocess functions from. - * @param array $parent_hook + * @param \Drupal\Core\Theme\ThemeHook[] $cache + * The theme registry, as documented in + * \Drupal\Core\Theme\Registry::processExtension(). + * @param \Drupal\Core\Theme\ThemeHook $parent_hook * The parent hook if it exists. Either an incomplete hook from suggestions * or a base hook. - * @param array $cache - * The theme registry, as documented in - * \Drupal\Core\Theme\Registry::processExtension(). */ - protected function mergePreprocessFunctions($destination_hook_name, $source_hook_name, $parent_hook, array &$cache) { - // If base hook exists clone of it for the preprocess function - // without a template. - // @see https://www.drupal.org/node/2457295 - if (isset($cache[$source_hook_name]) && (!isset($cache[$source_hook_name]['incomplete preprocess functions']) || !isset($cache[$destination_hook_name]['incomplete preprocess functions']))) { - $cache[$destination_hook_name] = $parent_hook + $cache[$source_hook_name]; - if (isset($parent_hook['preprocess functions'])) { - $diff = array_diff($parent_hook['preprocess functions'], $cache[$source_hook_name]['preprocess functions']); - $cache[$destination_hook_name]['preprocess functions'] = array_merge($cache[$source_hook_name]['preprocess functions'], $diff); + protected function mergePreprocessFunctions($destination_hook_name, $source_hook_name, array &$cache, ThemeHook $parent_hook = NULL) { + // If the source hook doesn't exist, do not continue. + if (!isset($cache[$source_hook_name])) { + return; + } + + // If either of the source or destination hook have complete preprocess + // functions, or the destination hook does not exist yet, continue. + if ($cache[$source_hook_name]->isComplete() || (!isset($cache[$destination_hook_name]) || $cache[$destination_hook_name]->isComplete())) { + $to_be_merged = clone $cache[$source_hook_name]; + // If a parent hook was provided, use it as the basis for a merged result. + if ($parent_hook) { + $to_be_merged = $parent_hook->merge($to_be_merged); } + $cache[$destination_hook_name] = ThemeHook::createFromExisting($destination_hook_name, $to_be_merged); + // If a base hook isn't set, this is the actual base hook. - if (!isset($cache[$source_hook_name]['base hook'])) { - $cache[$destination_hook_name]['base hook'] = $source_hook_name; + if (!$cache[$destination_hook_name]->getBaseHook()) { + $cache[$destination_hook_name]->setBaseHook($source_hook_name); } } } @@ -663,7 +641,7 @@ protected function mergePreprocessFunctions($destination_hook_name, $source_hook /** * Completes the theme registry adding discovered functions and hooks. * - * @param array $cache + * @param \Drupal\Core\Theme\ThemeHook[] $cache * The theme registry as documented in * \Drupal\Core\Theme\Registry::processExtension(). * @param \Drupal\Core\Theme\ActiveTheme $theme @@ -676,6 +654,7 @@ protected function postProcessExtension(array &$cache, ActiveTheme $theme) { // expected naming conventions. $prefixes = array_keys((array) $this->moduleHandler->getModuleList()); foreach (array_reverse($theme->getBaseThemes()) as $base) { + /** @var \Drupal\Core\Theme\ActiveTheme $base */ $prefixes[] = $base->getName(); } if ($theme->getEngine()) { @@ -718,16 +697,16 @@ protected function postProcessExtension(array &$cache, ActiveTheme $theme) { ksort($suggestion_level); foreach ($suggestion_level as $level => $item) { foreach ($item as $preprocessor => $hook) { - if (isset($cache[$hook]['preprocess functions']) && !in_array($hook, $cache[$hook]['preprocess functions'])) { + if (isset($cache[$hook]) && !$cache[$hook]->hasPreprocessFunction($hook)) { // Add missing preprocessor to existing hook. - $cache[$hook]['preprocess functions'][] = $preprocessor; + $cache[$hook]->addPreprocessFunction($preprocessor); } elseif (!isset($cache[$hook]) && strpos($hook, '__')) { // Process non-existing hook and register it. // Look for a previously defined hook that is either a less specific // suggestion hook or the base hook. $this->completeSuggestion($hook, $cache); - $cache[$hook]['preprocess functions'][] = $preprocessor; + $cache[$hook]->addPreprocessFunction($preprocessor); } } } @@ -738,18 +717,8 @@ protected function postProcessExtension(array &$cache, ActiveTheme $theme) { // The 'base hook' is only applied to derivative hooks already registered // from a pattern. This is typically set from // drupal_find_theme_functions() and drupal_find_theme_templates(). - if (isset($info['incomplete preprocess functions'])) { + if (!$info->isComplete()) { $this->completeSuggestion($hook, $cache); - unset($cache[$hook]['incomplete preprocess functions']); - } - - // Optimize the registry. - if (isset($cache[$hook]['preprocess functions']) && empty($cache[$hook]['preprocess functions'])) { - unset($cache[$hook]['preprocess functions']); - } - // Ensure uniqueness. - if (isset($cache[$hook]['preprocess functions'])) { - $cache[$hook]['preprocess functions'] = array_unique($cache[$hook]['preprocess functions']); } } } diff --git a/core/lib/Drupal/Core/Theme/ThemeHook.php b/core/lib/Drupal/Core/Theme/ThemeHook.php new file mode 100644 index 0000000..845e569 --- /dev/null +++ b/core/lib/Drupal/Core/Theme/ThemeHook.php @@ -0,0 +1,677 @@ + array('forum__' . $tid, 'forum'), + * '#forum' => $forum, + * ); + * @endcode + * + * @var string + */ + protected $pattern; + + /** + * Used for theme suggestions only: the base theme hook name. + * + * Instead of this suggestion's implementation being used directly, the base + * hook will be invoked with this implementation as its first suggestion. The + * base hook's files will be included and the base hook's preprocess functions + * will be called in addition to any suggestion's preprocess functions. If an + * implementation of hook_theme_suggestions_HOOK() (where HOOK is the base + * hook) changes the suggestion order, a different suggestion may be used in + * place of this suggestion. If after hook_theme_suggestions_HOOK() this + * suggestion remains the first suggestion, then this suggestion's function or + * template will be used to generate the rendered output. + * + * @var string + */ + protected $base_hook; + + /** + * @var string[] + */ + protected $includes = []; + + /** + * The file the implementation resides in. This file will be included prior to + * the theme being rendered, to make sure that the function or preprocess + * function (as needed) is actually loaded. + * + * @var string + */ + protected $file; + + /** + * If specified, this will be the function name to invoke for this + * implementation. If neither 'template' nor 'function' are specified, a + * default template name will be assumed. See above for more details. + * + * @deprecated in Drupal 8.0.x, will be removed in Drupal 9.0.x. + * + * @var string + */ + protected $function; + + /** + * If specified, the theme implementation is a template file, and this is the + * template name. Do not add 'html.twig' on the end of the template name. The + * extension will be added automatically by the default rendering engine + * (which is Twig.) If 'path' is specified, 'template' should also be + * specified. If neither 'template' nor 'function' are specified, a default + * template name will be assumed. For example, if a module registers the + * 'search_result' theme hook, 'search-result' will be assigned as its + * template name. + * + * @var string + */ + protected $template; + + /** + * Override the path of the file to be used. Ordinarily the module or theme + * path will be used, but if the file will not be in the default path, include + * it here. This path should be relative to the Drupal root directory. + * + * @var string + */ + protected $path; + + /** + * 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 twig_preprocess and twig_preprocess_HOOK as + * well as themename_preprocess and themename_preprocess_HOOK. + * + * @var string[] + */ + protected $preprocess_functions = []; + + /** + * @var bool + */ + protected $incomplete_preprocess_functions = FALSE; + + /** + * 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.html.twig are set, this can be set to true. Please keep in mind + * that when this is used by a theme, that theme becomes responsible for + * making sure necessary variables are set. + * + * @var bool + */ + protected $override_preprocess_functions = FALSE; + + /** + * Stores extra data. + * + * @var mixed[] + */ + protected $storage = []; + + /** + * @var string + */ + protected $hook; + + /** + * Constructs a new ThemeHook. + * + * @param string $hook + */ + protected function __construct($hook) { + $this->hook = $hook; + } + + /** + * Creates a new ThemeHook instance. + * + * @param string $hook + * + * @return static + */ + public static function create($hook) { + return new static($hook); + } + + /** + * Creates a new ThemeHook instance based on an existing ThemeHook. + * + * @param string $hook + * @param \Drupal\Core\Theme\ThemeHook $other + * + * @return static + */ + public static function createFromExisting($hook, ThemeHook $other) { + $instance = static::create($hook); + return $instance->merge($other); + } + + /** + * @deprecated + */ + public static function createFromLegacy($hook, array $values) { + $instance = static::create($hook); + foreach ($values as $name => $value) { + $instance->offsetSet($name, $value); + } + return $instance; + } + + /** + * @param \Drupal\Core\Theme\ThemeHook $other + * + * @return static + */ + public function merge(ThemeHook $other) { + $result = clone $this; + + // If this object doesn't have a value, use the value from the other object. + if (!$result->getType()) { + $result->setType($other->getType()); + } + if (!$result->getFile()) { + $result->setFile($other->getFile()); + } + if (!$result->getThemePath()) { + $result->setThemePath($other->getThemePath()); + } + if (!$result->getPath()) { + $result->setPath($other->getPath()); + } + if (!$result->getOverriddenPreprocessFunctionStatus()) { + $result->setOverriddenPreprocessFunctionStatus($other->getOverriddenPreprocessFunctionStatus()); + } + + // Only copy over the function and template if the original has neither. + if (!$result->getFunction() && !$result->getTemplate()) { + $result->setTemplate($other->getTemplate()); + $result->setFunction($other->getFunction()); + } + + // If the other object does not yet have a complete list of preprocess + // functions, mark the result as incomplete. + if (!$other->isComplete()) { + $result->markIncomplete(); + } + + // Merge all includes together. + $result->setIncludes(array_merge($other->getIncludes(), $result->getIncludes())); + + // Merge in the values that are considered "default". + $result->mergeDefaults($other); + + // Check for the override flag and prevent the cached variable preprocessors + // from being used. This allows themes or theme engines to remove variable + // preprocessors set earlier in the registry build. + if (!$result->getOverriddenPreprocessFunctionStatus()) { + $result->setPreprocessFunctions(array_merge($other->getPreprocessFunctions(), $result->getPreprocessFunctions())); + } + + return $result; + } + + /** + * @param \Drupal\Core\Theme\ThemeHook $other + * + * @return static + */ + public function mergeDefaults(ThemeHook $other) { + if ($other->getVariables() && !$this->getVariables()) { + $this->setVariables($other->getVariables()); + } + if (!$this->getPattern()) { + $this->setPattern($other->getPattern()); + } + if (!$this->getBaseHook()) { + $this->setBaseHook($other->getBaseHook()); + } + if ($other->getRenderElement() && !$this->getRenderElement()) { + $this->setRenderElement($other->getRenderElement()); + } + return $this; + } + + /** + * {@inheritdoc} + */ + public function &offsetGet($name) { + $value = NULL; + $name = str_replace(' ', '_', $name); + if (property_exists($this, $name)) { + $value = &$this->{$name}; + } + else { + if (isset($this->storage[$name])) { + $value = &$this->storage[$name]; + } + } + return $value; + } + + /** + * {@inheritdoc} + */ + public function offsetSet($name, $value) { + $name = str_replace(' ', '_', $name); + if (property_exists($this, $name)) { + $this->{$name} = $value; + } + else { + $this->storage[$name] = $value; + } + } + + /** + * {@inheritdoc} + */ + public function offsetUnset($name) { + $name = str_replace(' ', '_', $name); + if (property_exists($this, $name)) { + $reflection = new \ReflectionClass($this); + $this->{$name} = $reflection->getDefaultProperties()[$name]; + } + else { + unset($this->storage[$name]); + } + } + + /** + * {@inheritdoc} + */ + public function offsetExists($name) { + $name = str_replace(' ', '_', $name); + if (property_exists($this, $name)) { + return isset($this->{$name}); + } + else { + return isset($this->storage[$name]); + } + } + + /** + * @return mixed + */ + public function getThemePath() { + return $this->theme_path; + } + + /** + * @param string $theme_path + * + * @return $this + */ + public function setThemePath($theme_path) { + $this->theme_path = $theme_path; + + return $this; + } + + /** + * @return string + */ + public function getType() { + return $this->type; + } + + /** + * @param string $type + */ + public function setType($type) { + $this->type = $type; + + return $this; + } + + /** + * @return mixed[]|null + */ + public function getVariables() { + return $this->variables; + } + + /** + * @param mixed[] $variables + * + * @return $this + */ + public function setVariables(array $variables) { + $this->variables = $variables; + + return $this; + } + + /** + * @return string + */ + public function getRenderElement() { + return $this->render_element; + } + + /** + * @param string $render_element + * + * @return $this + */ + public function setRenderElement($render_element) { + $this->render_element = $render_element; + + return $this; + } + + /** + * @return string + */ + public function getPattern() { + return $this->pattern; + } + + /** + * @param string $pattern + * + * @return $this + */ + public function setPattern($pattern) { + $this->pattern = $pattern; + + return $this; + } + + /** + * @return string + */ + public function getBaseHook() { + return $this->base_hook; + } + + /** + * @param string $base_hook + * + * @return $this + */ + public function setBaseHook($base_hook) { + $this->base_hook = $base_hook; + + return $this; + } + + /** + * @return string[] + */ + public function getIncludes() { + return $this->includes; + } + + /** + * @param string[] $includes + * + * @return $this + */ + public function setIncludes(array $includes) { + $this->includes = $includes; + + return $this; + } + + /** + * @param string $include + * + * @return $this + */ + public function addInclude($include) { + $includes = $this->getIncludes(); + $includes[] = $include; + $this->setIncludes($includes); + + return $this; + } + + /** + * @return bool + */ + public function hasIncludes() { + return !empty($this->includes); + } + + /** + * @return string + */ + public function getFile() { + return $this->file; + } + + /** + * @param string $file + * + * @return $this + */ + public function setFile($file) { + $this->file = $file; + + return $this; + } + + /** + * @return string + */ + public function getFunction() { + return $this->function; + } + + /** + * @param string $function + * + * @return $this + */ + public function setFunction($function) { + $this->function = $function; + + return $this; + } + + /** + * @return string + */ + public function getTemplate() { + return $this->template; + } + + /** + * @param string $template + * + * @return $this + */ + public function setTemplate($template) { + $this->template = $template; + + return $this; + } + + /** + * @return string + */ + public function getPath() { + return $this->path; + } + + /** + * @param string $path + * + * @return $this + */ + public function setPath($path) { + $this->path = $path; + + return $this; + } + + /** + * @return string[] + */ + public function getPreprocessFunctions() { + return $this->preprocess_functions; + } + + /** + * @param string[] $preprocess_functions + * + * @return $this + */ + public function setPreprocessFunctions(array $preprocess_functions) { + $this->preprocess_functions = array_values(array_unique($preprocess_functions)); + + return $this; + } + + /** + * @param string $preprocess_function + * + * @return bool + */ + public function hasPreprocessFunction($preprocess_function) { + return in_array($preprocess_function, $this->getPreprocessFunctions()); + } + + /** + * @param string $preprocess_function + * + * @return $this + */ + public function addPreprocessFunction($preprocess_function) { + $preprocess_functions = $this->getPreprocessFunctions(); + $preprocess_functions[] = $preprocess_function; + $this->setPreprocessFunctions($preprocess_functions); + + return $this; + } + + /** + * @return bool + */ + public function getOverriddenPreprocessFunctionStatus() { + return $this->override_preprocess_functions; + } + + /** + * @return $this + */ + public function setOverriddenPreprocessFunctionStatus($status) { + $this->override_preprocess_functions = (bool) $status; + + return $this; + } + + /** + * @return bool + */ + public function isComplete() { + return !$this->incomplete_preprocess_functions; + } + + /** + * @return $this + */ + public function markIncomplete() { + $this->incomplete_preprocess_functions = TRUE; + + return $this; + } + + /** + * @return $this + */ + public function markComplete() { + $this->incomplete_preprocess_functions = FALSE; + + return $this; + } + + /** + * @param string $name + * + * @return $this + */ + public function setFlag($name) { + $this->flag[$name] = TRUE; + + return $this; + } + + /** + * @param string $name + * + * @return $this + */ + public function unsetFlag($name) { + unset($this->flag[$name]); + + return $this; + } + + /** + * @return mixed + */ + public function getHook() { + return $this->hook; + } + +} diff --git a/core/lib/Drupal/Core/Theme/ThemeManager.php b/core/lib/Drupal/Core/Theme/ThemeManager.php index c58d176..2e13410 100644 --- a/core/lib/Drupal/Core/Theme/ThemeManager.php +++ b/core/lib/Drupal/Core/Theme/ThemeManager.php @@ -189,26 +189,26 @@ public function render($hook, array $variables) { if (isset($variables['#theme']) || isset($variables['#theme_wrappers'])) { $element = $variables; $variables = []; - if (isset($info['variables'])) { - foreach (array_keys($info['variables']) as $name) { + if ($info->getVariables()) { + foreach (array_keys($info->getVariables()) as $name) { if (isset($element["#$name"]) || array_key_exists("#$name", $element)) { $variables[$name] = $element["#$name"]; } } } else { - $variables[$info['render element']] = $element; + $variables[$info->getRenderElement()] = $element; // Give a hint to render engines to prevent infinite recursion. - $variables[$info['render element']]['#render_children'] = TRUE; + $variables[$info->getRenderElement()]['#render_children'] = TRUE; } } // Merge in argument defaults. - if (!empty($info['variables'])) { - $variables += $info['variables']; + if ($info->getVariables()) { + $variables += $info->getVariables(); } - elseif (!empty($info['render element'])) { - $variables += [$info['render element'] => []]; + elseif ($info->getRenderElement()) { + $variables += [$info->getRenderElement() => []]; } // Supply original caller info. $variables += [ @@ -219,19 +219,14 @@ public function render($hook, array $variables) { // is called, we run hook_theme_suggestions_node_alter() rather than // hook_theme_suggestions_node__article_alter(), and also pass in the base // hook as the last parameter to the suggestions alter hooks. - if (isset($info['base hook'])) { - $base_theme_hook = $info['base hook']; - } - else { - $base_theme_hook = $hook; - } + $base_theme_hook = $info->getBaseHook() ?: $hook; // Invoke hook_theme_suggestions_HOOK(). $suggestions = $this->moduleHandler->invokeAll('theme_suggestions_' . $base_theme_hook, [$variables]); // If the theme implementation was invoked with a direct theme suggestion // like '#theme' => 'node__article', add it to the suggestions array before // invoking suggestion alter hooks. - if (isset($info['base hook'])) { + if ($info->getBaseHook()) { $suggestions[] = $hook; } @@ -258,31 +253,30 @@ public function render($hook, array $variables) { // Include a file if the theme function or variable preprocessor is held // elsewhere. - if (!empty($info['includes'])) { - foreach ($info['includes'] as $include_file) { + if ($info->hasIncludes()) { + foreach ($info->getIncludes() as $include_file) { include_once $this->root . '/' . $include_file; } } // Invoke the variable preprocessors, if any. - if (isset($info['base hook'])) { - $base_hook = $info['base hook']; + if ($base_hook = $info->getBaseHook()) { $base_hook_info = $theme_registry->get($base_hook); // Include files required by the base hook, since its variable // preprocessors might reside there. - if (!empty($base_hook_info['includes'])) { - foreach ($base_hook_info['includes'] as $include_file) { + if ($base_hook_info->hasIncludes()) { + foreach ($base_hook_info->getIncludes() as $include_file) { include_once $this->root . '/' . $include_file; } } - if (isset($base_hook_info['preprocess functions'])) { + if ($base_hook_info->getPreprocessFunctions()) { // Set a variable for the 'theme_hook_suggestion'. This is used to // maintain backwards compatibility with template engines. $theme_hook_suggestion = $hook; } } - if (isset($info['preprocess functions'])) { - foreach ($info['preprocess functions'] as $preprocessor_function) { + if ($preprocessor_functions = $info->getPreprocessFunctions()) { + foreach ($preprocessor_functions as $preprocessor_function) { if (function_exists($preprocessor_function)) { $preprocessor_function($variables, $hook, $info); } @@ -309,12 +303,12 @@ public function render($hook, array $variables) { // Generate the output using either a function or a template. $output = ''; - if (isset($info['function'])) { - if (function_exists($info['function'])) { + if ($function = $info->getFunction()) { + if (function_exists($function)) { // Theme functions do not render via the theme engine, so the output is // not autoescaped. However, we can only presume that the theme function // has been written correctly and that the markup is safe. - $output = Markup::create($info['function']($variables)); + $output = Markup::create($function($variables)); } } else { @@ -325,7 +319,7 @@ public function render($hook, array $variables) { // renderer. $theme_engine = $active_theme->getEngine(); if (isset($theme_engine)) { - if ($info['type'] != 'module') { + if ($info->getType() != 'module') { if (function_exists($theme_engine . '_render_template')) { $render_function = $theme_engine . '_render_template'; } @@ -367,9 +361,9 @@ public function render($hook, array $variables) { } // Render the output using the template file. - $template_file = $info['template'] . $extension; - if (isset($info['path'])) { - $template_file = $info['path'] . '/' . $template_file; + $template_file = $info->getTemplate() . $extension; + if ($path = $info->getPath()) { + $template_file = $path . '/' . $template_file; } // Add the theme suggestions to the variables array just before rendering // the template for backwards compatibility with template engines. diff --git a/core/lib/Drupal/Core/Utility/ThemeRegistry.php b/core/lib/Drupal/Core/Utility/ThemeRegistry.php index bc2ce5a..3bd985d 100644 --- a/core/lib/Drupal/Core/Utility/ThemeRegistry.php +++ b/core/lib/Drupal/Core/Utility/ThemeRegistry.php @@ -101,6 +101,8 @@ public function has($key) { /** * {@inheritdoc} + * + * @return \Drupal\Core\Theme\ThemeHook|array */ public function get($key) { // If the offset is set but empty, it is a registered theme hook that has diff --git a/core/modules/layout_discovery/tests/src/Kernel/LayoutTest.php b/core/modules/layout_discovery/tests/src/Kernel/LayoutTest.php index 9d4c446..c3785df 100644 --- a/core/modules/layout_discovery/tests/src/Kernel/LayoutTest.php +++ b/core/modules/layout_discovery/tests/src/Kernel/LayoutTest.php @@ -34,6 +34,17 @@ protected function setUp() { } /** + * Tests that a layout provided by a theme has the preprocess function set. + */ + public function testThemeProvidedLayout() { + $this->container->get('theme_installer')->install(['test_layout_theme']); + $this->config('system.theme')->set('default', 'test_layout_theme')->save(); + + $theme_definitions = $this->container->get('theme.registry')->get(); + $this->assertTrue(in_array('template_preprocess_layout', $theme_definitions['test_layout_theme']['preprocess functions'])); + } + + /** * Test rendering a layout. * * @dataProvider renderLayoutData diff --git a/core/modules/layout_discovery/tests/themes/test_layout_theme/templates/test-layout-theme.html.twig b/core/modules/layout_discovery/tests/themes/test_layout_theme/templates/test-layout-theme.html.twig new file mode 100644 index 0000000..67675f6 --- /dev/null +++ b/core/modules/layout_discovery/tests/themes/test_layout_theme/templates/test-layout-theme.html.twig @@ -0,0 +1 @@ +{{ content.content }} diff --git a/core/modules/layout_discovery/tests/themes/test_layout_theme/test_layout_theme.info.yml b/core/modules/layout_discovery/tests/themes/test_layout_theme/test_layout_theme.info.yml new file mode 100644 index 0000000..021d43f --- /dev/null +++ b/core/modules/layout_discovery/tests/themes/test_layout_theme/test_layout_theme.info.yml @@ -0,0 +1,6 @@ +name: 'Test layout theme' +type: theme +description: 'Theme for testing a theme-provided layout' +version: VERSION +base theme: classy +core: 8.x diff --git a/core/modules/layout_discovery/tests/themes/test_layout_theme/test_layout_theme.layouts.yml b/core/modules/layout_discovery/tests/themes/test_layout_theme/test_layout_theme.layouts.yml new file mode 100644 index 0000000..9da19dd --- /dev/null +++ b/core/modules/layout_discovery/tests/themes/test_layout_theme/test_layout_theme.layouts.yml @@ -0,0 +1,7 @@ +test_layout_theme: + label: 'Test Layout - Theme' + category: 'Test Layout Theme' + template: templates/test-layout-theme + regions: + content: + label: Content diff --git a/core/modules/locale/tests/modules/locale_test/locale_test.module b/core/modules/locale/tests/modules/locale_test/locale_test.module index 89ba2ba..9059e9c 100644 --- a/core/modules/locale/tests/modules/locale_test/locale_test.module +++ b/core/modules/locale/tests/modules/locale_test/locale_test.module @@ -154,7 +154,7 @@ function locale_test_theme() { $return = []; $return['locale_test_tokenized'] = [ - 'variable' => ['content' => ''], + 'variables' => ['content' => ''], ]; return $return; 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 2e6f164..674905b 100644 --- a/core/modules/system/tests/modules/theme_test/theme_test.module +++ b/core/modules/system/tests/modules/theme_test/theme_test.module @@ -6,6 +6,7 @@ */ use Drupal\Core\Extension\Extension; +use Drupal\Core\Theme\ThemeHook; /** * Implements hook_theme(). @@ -66,6 +67,9 @@ function theme_test_theme($existing, $type, $theme, $path) { 'bar' => '', ], ]; + $items[] = ThemeHook::create('theme_test_registered_by_module') + ->setRenderElement('content') + ->setBaseHook('container'); return $items; } diff --git a/core/modules/system/tests/themes/test_theme/templates/theme-test-registered-by-module.html.twig b/core/modules/system/tests/themes/test_theme/templates/theme-test-registered-by-module.html.twig new file mode 100644 index 0000000..3432e01 --- /dev/null +++ b/core/modules/system/tests/themes/test_theme/templates/theme-test-registered-by-module.html.twig @@ -0,0 +1,2 @@ +{# Output for Theme API test #} +Template provided by theme is registered by module. diff --git a/core/tests/Drupal/KernelTests/Core/Theme/RegistryTest.php b/core/tests/Drupal/KernelTests/Core/Theme/RegistryTest.php index a962fec..211f89e 100644 --- a/core/tests/Drupal/KernelTests/Core/Theme/RegistryTest.php +++ b/core/tests/Drupal/KernelTests/Core/Theme/RegistryTest.php @@ -142,6 +142,9 @@ public function testSuggestionPreprocessFunctions() { $this->assertIdentical($expected_preprocess_functions, $preprocess_functions, 'Suggestion implemented as a template correctly inherits preprocess functions.'); $this->assertTrue(isset($registry_theme->get()['theme_test_preprocess_suggestions__kitten__meerkat__tarsier__moose']), 'Preprocess function with an unimplemented lower-level suggestion is added to the registry.'); + foreach ($registry_theme->get() as $name => $info) { + $this->assertSame($name, $info->getHook()); + } } /** @@ -192,4 +195,22 @@ public function testThemeSuggestions() { ], $suggestions, 'Found expected page node suggestions.'); } + /** + * Tests theme-provided templates that are registered by modules. + */ + public function testThemeTemplatesRegisteredByModules() { + $theme_handler = \Drupal::service('theme_handler'); + $theme_handler->install(['test_theme']); + + $registry_theme = new Registry(\Drupal::root(), \Drupal::cache(), \Drupal::lock(), \Drupal::moduleHandler(), $theme_handler, \Drupal::service('theme.initialization'), 'test_theme'); + $registry_theme->setThemeManager(\Drupal::theme()); + + $expected = [ + 'template_preprocess', + 'template_preprocess_container', + ]; + $registry = $registry_theme->get(); + $this->assertEquals($expected, $registry['theme_test_registered_by_module']['preprocess functions']); + } + } diff --git a/core/tests/Drupal/Tests/Core/Theme/RegistryTest.php b/core/tests/Drupal/Tests/Core/Theme/RegistryTest.php index f2cd22e..aa0074e 100644 --- a/core/tests/Drupal/Tests/Core/Theme/RegistryTest.php +++ b/core/tests/Drupal/Tests/Core/Theme/RegistryTest.php @@ -9,6 +9,7 @@ use Drupal\Core\Theme\ActiveTheme; use Drupal\Core\Theme\Registry; +use Drupal\Core\Theme\ThemeHook; use Drupal\Tests\UnitTestCase; /** @@ -191,6 +192,17 @@ public function testGetRegistryForModule() { public function testPostProcessExtension($defined_functions, $hooks, $expected) { static::$functions['user'] = $defined_functions; + foreach ($expected as $name => $hook) { + if (is_array($hook)) { + $expected[$name] = ThemeHook::createFromLegacy($name, $hook); + } + } + foreach ($hooks as $name => $hook) { + if (is_array($hook)) { + $hooks[$name] = ThemeHook::createFromLegacy($name, $hook); + } + } + $theme = $this->prophesize(ActiveTheme::class); $theme->getBaseThemes()->willReturn([]); $theme->getName()->willReturn('test');