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..6816ba8 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 @@ -449,49 +419,54 @@ protected function processExtension(array &$cache, $name, $type, $theme, $path) // $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; + // @todo. + // @deprecated. + if (is_array($info)) { + $info = ThemeHook::createFromLegacy($hook, $info); + } // 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 +474,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 +502,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 +519,33 @@ 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']); + if (!$info->getOverriddenPreprocessFunctionStatus() && $cached_info) { + $info->mergePreprocessFunctions($cached_info); } - 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']; + $result[$hook] = $info; } // Merge the newly created theme hooks into the existing cache. - $cache = $result + $cache; + foreach ($result as $hook => $info) { + if (!isset($cache[$hook])) { + $cache[$hook] = $info; + } + else { + $cache[$hook] = $info->merge($cache[$hook]); + } + } } // Let themes have variable preprocessors even if they didn't register a @@ -577,17 +554,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 +573,38 @@ 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]->hasIncompletePreprocessFunctions()) && $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]->hasIncompletePreprocessFunctions()) { + $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]->hasCompletePreprocessFunctions()) { + $cache[$hook] = $cache[$hook]->merge($cache[$base_hook]); } + + // @todo. + $cache[$hook]->markComplete(); } /** @@ -636,26 +614,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]->hasCompletePreprocessFunctions() || (!isset($cache[$destination_hook_name]) || $cache[$destination_hook_name]->hasCompletePreprocessFunctions())) { + $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 +647,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 +660,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 +703,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,19 +723,13 @@ 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->hasIncompletePreprocessFunctions()) { $this->completeSuggestion($hook, $cache); - unset($cache[$hook]['incomplete preprocess functions']); + $info->markComplete(); } // 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']); - } + // @todo. } } @@ -783,12 +762,13 @@ public function destruct() { /** * Gets all user functions grouped by the word before the first underscore. * - * @param $prefixes + * @param array $prefixes * An array of function prefixes by which the list can be limited. + * * @return array * Functions grouped by the first prefix. */ - public function getPrefixGroupedUserFunctions($prefixes = []) { + public function getPrefixGroupedUserFunctions(array $prefixes = []) { $functions = get_defined_functions(); // If a list of prefixes is supplied, trim down the list to those items @@ -817,6 +797,7 @@ public function getPrefixGroupedUserFunctions($prefixes = []) { * The name of the item for which the path is requested. * * @return string + * The path to the requested module. */ protected function getPath($module) { return drupal_get_path('module', $module); diff --git a/core/lib/Drupal/Core/Theme/ThemeHook.php b/core/lib/Drupal/Core/Theme/ThemeHook.php new file mode 100644 index 0000000..295717d --- /dev/null +++ b/core/lib/Drupal/Core/Theme/ThemeHook.php @@ -0,0 +1,684 @@ + 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()); + } + + // @todo. + if (!$result->getFunction() && !$result->getTemplate()) { + $result->setTemplate($other->getTemplate()); + $result->setFunction($other->getFunction()); + } + + // @todo. + if ($other->hasIncompletePreprocessFunctions()) { + $result->markIncomplete(); + } + + // @todo. + $result->setIncludes(array_merge($result->getIncludes(), $other->getIncludes())); + + // @todo. + $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->mergePreprocessFunctions($other); + } + + 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()); + } + } + public function mergePreprocessFunctions(ThemeHook $other) { + $this->setPreprocessFunctions(array_merge($other->getPreprocessFunctions(), $this->getPreprocessFunctions())); + } + + /** + * {@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 hasCompletePreprocessFunctions() { + return !$this->incomplete_preprocess_functions; + } + + /** + * @return bool + */ + public function hasIncompletePreprocessFunctions() { + 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/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..41aa222 100644 --- a/core/modules/system/tests/modules/theme_test/theme_test.module +++ b/core/modules/system/tests/modules/theme_test/theme_test.module @@ -66,6 +66,10 @@ function theme_test_theme($existing, $type, $theme, $path) { 'bar' => '', ], ]; + $items['theme_test_registered_by_module'] = [ + 'render element' => 'content', + 'base hook' => '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..f7a683c 100644 --- a/core/tests/Drupal/Tests/Core/Theme/RegistryTest.php +++ b/core/tests/Drupal/Tests/Core/Theme/RegistryTest.php @@ -7,9 +7,17 @@ namespace Drupal\Tests\Core\Theme; +use Drupal\Core\Cache\CacheBackendInterface; +use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\Core\Extension\ThemeHandlerInterface; +use Drupal\Core\Lock\LockBackendInterface; use Drupal\Core\Theme\ActiveTheme; use Drupal\Core\Theme\Registry; +use Drupal\Core\Theme\ThemeHook; +use Drupal\Core\Theme\ThemeInitializationInterface; +use Drupal\Core\Theme\ThemeManagerInterface; use Drupal\Tests\UnitTestCase; +use Prophecy\Argument; /** * @coversDefaultClass \Drupal\Core\Theme\Registry @@ -25,20 +33,6 @@ class RegistryTest extends UnitTestCase { protected $registry; /** - * The mocked cache backend. - * - * @var \Drupal\Core\Cache\CacheBackendInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $cache; - - /** - * The mocked lock backend. - * - * @var \Drupal\Core\Lock\LockBackendInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $lock; - - /** * The mocked module handler. * * @var \Drupal\Core\Extension\ModuleHandlerInterface|\PHPUnit_Framework_MockObject_MockObject @@ -46,23 +40,9 @@ class RegistryTest extends UnitTestCase { protected $moduleHandler; /** - * The mocked theme handler. - * - * @var \Drupal\Core\Extension\ThemeHandlerInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $themeHandler; - - /** - * The mocked theme initialization. - * - * @var \Drupal\Core\Theme\ThemeInitializationInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $themeInitialization; - - /** * The theme manager. * - * @var \Drupal\Core\Theme\ThemeManagerInterface|\PHPUnit_Framework_MockObject_MockObject + * @var \Drupal\Core\Theme\ThemeManagerInterface|\Prophecy\Prophecy\ProphecyInterface */ protected $themeManager; @@ -79,14 +59,16 @@ class RegistryTest extends UnitTestCase { protected function setUp() { parent::setUp(); - $this->cache = $this->getMock('Drupal\Core\Cache\CacheBackendInterface'); - $this->lock = $this->getMock('Drupal\Core\Lock\LockBackendInterface'); - $this->moduleHandler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface'); - $this->themeHandler = $this->getMock('Drupal\Core\Extension\ThemeHandlerInterface'); - $this->themeInitialization = $this->getMock('Drupal\Core\Theme\ThemeInitializationInterface'); - $this->themeManager = $this->getMock('Drupal\Core\Theme\ThemeManagerInterface'); + $cache = $this->prophesize(CacheBackendInterface::class); + $lock = $this->prophesize(LockBackendInterface::class); + $theme_handler = $this->prophesize(ThemeHandlerInterface::class); + $theme_initialization = $this->prophesize(ThemeInitializationInterface::class); - $this->setupTheme(); + $this->moduleHandler = $this->prophesize(ModuleHandlerInterface::class); + $this->themeManager = $this->prophesize(ThemeManagerInterface::class); + + $this->registry = new TestRegistry($this->root, $cache->reveal(), $lock->reveal(), $this->moduleHandler->reveal(), $theme_handler->reveal(), $theme_initialization->reveal()); + $this->registry->setThemeManager($this->themeManager->reveal()); } /** @@ -127,20 +109,16 @@ public function testGetRegistryForModule() { 'base_themes' => [], ]); - $this->themeManager->expects($this->exactly(2)) - ->method('getActiveTheme') - ->willReturnOnConsecutiveCalls($test_theme, $test_stable); + $this->themeManager->getActiveTheme()->willReturn($test_theme); + $this->themeManager->alterForTheme($test_theme, 'theme_registry', Argument::any())->shouldBeCalled(); // Include the module and theme files so that hook_theme can be called. include_once $this->root . '/core/modules/system/tests/modules/theme_test/theme_test.module'; include_once $this->root . '/core/modules/system/tests/themes/test_stable/test_stable.theme'; - $this->moduleHandler->expects($this->exactly(2)) - ->method('getImplementations') - ->with('theme') - ->will($this->returnValue(['theme_test'])); - $this->moduleHandler->expects($this->atLeastOnce()) - ->method('getModuleList') - ->willReturn([]); + $this->moduleHandler->getImplementations('theme')->willReturn(['theme_test']); + $this->moduleHandler->getModuleList()->willReturn([]); + $this->moduleHandler->isLoaded()->willReturn(TRUE); + $this->moduleHandler->alter('theme_registry', Argument::any())->shouldBeCalled(); $registry = $this->registry->get(); @@ -169,6 +147,9 @@ public function testGetRegistryForModule() { // The second call will initialize with the second theme. Ensure that this // returns a different object and the discovery for the second theme's // preprocess function worked. + $this->themeManager->getActiveTheme()->willReturn($test_stable); + $this->themeManager->alterForTheme($test_stable, 'theme_registry', Argument::any())->shouldBeCalled(); + $other_registry = $this->registry->get(); $this->assertNotSame($registry, $other_registry); $this->assertTrue(in_array('test_stable_preprocess_theme_test_render_element', $other_registry['theme_test_render_element']['preprocess functions'])); @@ -176,8 +157,6 @@ public function testGetRegistryForModule() { /** * @covers ::postProcessExtension - * @covers ::completeSuggestion - * @covers ::mergePreprocessFunctions * * @dataProvider providerTestPostProcessExtension * @@ -191,14 +170,23 @@ 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'); $theme->getEngine()->willReturn('twig'); - $this->moduleHandler->expects($this->atLeastOnce()) - ->method('getModuleList') - ->willReturn([]); + $this->moduleHandler->getModuleList()->willReturn([]); $class = new \ReflectionClass(TestRegistry::class); $reflection_method = $class->getMethod('postProcessExtension'); @@ -485,11 +473,6 @@ public function providerTestPostProcessExtension() { return $data; } - protected function setupTheme() { - $this->registry = new TestRegistry($this->root, $this->cache, $this->lock, $this->moduleHandler, $this->themeHandler, $this->themeInitialization); - $this->registry->setThemeManager($this->themeManager); - } - } class TestRegistry extends Registry {