diff --git a/core/lib/Drupal/Core/Theme/Registry.php b/core/lib/Drupal/Core/Theme/Registry.php index a0af702..6f007ce 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,172 @@ 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()]; + } + 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); + } + } + } + } + protected function handleResult($hook, ThemeHook $info, $name, array $module_list, $theme, 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->setFlag('incomplete preprocess functions'); + } + + if ($cached_info && $cached_info->getIncludes()) { + $info->setIncludes($cached_info->getIncludes()); + } + + // 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()]; + // 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(); + include_once $this->root . '/' . $include_file; + $info->addInclude($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 ($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) { + if ($cached_info->getVariables() && !$info->getVariables()) { + $info->setVariables($cached_info->getVariables()); + } + if ($cached_info->getRenderElement() && !$info->getRenderElement()) { + $info->setRenderElement($cached_info->getRenderElement()); + } + if ($cached_info->getPattern() && !$info->getPattern()) { + $info->setPattern($cached_info->getPattern()); + } + if ($cached_info->getBaseHook() && !$info->getBaseHook()) { + $info->setBaseHook($cached_info->getBaseHook()); + } + } + $this->handlePreprocessFunctions($info, $module_list, $name, $theme, $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 ($info->hasFlag('override preprocess functions')) { + // Flag not needed inside the registry. + $info->unsetFlag('override preprocess functions'); + } + elseif ($cached_info && $cached_info->getPreprocessFunctions()) { + $info->setPreprocessFunctions(array_merge($cached_info->getPreprocessFunctions(), $info->getPreprocessFunctions())); + } + return $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 +546,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 +553,26 @@ 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; + if (is_array($info)) { + $info = ThemeHook::createFromLegacy($info); + } + + if (!($info instanceof ThemeHook)) { + throw new \UnexpectedValueException('booooo'); + } // Fill in the type and path of the module, theme, or engine that // implements this theme function. - $result[$hook]['type'] = $type; - $result[$hook]['theme path'] = $path; - - // If 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']; + $info->setType($type); + $info->setThemePath($path); + $to_be_merged[$hook] = $this->handleResult($hook, $info, $name, $module_list, $theme, $cached_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 +581,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,7 +600,7 @@ 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(). */ @@ -609,23 +610,23 @@ protected function completeSuggestion($hook, array &$cache) { // 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]->hasFlag('incomplete preprocess functions')) && $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'])) { + if (isset($cache[$previous_hook]) && !$incomplete_previous_hook && $cache[$previous_hook]->hasFlag('incomplete preprocess functions')) { $incomplete_previous_hook = $cache[$previous_hook]; - unset($incomplete_previous_hook['incomplete preprocess functions']); + $incomplete_previous_hook->unsetFlag('incomplete preprocess functions'); } $previous_hook = substr($previous_hook, 0, $pos); $this->mergePreprocessFunctions($hook, $previous_hook, $incomplete_previous_hook, $cache); } // In addition to processing suggestions, include base hooks. - if (isset($cache[$hook]['base hook'])) { + if (isset($cache[$hook]) && $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[$hook], $cache); } } @@ -636,26 +637,26 @@ 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 $parent_hook * The parent hook if it exists. Either an incomplete hook from suggestions * or a base hook. - * @param array $cache + * @param \Drupal\Core\Theme\ThemeHook[] $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, ThemeHook $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']))) { + if (isset($cache[$source_hook_name]) && (!$cache[$source_hook_name]->hasFlag('incomplete preprocess functions') || !$cache[$destination_hook_name]->hasFlag('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 ($parent_hook->getPreprocessFunctions()) { + $diff = array_diff($parent_hook->getPreprocessFunctions(), $cache[$source_hook_name]->getPreprocessFunctions()); + $cache[$destination_hook_name]->setPreprocessFunctions(array_merge($cache[$source_hook_name]->getPreprocessFunctions(), $diff)); } // 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[$source_hook_name]->getBaseHook()) { + $cache[$destination_hook_name]->setBaseHook($source_hook_name); } } } @@ -663,7 +664,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 +677,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 +720,17 @@ 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]) && !in_array($hook, $cache[$hook]->getPreprocessFunctions())) { // 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] = ThemeHook::createEmpty(); + $cache[$hook]->addPreprocessFunction($preprocessor); } } } @@ -738,19 +741,13 @@ protected function postProcessExtension(array &$cache, ActiveTheme $theme) { // The 'base hook' is only applied to derivative hooks already registered // from a pattern. This is typically set from // drupal_find_theme_functions() and drupal_find_theme_templates(). - if (isset($info['incomplete preprocess functions'])) { + if ($info->hasFlag('incomplete preprocess functions')) { $this->completeSuggestion($hook, $cache); - unset($cache[$hook]['incomplete preprocess functions']); + $info->unsetFlag('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->setPreprocessFunctions(array_unique($info->getPreprocessFunctions())); } } @@ -783,12 +780,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 +815,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..3c5ebfe --- /dev/null +++ b/core/lib/Drupal/Core/Theme/ThemeHook.php @@ -0,0 +1,451 @@ + $value) { + $this->offsetSet($name, $value); + } + } + + /** + * @todo. + */ + public static function createEmpty() { + return new static(); + } + + /** + * @todo. + */ + public static function createFromLegacy(array $info) { + return new static($info); + } + + /** + * @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[] + */ + public function getVariables() { + return $this->variables; + } + + /** + * @todo. + * + * @param mixed[] $variables + * + * @return $this + */ + public function setVariables($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; + } + + /** + * {@inheritdoc} + */ + public function &offsetGet($name) { + $name = str_replace(' ', '_', $name); + $value = NULL; + if (property_exists($this, $name)) { + $value = $this->{$name}; + } + else { + 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 { + 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 { + throw new \Exception($name); + } + } + + /** + * {@inheritdoc} + */ + public function offsetExists($name) { + $name = str_replace(' ', '_', $name); + if (property_exists($this, $name)) { + return isset($this->{$name}); + } + else { + throw new \Exception($name); + } + } + + /** + * @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) { + $this->includes[] = $include; + + 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 = $preprocess_functions; + + return $this; + } + public function addPreprocessFunction($preprocess_function) { + $this->preprocess_functions[] = $preprocess_function; + } + + /** + * @param string $name + * + * @return bool + */ + public function hasFlag($name) { + return !empty($this->flag[$name]); + } + + /** + * @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/system/tests/modules/theme_test/theme_test.module b/core/modules/system/tests/modules/theme_test/theme_test.module index 2e6f164..3884a27 100644 --- a/core/modules/system/tests/modules/theme_test/theme_test.module +++ b/core/modules/system/tests/modules/theme_test/theme_test.module @@ -6,11 +6,13 @@ */ use Drupal\Core\Extension\Extension; +use Drupal\Core\Theme\ThemeHook; /** * Implements hook_theme(). */ function theme_test_theme($existing, $type, $theme, $path) { + $items['asdf'] = ThemeHook::createEmpty(); $items['theme_test'] = [ 'file' => 'theme_test.inc', 'variables' => ['foo' => ''], diff --git a/core/tests/Drupal/Tests/Core/Theme/RegistryTest.php b/core/tests/Drupal/Tests/Core/Theme/RegistryTest.php index f2cd22e..cd06a15 100644 --- a/core/tests/Drupal/Tests/Core/Theme/RegistryTest.php +++ b/core/tests/Drupal/Tests/Core/Theme/RegistryTest.php @@ -7,9 +7,16 @@ 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\ThemeInitializationInterface; +use Drupal\Core\Theme\ThemeManagerInterface; use Drupal\Tests\UnitTestCase; +use Prophecy\Argument; /** * @coversDefaultClass \Drupal\Core\Theme\Registry @@ -25,20 +32,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 +39,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 +58,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 +108,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 +146,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'])); @@ -196,9 +176,7 @@ public function testPostProcessExtension($defined_functions, $hooks, $expected) $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 +463,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 {