diff --git a/core/lib/Drupal/Core/Render/theme.api.php b/core/lib/Drupal/Core/Render/theme.api.php index 44fcdd0..3d58978 100644 --- a/core/lib/Drupal/Core/Render/theme.api.php +++ b/core/lib/Drupal/Core/Render/theme.api.php @@ -1092,87 +1092,8 @@ 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 - * 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 - * 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. + * @return \Drupal\Core\Theme\ThemeHook[] + * An array of theme hook objects. * * @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..0867253 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,130 +415,31 @@ 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->getName(); + } + + // @todo Remove support for legacy array-based theme hooks. + 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 + // Fill in the name, type, and path of the module, theme, or engine that // implements this theme function. - $result[$hook]['type'] = $type; - $result[$hook]['theme path'] = $path; + $info->setProvider($name); + $info->setProviderType($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 (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 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; - } - - // 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'])) { - throw new \BadFunctionCallException(sprintf( - 'Theme hook "%s" refers to a theme function callback that does not exist: "%s"', - $hook, - $info['function'] - )); - } - } - // Provide a default naming convention for 'template' based on the - // 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']; - } - - // 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 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); - } - - // 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'] = []; - $prefixes = []; - if ($type == 'module') { - // Default variable preprocessor prefix. - $prefixes[] = 'template'; - // Add all modules so they can intervene with their own variable - // preprocessors. This allows them to provide variable preprocessors - // even if they are not the owner of the current hook. - $prefixes = array_merge($prefixes, $module_list); - } - elseif ($type == 'theme_engine' || $type == 'base_theme_engine') { - // Theme engines get an extra set that come before the normally - // named variable preprocessors. - $prefixes[] = $name . '_engine'; - // The theme engine registers on behalf of the theme using the - // theme's name. - $prefixes[] = $theme; - } - else { - // This applies when the theme manually registers their own variable - // preprocessors. - $prefixes[] = $name; - } - foreach ($prefixes as $prefix) { - // Only use non-hook-specific variable preprocessors for theming - // hooks implemented as templates. See the @defgroup themeable - // topic. - if (isset($info['template']) && function_exists($prefix . '_preprocess')) { - $info['preprocess functions'][] = $prefix . '_preprocess'; - } - if (function_exists($prefix . '_preprocess_' . $hook)) { - $info['preprocess functions'][] = $prefix . '_preprocess_' . $hook; - } - } - } - // Check for the override flag and prevent the cached variable - // preprocessors from being used. This allows themes or theme engines - // to remove variable preprocessors set earlier in the registry build. - if (!empty($info['override preprocess functions'])) { - // Flag not needed inside the registry. - unset($result[$hook]['override preprocess functions']); - } - elseif (isset($cache[$hook]['preprocess functions']) && is_array($cache[$hook]['preprocess functions'])) { - $info['preprocess functions'] = array_merge($cache[$hook]['preprocess functions'], $info['preprocess functions']); - } - $result[$hook]['preprocess functions'] = $info['preprocess functions']; + // Process the theme hook. + $cache[$hook] = $info->process($this->root, $theme, $module_list, $cached_info); } - - // Merge the newly created theme hooks into the existing cache. - $cache = $result + $cache; } // Let themes have variable preprocessors even if they didn't register a @@ -577,17 +448,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 +467,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]->isIncomplete()) && $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]->isIncomplete()) { + $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]->isIncomplete()) { + $cache[$hook] = $cache[$hook]->merge($cache[$base_hook]); } + + $cache[$hook]->markComplete(); } /** @@ -636,26 +507,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]->isIncomplete() || (!isset($cache[$destination_hook_name]) || !$cache[$destination_hook_name]->isIncomplete())) { + $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 +540,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 +553,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 +596,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 +616,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->isIncomplete()) { $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..2ed4098 --- /dev/null +++ b/core/lib/Drupal/Core/Theme/ThemeHook.php @@ -0,0 +1,969 @@ +name = $name; + } + + /** + * Creates a new ThemeHook instance. + * + * @param string $name + * The machine name of the theme hook. + * + * @return static + */ + public static function create($name) { + return new static($name); + } + + /** + * Creates a new ThemeHook instance based on an existing ThemeHook. + * + * @param string $name + * The machine name of the theme hook. + * @param \Drupal\Core\Theme\ThemeHook $other + * Another theme hook to use as the basis for a new theme hook. + * + * @return static + */ + public static function createFromExisting($name, ThemeHook $other) { + $instance = static::create($name); + return $instance->merge($other); + } + + /** + * Constructs a new ThemeHook using the legacy array-based format. + * + * @param string $name + * The machine name of the theme hook. + * @param array $values + * An array of values. + * + * @return static + * + * @deprecated + */ + public static function createFromLegacy($name, array $values) { + $instance = static::create($name); + foreach ($values as $key => $value) { + $instance->offsetSet($key, $value); + } + return $instance; + } + + /** + * Merges another theme hook into the values of the current theme hook. + * + * This does not modify the current theme hook, but returns a new instance. + * + * @param \Drupal\Core\Theme\ThemeHook $other + * Another theme hook to use as the basis for a new theme hook. + * + * @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->getProviderType()) { + $result->setProviderType($other->getProviderType()); + } + 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()); + } + if ($other->getVariables() && !$result->getVariables()) { + $result->setVariables($other->getVariables()); + } + if (!$result->getPattern()) { + $result->setPattern($other->getPattern()); + } + if (!$result->getBaseHook()) { + $result->setBaseHook($other->getBaseHook()); + } + if ($other->getRenderElement() && !$result->getRenderElement()) { + $result->setRenderElement($other->getRenderElement()); + } + + // 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->isIncomplete()) { + $result->markIncomplete(); + } + + // Merge all includes together. + $result->setIncludes(array_merge($other->getIncludes(), $result->getIncludes())); + + // 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; + } + + /** + * Gets the directory path of the theme or module. + * + * @return string + * The directory path of the theme or module. + */ + public function getThemePath() { + return $this->theme_path; + } + + /** + * Sets the directory path of the theme or module. + * + * @param string $theme_path + * The directory path of the theme or module. + * + * @return $this + */ + public function setThemePath($theme_path) { + $this->theme_path = $theme_path; + + return $this; + } + + /** + * Gets the type of provider of the theme hook. + * + * @return string + * The type of provider of the theme hook. May be one of: + * - 'module': A module is being checked for theme implementations. + * - 'base_theme_engine': A theme engine is being checked for a theme that + * is a parent of the actual theme being used. + * - 'theme_engine': A theme engine is being checked for the actual theme + * being used. + * - 'base_theme': A base theme is being checked for theme implementations. + * - 'theme': The actual theme in use is being checked. + */ + public function getProviderType() { + return $this->type; + } + + /** + * Sets the type of provider of the theme hook. + * + * @param string $type + * The type of provider of the theme hook. + * + * @return $this + */ + public function setProviderType($type) { + $this->type = $type; + + return $this; + } + + /** + * Gets the provider of the theme hook. + * + * @return string + * The provider of the theme hook. + */ + public function getProvider() { + return $this->type; + } + + /** + * Sets the provider of the theme hook. + * + * @param string $type + * The provider of the theme hook. + * + * @return $this + */ + public function setProvider($type) { + $this->type = $type; + + return $this; + } + + /** + * Returns the default values to be passed to the template. + * + * @return mixed[]|null + * An associative array where the keys are names of variables, and the + * values are the default values if they are not given in the render array. + * If this returns NULL, self::getRenderElement() will be used instead. + */ + public function getVariables() { + return $this->variables; + } + + /** + * Sets the default values to be passed to the template. + * + * Only used for #theme in 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 #. + * + * @param mixed[] $variables + * An associative array where the keys are names of variables, and the + * values are the default values if they are not given in the render array. + * + * @return $this + */ + public function setVariables(array $variables) { + $this->variables = $variables; + + return $this; + } + + /** + * Gets the name of the renderable element. + * + * @return string + * The name of the renderable element to pass to the theme function. + */ + public function getRenderElement() { + return $this->render_element; + } + + /** + * Sets the name of the renderable element. + * + * Used for render element items only. This name is used as the name of the + * variable that holds the renderable element or tree in preprocess functions. + * + * @param string $render_element + * The name of the renderable element to pass to the theme function. + * + * @return $this + */ + public function setRenderElement($render_element) { + $this->render_element = $render_element; + + return $this; + } + + /** + * Gets the regular expression pattern. + * + * @return string|null + * A regular expression pattern, if one exists. + */ + public function getPattern() { + return $this->pattern; + } + + /** + * Sets the regular expression pattern. + * + * A pattern allows the 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 + * + * @param string|null $pattern + * A regular expression pattern, or NULL to set no pattern. + * + * @return $this + */ + public function setPattern($pattern) { + $this->pattern = $pattern; + + return $this; + } + + /** + * Gets the base theme hook name. + * + * @return string|null + * The name of a theme hook to use as the basis for this theme hook, or NULL + * if no base hook exists. + */ + public function getBaseHook() { + return $this->base_hook; + } + + /** + * Sets the base theme hook name. + * + * Used for theme suggestions only. + * + * 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. + * + * @param string|null $base_hook + * The name of a theme hook to use as the basis for this theme hook, or NULL + * if no base hook exists. + * + * @return $this + */ + public function setBaseHook($base_hook) { + $this->base_hook = $base_hook; + + return $this; + } + + /** + * Gets the array of files to be included. + * + * @return string[] + * An array of files to be included. The file paths are relative to the + * Drupal root directory. + */ + public function getIncludes() { + return $this->includes; + } + + /** + * Sets the array of files to be included. + * + * @param string[] $includes + * An array of files to be included. The file paths must be relative to the + * Drupal root directory. + * + * @return $this + */ + public function setIncludes(array $includes) { + $this->includes = $includes; + + return $this; + } + + /** + * Adds a file to be included. + * + * @param string $include + * A path relative to the Drupal root directory, or NULL if no path is set. + * + * @return $this + */ + public function addInclude($include) { + $includes = $this->getIncludes(); + $includes[] = $include; + $this->setIncludes($includes); + + return $this; + } + + /** + * Returns whether any files are specified for inclusion. + * + * @return bool + * TRUE if files exist to be included, FALSE otherwise. + */ + public function hasIncludes() { + return !empty($this->includes); + } + + /** + * Gets the file the theme implementation resides in. + * + * @return string + * The file the theme implementation resides in. + */ + public function getFile() { + return $this->file; + } + + /** + * Gets the file the theme 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. + * + * @param string $file + * The file the theme implementation resides in. + * + * @return $this + */ + public function setFile($file) { + $this->file = $file; + + return $this; + } + + /** + * Gets the function name to invoke for this implementation, if it exists. + * + * If this is set, self::getTemplate() will be ignored. + * + * @return string|null + * The function name to invoke for the theme implementation. + * + * @deprecated in Drupal 8.0.x, will be removed in Drupal 9.0.x. + */ + public function getFunction() { + return $this->function; + } + + /** + * Sets the function name to invoke for this implementation. + * + * If this is set, self::getTemplate() will be ignored. + * + * @param string $function + * The function name to invoke for the theme implementation. + * + * @return $this + * + * @deprecated in Drupal 8.0.x, will be removed in Drupal 9.0.x. + */ + public function setFunction($function) { + $this->function = $function; + + return $this; + } + + /** + * Gets the template name to use for the theme implementation. + * + * This is ignored if self::getFunction() is set. + * + * @return string + * The template name for this theme implementation. + */ + public function getTemplate() { + return $this->template; + } + + /** + * Sets the template name to use for the theme implementation. + * + * This is ignored if self::getFunction() is set. If neither this nor + * self::getFunction() 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. + * + * @param string|null $template + * The template name for this theme implementation. 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). + * + * @return $this + */ + public function setTemplate($template) { + $this->template = $template; + + return $this; + } + + /** + * Gets the path to the theme implementation, if it exists. + * + * @return string|null + * A path relative to the Drupal root directory, or NULL if no path is set. + */ + public function getPath() { + return $this->path; + } + + /** + * Overrides the path to the file containing the theme implementation. + * + * If this is used, self::setTemplate() should also be called. + * + * @param string|null $path + * A path relative to the Drupal root directory, or NULL to use the default + * theme path. + * + * @return $this + */ + public function setPath($path) { + $this->path = $path; + + return $this; + } + + /** + * Gets the list of functions used to preprocess this data. + * + * @return string[] + * An array of functions to be called during the preprocess phase. + */ + public function getPreprocessFunctions() { + return $this->preprocess_functions; + } + + /** + * Sets the 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. + * + * @param string[] $preprocess_functions + * An array of functions to be called during the preprocess phase. + * + * @return $this + */ + public function setPreprocessFunctions(array $preprocess_functions) { + $this->preprocess_functions = array_values(array_unique($preprocess_functions)); + + return $this; + } + + /** + * Indicates if a given preprocess function will be used for this theme hook. + * + * @param string $preprocess_function + * The name of a preprocess function. + * + * @return bool + * TRUE if this preprocess function will be used for this theme hook, + * FALSE otherwise. + */ + public function hasPreprocessFunction($preprocess_function) { + return in_array($preprocess_function, $this->getPreprocessFunctions()); + } + + /** + * Adds a preprocess function to this theme hook. + * + * @param string $preprocess_function + * The name of a preprocess function. + * + * @return $this + */ + public function addPreprocessFunction($preprocess_function) { + $preprocess_functions = $this->getPreprocessFunctions(); + $preprocess_functions[] = $preprocess_function; + $this->setPreprocessFunctions($preprocess_functions); + + return $this; + } + + /** + * Determines if standard preprocess functions should be ignored. + * + * @todo This is a terrible method name. Come up with a better one. + * + * @return bool + * TRUE if standard preprocess functions should be ignored, FALSE otherwise. + */ + public function getOverriddenPreprocessFunctionStatus() { + return $this->override_preprocess_functions; + } + + /** + * Prevents standard preprocess functions from running if set to TRUE. + * + * 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. + * + * @todo This is a terrible method name. Come up with a better one. + * + * @param bool $status + * TRUE if standard preprocess functions should be ignored, FALSE otherwise. + * + * @return $this + */ + public function setOverriddenPreprocessFunctionStatus($status) { + $this->override_preprocess_functions = (bool) $status; + + return $this; + } + + /** + * Returns whether the list of preprocess functions is incomplete. + * + * Generally this is for internal use only. A completely built registry should + * contain only completed theme hooks. + * + * @return bool + * TRUE if the list of preprocess functions is incomplete. + */ + public function isIncomplete() { + return $this->incomplete_preprocess_functions; + } + + /** + * Marks this theme hook as being incomplete. + * + * @see self::isIncomplete() + * + * @return $this + */ + public function markIncomplete() { + $this->incomplete_preprocess_functions = TRUE; + + return $this; + } + + /** + * Marks this theme hook as being complete. + * + * @see self::isIncomplete() + * + * @return $this + */ + public function markComplete() { + $this->incomplete_preprocess_functions = FALSE; + + return $this; + } + + /** + * Gets the name of the theme hook. + * + * @return string + * The machine name of the theme hook. + */ + public function getName() { + return $this->name; + } + + /** + * Process a theme hook. + * + * This is intended for usage by the theme registry. + * + * @param string $root + * The app root. + * @param string $theme + * The theme currently being processed. + * @param array $module_list + * An array of module names. + * @param \Drupal\Core\Theme\ThemeHook|null $existing_theme_hook + * An existing theme hook, if it exists. + * + * @return static + */ + public function process($root, $theme, array $module_list, ThemeHook $existing_theme_hook = NULL) { + $hook = $this->getName(); + + // 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 ($this->getBaseHook()) { + $this->markIncomplete(); + } + + $this->handleIncludes($root, $existing_theme_hook); + + // 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 ($function = $this->getFunction()) { + if (!function_exists($function)) { + throw new \BadFunctionCallException(sprintf( + 'Theme hook "%s" refers to a theme function callback that does not exist: "%s"', + $hook, + $function + )); + } + } + // Provide a default naming convention for 'template' based on the 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 (!$this->getTemplate()) { + $this->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 ($this->getTemplate() && !$this->getPath()) { + $this->setPath($this->getThemePath() . '/templates'); + } + + $this->handleDefaultValues($existing_theme_hook); + $this->handlePreprocess($theme, $module_list); + + // Merge the newly created theme hooks into the existing cache. + if ($existing_theme_hook) { + return $this->merge($existing_theme_hook); + } + else { + return $this; + } + } + + /** + * Handles including any files for the processing of this theme hook. + * + * @param string $root + * The app root. + * @param \Drupal\Core\Theme\ThemeHook|null $existing_theme_hook + * An existing theme hook, if it exists. + */ + protected function handleIncludes($root, ThemeHook $existing_theme_hook = NULL) { + if ($existing_theme_hook && $existing_theme_hook->hasIncludes()) { + $this->setIncludes($existing_theme_hook->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 ($this->getFile()) { + $include_file = $this->getPath() ?: $this->getThemePath(); + $include_file .= '/' . $this->getFile(); + $this->addInclude($include_file); + } + + // Load the includes, as they may contain preprocess functions. + if ($this->hasIncludes()) { + foreach ($this->getIncludes() as $include_file) { + include_once $root . '/' . $include_file; + } + } + } + + /** + * Handles merging in any default values from an existing theme hook. + * + * @param \Drupal\Core\Theme\ThemeHook|null $existing_theme_hook + * An existing theme hook, if it exists. + */ + protected function handleDefaultValues(ThemeHook $existing_theme_hook = NULL) { + if ($existing_theme_hook) { + if ($existing_theme_hook->getVariables() && !$this->getVariables()) { + $this->setVariables($existing_theme_hook->getVariables()); + } + if (!$this->getPattern()) { + $this->setPattern($existing_theme_hook->getPattern()); + } + if (!$this->getBaseHook()) { + $this->setBaseHook($existing_theme_hook->getBaseHook()); + } + if ($existing_theme_hook->getRenderElement() && !$this->getRenderElement()) { + $this->setRenderElement($existing_theme_hook->getRenderElement()); + } + } + } + + /** + * Generates the list of preprocess functions from the theme and module list. + * + * @param string $theme + * The theme currently being processed. + * @param array $module_list + * An array of module names. + */ + protected function handlePreprocess($theme, array $module_list) { + // Preprocess variables for all theming hooks, whether the hook is + // implemented as a template or as a function. Ensure they are arrays. + if (!$this->getPreprocessFunctions()) { + $provider_name = $this->getProvider(); + $type = $this->getProviderType(); + $hook = $this->getName(); + + $prefixes = []; + if ($type == 'module') { + // Default variable preprocessor prefix. + $prefixes[] = 'template'; + // Add all modules so they can intervene with their own variable + // preprocessors. This allows them to provide variable preprocessors + // even if they are not the owner of the current hook. + $prefixes = array_merge($prefixes, $module_list); + } + elseif ($type == 'theme_engine' || $type == 'base_theme_engine') { + // Theme engines get an extra set that come before the normally + // named variable preprocessors. + $prefixes[] = $provider_name . '_engine'; + // The theme engine registers on behalf of the theme using the + // theme's name. + $prefixes[] = $theme; + } + else { + // This applies when the theme manually registers their own variable + // preprocessors. + $prefixes[] = $provider_name; + } + foreach ($prefixes as $prefix) { + // Only use non-hook-specific variable preprocessors for theming + // hooks implemented as templates. See the @defgroup themeable + // topic. + if ($this->getTemplate() && function_exists($prefix . '_preprocess')) { + $this->addPreprocessFunction($prefix . '_preprocess'); + } + if (function_exists($prefix . '_preprocess_' . $hook)) { + $this->addPreprocessFunction($prefix . '_preprocess_' . $hook); + } + } + } + } + +} diff --git a/core/lib/Drupal/Core/Theme/ThemeManager.php b/core/lib/Drupal/Core/Theme/ThemeManager.php index c58d176..8ba9bbb 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->getProviderType() != '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/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..2c94d7f 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,8 @@ function theme_test_theme($existing, $type, $theme, $path) { 'bar' => '', ], ]; + $items[] = ThemeHook::create('theme_test_object_based') + ->setRenderElement('elements'); return $items; } diff --git a/core/tests/Drupal/KernelTests/Core/Theme/RegistryTest.php b/core/tests/Drupal/KernelTests/Core/Theme/RegistryTest.php index a962fec..1c1e06d 100644 --- a/core/tests/Drupal/KernelTests/Core/Theme/RegistryTest.php +++ b/core/tests/Drupal/KernelTests/Core/Theme/RegistryTest.php @@ -141,7 +141,16 @@ public function testSuggestionPreprocessFunctions() { $preprocess_functions = $registry_theme->get()['theme_test_preprocess_suggestions__kitten__bearcat']['preprocess functions']; $this->assertIdentical($expected_preprocess_functions, $preprocess_functions, 'Suggestion implemented as a template correctly inherits preprocess functions.'); + // Test an object-based theme hook. + $info = $registry_theme->get()['theme_test_object_based']; + $this->assertSame(['template_preprocess'], $info['preprocess functions']); + $this->assertSame('elements', $info['render element']); + $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->getName()); + $this->assertTrue($info->getTemplate() || $info->getFunction()); + } } /** diff --git a/core/tests/Drupal/Tests/Core/Theme/RegistryTest.php b/core/tests/Drupal/Tests/Core/Theme/RegistryTest.php index f2cd22e..fe129cd 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,20 @@ public function testGetRegistryForModule() { public function testPostProcessExtension($defined_functions, $hooks, $expected) { static::$functions['user'] = $defined_functions; + // @todo The BC layer is in \Drupal\Core\Theme\Registry::processExtension() + // which this test bypasses via reflection. These legacy conversion calls + // are necessary until the test data and expectations are converted. + 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');