/** * Builds the theme registry cache. * * Theme hook definitions are collected in the following order: * - Modules * - Base theme engines * - Base themes * - Theme engine * - Theme * * All theme hook definitions are essentially just collated and merged in the * above order. However, various extension-specific default values and * customizations are required; e.g., to record the effective file path for * theme template. Therefore, this method first collects all extensions per * type, and then dispatches the processing for each extension to * processExtension(). * * After completing the collection, modules are allowed to alter it. Lastly, * any derived and incomplete theme hook definitions that are hook suggestions * for base hooks (e.g., 'block__node' for the base hook 'block') need to be * determined based on the full registry and classified as 'base hook'. * * See the @link themeable Default theme implementations topic @endlink for * details. * * @return \Drupal\Core\Utility\ThemeRegistry * The build theme registry. * * @see hook_theme_registry_alter() */ protected function build() { $cache = array(); // First, preprocess the theme hooks advertised by modules. This will // serve as the basic registry. Since the list of enabled modules is the // same regardless of the theme used, this is cached in its own entry to // save building it for every theme. if ($cached = $this->cache->get('theme_registry:build:modules')) { $cache = $cached->data; } else { foreach ($this->moduleHandler->getImplementations('theme') as $module) { $this->processExtension($cache, $module, 'module', $module, $this->getPath($module)); } // Only cache this registry if all modules are loaded. if ($this->moduleHandler->isLoaded()) { $this->cache->set("theme_registry:build:modules", $cache, Cache::PERMANENT, array('theme_registry')); } } // 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) { // If the base theme uses a theme engine, process its hooks. $base_path = $base->getPath(); if ($this->theme->getEngine()) { $this->processExtension($cache, $this->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()); } // Hooks provided by the theme itself. $this->processExtension($cache, $this->theme->getName(), 'theme', $this->theme->getName(), $this->theme->getPath()); // Discover and add all preprocess functions for theme hook suggestions. $this->postProcessExtension($cache, $this->theme); // Let modules and themes alter the registry. $this->moduleHandler->alter('theme_registry', $cache); $this->themeManager->alterForTheme($this->theme, 'theme_registry', $cache); // @todo Implement more reduction of the theme registry entry. // Optimize the registry to not have empty arrays for functions. foreach ($cache as $hook => $info) { if (empty($info['preprocess functions'])) { unset($cache[$hook]['preprocess functions']); } } $this->registry[$this->theme->getName()] = $cache; return $this->registry[$this->theme->getName()]; }
/** * Completes the theme registry adding discovered functions and hooks. * * @param array $cache * The theme registry as documented in * \Drupal\Core\Theme\Registry::processExtension(). * @param \Drupal\Core\Theme\ActiveTheme $theme * Current active theme. * * @see ::processExtension() */ protected function postProcessExtension(array &$cache, ActiveTheme $theme) { $grouped_functions = $this->getPrefixGroupedUserFunctions(); // Gather prefixes. This will be used to limit the found functions to the // expected naming conventions. $prefixes = array_keys((array) $this->moduleHandler->getModuleList()); foreach (array_reverse($theme->getBaseThemes()) as $base) { $prefixes[] = $base->getName(); } if ($theme->getEngine()) { $prefixes[] = $theme->getEngine() . '_engine'; } $prefixes[] = $theme->getName(); // Collect all variable preprocess functions in the correct order. $suggestion_level = []; $matches = []; // Look for functions named according to the pattern and add them if they // have matching hooks in the registry. foreach ($prefixes as $prefix) { // Grep only the functions which are within the prefix group. list($first_prefix, ) = explode('_', $prefix, 2); if (!isset($grouped_functions[$first_prefix])) { continue; } // Add the function and the name of the associated theme hook to the list // of preprocess functions grouped by suggestion specificity if a matching // base hook is found. foreach ($grouped_functions[$first_prefix] as $candidate) { if (preg_match("/^{$prefix}_preprocess_(((?:[^_]++|_(?!_))+)__.*)/", $candidate, $matches)) { if (isset($cache[$matches[2]])) { $level = substr_count($matches[1], '__'); $suggestion_level[$level][$candidate] = $matches[1]; } } } } // Add missing variable preprocessors. This is needed for modules that do // not explicitly register the hook. For example, when a theme contains a // variable preprocess function but it does not implement a template, it // will go missing. This will add the expected function. It also allows // modules or themes to have a variable process function based on a pattern // even if the hook does not exist. 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'])) { // Add missing preprocessor to existing hook. $cache[$hook]['preprocess functions'][] = $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; } } } // Inherit all base hook variable preprocess functions into suggestion // hooks. This ensures that derivative hooks have a complete set of variable // preprocess functions. foreach ($cache as $hook => $info) { // 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'])) { $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']); } } }
/** * {@inheritdoc} * * @todo Should we cache some of these information? */ public function alterForTheme(ActiveTheme $theme, $type, &$data, &$context1 = NULL, &$context2 = NULL) { // Most of the time, $type is passed as a string, so for performance, // normalize it to that. When passed as an array, usually the first item in // the array is a generic type, and additional items in the array are more // specific variants of it, as in the case of array('form', 'form_FORM_ID'). if (is_array($type)) { $extra_types = $type; $type = array_shift($extra_types); // Allow if statements in this function to use the faster isset() rather // than !empty() both when $type is passed as a string, or as an array with // one item. if (empty($extra_types)) { unset($extra_types); } } $theme_keys = array(); foreach ($theme->getBaseThemes() as $base) { $theme_keys[] = $base->getName(); } $theme_keys[] = $theme->getName(); $functions = array(); foreach ($theme_keys as $theme_key) { $function = $theme_key . '_' . $type . '_alter'; if (function_exists($function)) { $functions[] = $function; } if (isset($extra_types)) { foreach ($extra_types as $extra_type) { $function = $theme_key . '_' . $extra_type . '_alter'; if (function_exists($function)) { $functions[] = $function; } } } } foreach ($functions as $function) { $function($data, $context1, $context2); } }
/** * {@inheritdoc} */ public function loadActiveTheme(ActiveTheme $active_theme) { // Initialize the theme. if ($theme_engine = $active_theme->getEngine()) { // Include the engine. include_once DRUPAL_ROOT . '/' . $active_theme->getOwner(); if (function_exists($theme_engine . '_init')) { foreach ($active_theme->getBaseThemes() as $base) { call_user_func($theme_engine . '_init', $base->getExtension()); } call_user_func($theme_engine . '_init', $active_theme->getExtension()); } } else { // include non-engine theme files foreach ($active_theme->getBaseThemes() as $base) { // Include the theme file or the engine. if ($base->getOwner()) { include_once DRUPAL_ROOT . '/' . $base->getOwner(); } } // and our theme gets one too. if ($active_theme->getOwner()) { include_once DRUPAL_ROOT . '/' . $active_theme->getOwner(); } } // Always include Twig as the default theme engine. include_once DRUPAL_ROOT . '/core/themes/engines/twig/twig.engine'; }
/** * Ensures the phase functions are invoked in the correct order. * * @param array $functions * The phase functions to iterate over. * @param string $hook * The current hook being processed. * @param \Drupal\Core\Theme\ActiveTheme $theme * Current active theme. * * @see https://www.drupal.org/node/2098551 */ protected function sortFunctions(array &$functions, $hook, ActiveTheme $theme) { // Immediately return if there is nothing to sort. if (count($functions) < 2) { return; } $themes = array_keys($theme->getBaseThemes()); $themes[] = $theme->getName(); // Create an associative array of theme functions to ensure sort order. $theme_functions = array_fill_keys($themes, array()); // Iterate over all the themes. foreach ($themes as $theme) { // Only add the function to the array of theme functions if it currently // exists in the $functions array. $function = $theme . '_preprocess_' . $hook; $key = array_search($function, $functions); if ($key !== FALSE) { // Save the theme function to be added later, but sorted. $theme_functions[$theme][] = $function; // Remove it from the current $functions array. unset($functions[$key]); } } // Iterate over all the captured theme functions and place them back into // the phase functions array. foreach ($theme_functions as $array) { $functions = array_merge($functions, $array); } }