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..0022c76 100644 --- a/core/lib/Drupal/Core/Theme/Registry.php +++ b/core/lib/Drupal/Core/Theme/Registry.php @@ -25,13 +25,6 @@ class Registry implements DestructableInterface { /** - * The theme object representing the active theme for this registry. - * - * @var \Drupal\Core\Theme\ActiveTheme - */ - protected $theme; - - /** * The lock backend that should be used. * * @var \Drupal\Core\Lock\LockBackendInterface @@ -106,13 +99,6 @@ class Registry implements DestructableInterface { protected $runtimeRegistry = []; /** - * Stores whether the registry was already initialized. - * - * @var bool - */ - protected $initialized = FALSE; - - /** * The name of the theme for which to construct the registry, if given. * * @var string|null @@ -141,6 +127,13 @@ class Registry implements DestructableInterface { protected $themeManager; /** + * The theme initialization. + * + * @var \Drupal\Core\Theme\ThemeInitializationInterface + */ + protected $themeInitialization; + + /** * The runtime cache. * * @var \Drupal\Core\Cache\CacheBackendInterface @@ -196,37 +189,37 @@ public function setThemeManager(ThemeManagerInterface $theme_manager) { * * @param string $theme_name * (optional) The name of the theme for which to construct the registry. + * + * @return \Drupal\Core\Theme\ActiveTheme + * The active theme. */ - protected function init($theme_name = NULL) { - if ($this->initialized) { - return; - } + protected function getTheme($theme_name = NULL) { // Unless instantiated for a specific theme, use globals. if (!isset($theme_name)) { - $this->theme = $this->themeManager->getActiveTheme(); + $theme = $this->themeManager->getActiveTheme(); } // Instead of the active theme, a specific theme was requested. else { - $this->theme = $this->themeInitialization->getActiveThemeByName($theme_name); - $this->themeInitialization->loadActiveTheme($this->theme); + $theme = $this->themeInitialization->initTheme($theme_name); } + return $theme; } /** * 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 */ public function get() { - $this->init($this->themeName); - if (isset($this->registry[$this->theme->getName()])) { - return $this->registry[$this->theme->getName()]; + $theme = $this->getTheme($this->themeName); + if (isset($this->registry[$theme->getName()])) { + return $this->registry[$theme->getName()]; } - if ($cache = $this->cache->get('theme_registry:' . $this->theme->getName())) { - $this->registry[$this->theme->getName()] = $cache->data; + if ($cache = $this->cache->get('theme_registry:' . $theme->getName())) { + $this->registry[$theme->getName()] = $cache->data; } else { $this->build(); @@ -235,7 +228,7 @@ public function get() { $this->setCache(); } } - return $this->registry[$this->theme->getName()]; + return $this->registry[$theme->getName()]; } /** @@ -247,18 +240,19 @@ public function get() { * lightweight than the full registry. */ public function getRuntime() { - $this->init($this->themeName); - if (!isset($this->runtimeRegistry[$this->theme->getName()])) { - $this->runtimeRegistry[$this->theme->getName()] = new ThemeRegistry('theme_registry:runtime:' . $this->theme->getName(), $this->runtimeCache ?: $this->cache, $this->lock, ['theme_registry'], $this->moduleHandler->isLoaded()); + $theme = $this->getTheme($this->themeName); + if (!isset($this->runtimeRegistry[$theme->getName()])) { + $this->runtimeRegistry[$theme->getName()] = new ThemeRegistry('theme_registry:runtime:' . $theme->getName(), $this->runtimeCache ?: $this->cache, $this->lock, ['theme_registry'], $this->moduleHandler->isLoaded()); } - return $this->runtimeRegistry[$this->theme->getName()]; + return $this->runtimeRegistry[$theme->getName()]; } /** * Persists the theme registry in the cache backend. */ protected function setCache() { - $this->cache->set('theme_registry:' . $this->theme->getName(), $this->registry[$this->theme->getName()], Cache::PERMANENT, ['theme_registry']); + $theme = $this->getTheme($this->themeName); + $this->cache->set('theme_registry:' . $theme->getName(), $this->registry[$theme->getName()], Cache::PERMANENT, ['theme_registry']); } /** @@ -271,7 +265,6 @@ protected function setCache() { * The name of the base hook or FALSE. */ public function getBaseHook($hook) { - $this->init($this->themeName); $base_hook = $hook; // Iteratively strip everything after the last '__' delimiter, until a // base hook definition is found. Recursive base hooks of base hooks are @@ -338,49 +331,165 @@ protected function build() { } } + $theme = $this->getTheme($this->themeName); // Process each base theme. // Ensure that we start with the root of the parents, so that both CSS files // and preprocess functions comes first. - foreach (array_reverse($this->theme->getBaseThemes()) as $base) { + foreach (array_reverse($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); + if ($theme->getEngine()) { + $this->processExtension($cache, $theme->getEngine(), 'base_theme_engine', $base->getName(), $base_path); } $this->processExtension($cache, $base->getName(), 'base_theme', $base->getName(), $base_path); } // And then the same thing, but for the theme. - if ($this->theme->getEngine()) { - $this->processExtension($cache, $this->theme->getEngine(), 'theme_engine', $this->theme->getName(), $this->theme->getPath()); + if ($theme->getEngine()) { + $this->processExtension($cache, $theme->getEngine(), 'theme_engine', $theme->getName(), $theme->getPath()); } // Hooks provided by the theme itself. - $this->processExtension($cache, $this->theme->getName(), 'theme', $this->theme->getName(), $this->theme->getPath()); + $this->processExtension($cache, $theme->getName(), 'theme', $theme->getName(), $theme->getPath()); // Discover and add all preprocess functions for theme hook suggestions. - $this->postProcessExtension($cache, $this->theme); + $this->postProcessExtension($cache, $theme); // Let modules and themes alter the registry. $this->moduleHandler->alter('theme_registry', $cache); - $this->themeManager->alterForTheme($this->theme, 'theme_registry', $cache); + $this->themeManager->alterForTheme($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[$theme->getName()] = $cache; + + return $this->registry[$theme->getName()]; + } + + /** + * @todo. + */ + protected function handlePreprocessFunctions(ThemeHook $info, array $module_list, $name, $theme, $hook) { + // Preprocess variables for all theming hooks, whether the hook is + // implemented as a template or as a function. Ensure they are arrays. + if (!$info->getPreprocessFunctions()) { + $prefixes = []; + if ($info->getType() == '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 ($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'; + // 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 ($info->getTemplate() && function_exists($prefix . '_preprocess')) { + $info->addPreprocessFunction($prefix . '_preprocess'); + } + if (function_exists($prefix . '_preprocess_' . $hook)) { + $info->addPreprocessFunction($prefix . '_preprocess_' . $hook); + } + } + } + } + + /** + * @todo. + */ + protected function handleIncludes(ThemeHook $info, ThemeHook $cached_info = NULL) { + 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 ($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; } } - $this->registry[$this->theme->getName()] = $cache; + } - return $this->registry[$this->theme->getName()]; + /** + * @todo. + */ + protected function handleResult($hook, ThemeHook $info, ThemeHook $cached_info = NULL) { + // When a theme or engine overrides a module's theme function $info will + // only contain key/value pairs for information being overridden. Pull the + // rest of the information from what was defined by an earlier hook. + + // 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 ($info->getBaseHook()) { + $info->markPreprocessFunctionsIncomplete(); + } + + $this->handleIncludes($info, $cached_info); + + // 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 = $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, + $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 (!$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 ($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 ($cached_info) { + $info->mergeVariables($cached_info); + $info->mergeRenderElement($cached_info); + $info->mergePattern($cached_info); + $info->mergeBaseHook($cached_info); + } } /** * 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: @@ -430,13 +539,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 @@ -444,131 +546,32 @@ protected function processExtension(array &$cache, $name, $type, $theme, $path) $function = $name . '_theme'; if (function_exists($function)) { $result = $function($cache, $type, $theme, $path); + $to_be_merged = []; foreach ($result as $hook => $info) { - // When a theme or engine overrides a module's theme function - // $result[$hook] will only contain key/value pairs for information being - // overridden. Pull the rest of the information from what was defined by - // an earlier hook. + $cached_info = isset($cache[$hook]) ? $cache[$hook] : NULL; + // @todo. + if (is_array($info)) { + $info = ThemeHook::createFromLegacy($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); + $this->handleResult($hook, $info, $cached_info); + $this->handlePreprocessFunctions($info, $module_list, $name, $theme, $hook); - // 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']); + 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']; + $to_be_merged[$hook] = $info; } // Merge the newly created theme hooks into the existing cache. - $cache = $result + $cache; + $cache = $to_be_merged + $cache; } // Let themes have variable preprocessors even if they didn't register a @@ -577,17 +580,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 +599,42 @@ 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 (!$incomplete_previous_hook) { + if (isset($cache[$previous_hook]) && $cache[$previous_hook]->hasIncompletePreprocessFunctions()) { + $incomplete_previous_hook = $cache[$previous_hook]; + $incomplete_previous_hook->markPreprocessFunctionsComplete(); + } + else { + $incomplete_previous_hook = ThemeHook::create(); + } } $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'])) { + if ($cache[$hook]->getBaseHook()) { // 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); + $this->mergePreprocessFunctions($hook, $cache[$hook]->getBaseHook(), $cache, $cache[$hook]); } + + // @todo. + $cache[$hook]->markPreprocessFunctionsComplete(); } /** @@ -636,26 +644,23 @@ 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) { + protected function mergePreprocessFunctions($destination_hook_name, $source_hook_name, array &$cache, ThemeHook $parent_hook) { // 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); - } + if (isset($cache[$source_hook_name]) && (!$cache[$source_hook_name]->hasIncompletePreprocessFunctions() || !isset($cache[$destination_hook_name]) || !$cache[$destination_hook_name]->hasIncompletePreprocessFunctions())) { + $cache[$destination_hook_name] = $parent_hook->merge($cache[$source_hook_name]); + // 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 +668,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 +681,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 +724,20 @@ 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; + // @todo. + if (!isset($cache[$hook])) { + throw new \Exception($hook); + } + $cache[$hook]->addPreprocessFunction($preprocessor); } } } @@ -738,18 +748,9 @@ 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']); - } - - // 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']); + $info->markPreprocessFunctionsComplete(); } } } @@ -783,12 +784,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 +819,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..2071f7d --- /dev/null +++ b/core/lib/Drupal/Core/Theme/ThemeHook.php @@ -0,0 +1,668 @@ + 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 the attribute data. + * + * @var \Drupal\Core\Template\AttributeValueBase[] + */ + protected $storage = []; + + /** + * Constructs a \Drupal\Core\Template\Attribute object. + * + * @param array $attributes + * An associative array of key-value pairs to be converted to attributes. + */ + protected function __construct($attributes = []) { + foreach ($attributes as $name => $value) { + $this->offsetSet($name, $value); + } + } + + /** + * @todo. + */ + public static function create() { + return new static(); + } + + /** + * @todo. + * + * @deprecated + */ + public static function createFromLegacy(array $info) { + return new static($info); + } + + /** + * @return $this + */ + 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->markPreprocessFunctionsIncomplete(); + } + + // @todo. + $result->setIncludes(array_merge($result->getIncludes(), $other->getIncludes())); + + // @todo. + $result->mergeVariables($other); + $result->mergeRenderElement($other); + $result->mergePattern($other); + $result->mergeBaseHook($other); + + $result->mergePreprocessFunctions($other); + + return $result; + } + public function mergeVariables(ThemeHook $other) { + if ($other->getVariables() && !$this->getVariables()) { + $this->setVariables($other->getVariables()); + } + } + public function mergePattern(ThemeHook $other) { + if (!$this->getPattern()) { + $this->setPattern($other->getPattern()); + } + } + public function mergeBaseHook(ThemeHook $other) { + if (!$this->getBaseHook()) { + $this->setBaseHook($other->getBaseHook()); + } + } + public function mergeRenderElement(ThemeHook $other) { + 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) { + $name = str_replace(' ', '_', $name); + if (property_exists($this, $name)) { + $value = &$this->{$name}; + } + else { + // @todo. + throw new \Exception($name); + } + return $value; + } + + /** + * {@inheritdoc} + */ + public function offsetSet($name, $value) { + $name = str_replace(' ', '_', $name); + if (property_exists($this, $name)) { + $this->{$name} = $value; + } + else { + // @todo. + throw new \Exception($name); + } + } + + /** + * {@inheritdoc} + */ + public function offsetUnset($name) { + $name = str_replace(' ', '_', $name); + if (property_exists($this, $name)) { + $reflection = new \ReflectionClass($this); + $this->{$name} = $reflection->getDefaultProperties()[$name]; + } + else { + // @todo. + throw new \Exception($name); + } + } + + /** + * {@inheritdoc} + */ + public function offsetExists($name) { + $name = str_replace(' ', '_', $name); + if (property_exists($this, $name)) { + return isset($this->{$name}); + } + else { + // @todo. + throw new \Exception($name); + } + } + + /** + * @return mixed + */ + public function getThemePath() { + return $this->theme_path; + } + + /** + * @todo. + * + * @param string $theme_path + * + * @return $this + */ + public function setThemePath($theme_path) { + $this->theme_path = $theme_path; + + return $this; + } + + /** + * @todo. + * + * @return string + */ + public function getType() { + return $this->type; + } + + /** + * @todo. + * + * @param string $type + */ + public function setType($type) { + $this->type = $type; + + return $this; + } + + /** + * @todo. + * + * @return mixed[]|null + */ + public function getVariables() { + return $this->variables; + } + + /** + * @todo. + * + * @param mixed[] $variables + * + * @return $this + */ + public function setVariables(array $variables) { + $this->variables = $variables; + + return $this; + } + + /** + * @todo. + * + * @return string + */ + public function getRenderElement() { + return $this->render_element; + } + + /** + * @todo. + * + * @param string $render_element + * + * @return $this + */ + public function setRenderElement($render_element) { + $this->render_element = $render_element; + + return $this; + } + + /** + * @todo. + * + * @return string + */ + public function getPattern() { + return $this->pattern; + } + + /** + * @todo. + * + * @param string $pattern + * + * @return $this + */ + public function setPattern($pattern) { + $this->pattern = $pattern; + + return $this; + } + + /** + * @todo. + * + * @return string + */ + public function getBaseHook() { + return $this->base_hook; + } + + /** + * @todo. + * + * @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 hasIncompletePreprocessFunctions() { + return $this->incomplete_preprocess_functions; + } + + /** + * @return $this + */ + public function markPreprocessFunctionsIncomplete() { + $this->incomplete_preprocess_functions = TRUE; + + return $this; + } + + /** + * @return $this + */ + public function markPreprocessFunctionsComplete() { + $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; + } + +} 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/tests/Drupal/Tests/Core/Theme/RegistryTest.php b/core/tests/Drupal/Tests/Core/Theme/RegistryTest.php index f2cd22e..0453502 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'])); @@ -191,14 +172,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($hook); + } + } + foreach ($hooks as $name => $hook) { + if (is_array($hook)) { + $hooks[$name] = ThemeHook::createFromLegacy($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 +475,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 {