diff --git a/core/includes/theme.inc b/core/includes/theme.inc index 89c9c54..2d65525 100644 --- a/core/includes/theme.inc +++ b/core/includes/theme.inc @@ -808,10 +808,8 @@ function drupal_find_theme_functions($cache, $prefixes) { if ($matches) { foreach ($matches as $match) { $new_hook = substr($match, strlen($prefix) + 1); - $arg_name = isset($info['variables']) ? 'variables' : 'render element'; $implementations[$new_hook] = array( 'function' => $match, - $arg_name => $info[$arg_name], 'base hook' => $hook, ); } @@ -931,11 +929,9 @@ function drupal_find_theme_templates($cache, $extension, $path) { } // Put the underscores back in for the hook name and register this // pattern. - $arg_name = isset($info['variables']) ? 'variables' : 'render element'; $implementations[strtr($file, '-', '_')] = array( 'template' => $file, 'path' => dirname($files[$match]->uri), - $arg_name => $info[$arg_name], 'base hook' => $hook, ); } diff --git a/core/lib/Drupal/Core/Theme/Registry.php b/core/lib/Drupal/Core/Theme/Registry.php index cc17d6f..0cd5592 100644 --- a/core/lib/Drupal/Core/Theme/Registry.php +++ b/core/lib/Drupal/Core/Theme/Registry.php @@ -314,7 +314,7 @@ protected function build() { drupal_alter('theme_registry', $registry); $this->registry = $registry; - $this->postProcess(); + $this->compile(); return $this->registry; } @@ -324,7 +324,7 @@ protected function build() { * @param array $registry * The theme registry that will eventually be cached. * @param string $type - * One of 'module', 'base_theme_engine', 'theme_engine', 'base_theme', or + * One of 'module', 'base_theme_engine', 'base_theme', 'theme_engine', or * 'theme'. * @param string $name * The name of the extension implementing hook_theme(). @@ -365,6 +365,8 @@ protected function processExtension(&$registry, $type, $name, $theme_name, $them // 'render element', in which case we need to assign default values for // 'template' or 'function'. if (isset($info['variables']) || isset($info['render element'])) { + // Add an internal build process marker to track that this an actual + // theme hook and not a suggestion. $info['exists'] = TRUE; // The effective path_to_theme() during theme(). @@ -377,7 +379,7 @@ protected function processExtension(&$registry, $type, $name, $theme_name, $them $info['path'] = $theme_path . '/templates'; } // Find the preferred theme engine for this module template. - // @todo Remove this hack. Simply support multiple theme engines; + // @todo Remove this. Simply support multiple theme engines; // which will simplify the entire processing in the first place. if ($type == 'module') { $render_engines = array( @@ -414,7 +416,7 @@ protected function processExtension(&$registry, $type, $name, $theme_name, $them // for base hooks, since suggestions are extending hooks horizontally // (instead of overriding vertically); therefore it happens after // per-extension processing. - // @see Registry::build() + // @see Registry::compile() // Revert the above theme engine hack for Twig, if the actual theme // engine returns a template. @@ -425,17 +427,6 @@ protected function processExtension(&$registry, $type, $name, $theme_name, $them } if (isset($info['template'])) { - // If it was a function before, and the registry contains (pre)process - // functions, merge them into this hook and remove them from the existing registry, so that . - if (isset($registry[$hook]['function'])) { - foreach (array('preprocess', 'process') as $phase) { - if (isset($registry[$hook][$phase])) { - $info += array($phase => array()); - $info[$phase] = array_merge($registry[$hook][$phase], $info[$phase]); - $registry[$hook][$phase] = array(); - } - } - } // A template implementation always takes precedence over functions. // A potentially existing function pointer is obsolete. unset($registry[$hook]['function']); @@ -449,7 +440,7 @@ protected function processExtension(&$registry, $type, $name, $theme_name, $them } } - // Inject preprocess functions in their required order. + // Record (pre)process functions by extension type. // The override logic here is essential: // - The first time a 'template' is defined by any extension, default // template (pre)processor functions need to be injected. @@ -461,7 +452,6 @@ protected function processExtension(&$registry, $type, $name, $theme_name, $them // - Followed by the global hook_(pre)process() functions that apply to // all templates, which need to be collated from all modules, all // engines, and all themes (in this order). - // @todo Speaking of "all themes", base themes are missing in the below. // - And lastly, any other (pre)process functions that have been declared // in hook_theme(). // Furthermore: @@ -469,66 +459,71 @@ protected function processExtension(&$registry, $type, $name, $theme_name, $them // run for theme *functions*, not only templates. All other template/ // default processors are omitted, unless explicitly declared in // hook_theme(). (performance). - // - If a "later" extension type in the build process replaces a theme + // - If a later extension type in the build process replaces a theme // function with a theme template by declaring 'template' (e.g., a theme // wants to use a template instead of a function), then all of the // default processors need to be injected (in the order described above). - // To achieve the required ordering, the build process leverages - // NestedArray::mergeDeep() and all functions that need to be sorted - // specially are added with a string key, and all others are using integer - // keys. Additionally, upon first injection, the special functions are - // prepended to the stack. Due to this, the build process is able to - // re-merge all defined functions again for each extension. + // - All recorded data of previous processing steps is expected to be + // available to hook_theme() implementations, which means that these + // operations cannot happen in Registry::compile(). + // To achieve the required ordering, the build process records all + // registered and automatically determined (pre)process functions keyed by + // extension type. This allows the final compile pass in + // Registry::compile() to sort the final list of functions in their + // required order. + // Additionally, all functions are added with a string key, so they do not + // get duplicated when merging the info of the current extension into the + // existing registry info. // @see theme() + $has_template = isset($registry[$hook]['template']) || isset($info['template']); foreach (array('preprocess', 'process') as $phase) { - if (isset($info[$phase]) || isset($registry[$hook]['template']) || isset($info['template'])) { - $info += array($phase => array()); - if (!empty($info[$phase])) { + if (isset($info[$phase]) || $has_template) { + if (isset($info[$phase])) { $info[$phase] = array_combine($info[$phase], $info[$phase]); } + else { + $info[$phase] = array(); + } $functions = array(); - // 1) The base template_(pre)process(). - // Not included for functions. Injected on the first occurrence of a - // 'template' property. - // @see theme() - if (isset($info['template'])) { - if (function_exists("template_{$phase}")) { - $functions["template_{$phase}"] = "template_{$phase}"; + // 1) The base template_(pre)process(). Only for templates. + if ($has_template) { + $template_function = "template_{$phase}"; + if (function_exists($template_function)) { + $functions['template'][$template_function] = $template_function; } } - // 2) The original hook owners' template_(pre)process_HOOK(), if - // registered in $info, and remove it, as the merge would duplicate it - // otherwise. - // @todo Consider to change this to a generic 'template_' prefix, - // allowing all extensions to inject pre-theme-hook-processors. - if (in_array("template_{$phase}_{$hook}", $info[$phase])) { - $functions["template_{$phase}_{$hook}"] = "template_{$phase}_{$hook}"; - $info[$phase] = array_diff($info[$phase], array("template_{$phase}_{$hook}")); + // 2) template_(pre)process_HOOK(), if registered in $info. + $template_function = "template_{$phase}_{$hook}"; + if (isset($info[$phase][$template_function])) { + $functions['template_hook'][$template_function] = $template_function; + unset($info[$phase][$template_function]); } - // 3) Global hook_(pre)process() of all extensions. Not for functions. - // @see theme() - if (isset($info['template'])) { - // Module hook implementations need to be retrieved regardless of - // the current extension type being processed, since themes might - // declare templates, in which case module extensions have been - // processed already. - $functions += $this->getHookImplementations($phase); + // 3) hook_(pre)process() of all modules. Only for templates. + // Since modules are processed before themes, but themes can declare + // templates, module hook implementations need to be added whenever a + // template is added. + if ($has_template) { + $functions['module'] = $this->getHookImplementations($phase); } - // 4) And the reverse of the above; when template hooks of modules are - // processed for later extension types, the extension's $info normally - // does not override 'template', but yet, we need to add - // hook_(pre)process() functions. - if ($type != 'module' && (isset($registry[$hook]['template']) || isset($info['template']))) { + // 4) hook_(pre)process() of theme engines and themes. + // Template hooks of modules are processed in later steps, so we need + // to add hook_(pre)process() functions. + if ($type != 'module' && $has_template) { $function = $name . '_' . $phase; - // @todo The theme registry build should not rely on loaded code. - // Introduce theme_implements(). if (function_exists($function)) { - $functions[$function] = $function; + $functions[$type][$function] = $function; } } - // 5) Append the declared hook_(pre)process_HOOK() functions. They - // are merged in the vertical processing order of this build mechanism. - $info[$phase] = array_merge($functions, $info[$phase]); + // 5) hook_(pre)process_HOOK(), as declared in $info. + // Since template_(pre)process_HOOK() was removed above, check whether + // any functions are left first. + if (!empty($info[$phase])) { + $key = $type . '_hook'; + $functions[$key] = $info[$phase]; + } + + // Replace the list functions (they are keyed by extension type now). + $info[$phase] = $functions; } } @@ -546,7 +541,15 @@ protected function processExtension(&$registry, $type, $name, $theme_name, $them } } - protected function postProcess() { + /** + * Compiles the theme registry. + * + * Compilation involves these steps: + * - Theme hook suggestions are mapped and resolved to base hooks. + * - (Pre)process functions are sorted. + * - Unnecessary data is removed. + */ + protected function compile() { // Merge base hooks into suggestions and remove unnecessary hooks. foreach ($this->registry as $hook => &$info) { if (empty($info['exists'])) { @@ -565,16 +568,41 @@ protected function postProcess() { } } - // Clean up unnecessary data. + // Compile (pre)process functions and clean up unnecessary data. + $preprocessor_phases = array( + 'template', + 'template_hook', + 'module', + 'module_hook', + 'base_theme_engine', + 'base_theme_engine_hook', + 'theme_engine', + 'theme_engine_hook', + 'base_theme', + 'base_theme_hook', + 'theme', + 'theme_hook', + ); foreach ($this->registry as $hook => &$info) { if (isset($info['exists'])) { unset($info['exists']); } - if (isset($info['preprocess'])) { - $info['preprocess'] = array_values($info['preprocess']); - } - if (isset($info['process'])) { - $info['process'] = array_values($info['process']); + // (Pre)process functions have been collected separately by extension type + // during the build process. Due to the required final merging of base + // hooks and hook suggestions, as well as the possibility of functions + // getting replaced with templates by themes, the final set of functions + // needs to be determined and compiled now. + foreach (array('preprocess', 'process') as $phase) { + if (isset($info[$phase])) { + $functions = array(); + foreach ($preprocessor_phases as $type) { + if (isset($info[$phase][$type])) { + $functions += $info[$phase][$type]; + } + } + // Remove the unnecessary array keys to decrease the array size. + $info[$phase] = array_values($functions); + } } } @@ -632,9 +660,10 @@ protected function getHookImplementations($hook) { * To be called when the list of enabled extensions is changed. */ public function reset() { - unset($this->runtimeRegistry); - unset($this->registry); + $this->runtimeRegistry = NULL; + $this->registry = NULL; $this->cache->invalidateTags(array('theme_registry' => TRUE)); + return $this; } } diff --git a/core/modules/system/lib/Drupal/system/Tests/Theme/RegistryUnitTest.php b/core/modules/system/lib/Drupal/system/Tests/Theme/RegistryUnitTest.php index 91c7000..0fd25f6 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Theme/RegistryUnitTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Theme/RegistryUnitTest.php @@ -34,6 +34,7 @@ public static function getInfo() { function setUp() { parent::setUp(); + $this->registries = array(); $this->installSchema('system', 'variable');