diff --git a/core/includes/theme.inc b/core/includes/theme.inc index 35b9cd3..27fd61b 100644 --- a/core/includes/theme.inc +++ b/core/includes/theme.inc @@ -465,6 +465,31 @@ function _theme_process_registry(&$cache, $name, $type, $theme, $path) { $function = $name . '_theme'; if (function_exists($function)) { $result = $function($cache, $type, $theme, $path); + + // Prepare prefixes for processor functions. + $prefixes = array(); + if ($type == 'module') { + // Default variable processor prefix. + $prefixes[] = 'template'; + // Add all modules so they can intervene with their own variable + // processors. This allows them to provide variable processors even if + // they are not the owner of the current hook. + $prefixes += module_list(); + } + elseif ($type == 'theme_engine' || $type == 'base_theme_engine') { + // Theme engines get an extra set that come before the normally + // named variable processors. + $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 + // processors. + $prefixes[] = $name; + } + 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 @@ -482,6 +507,7 @@ function _theme_process_registry(&$cache, $name, $type, $theme, $path) { $result[$hook]['function'] = ($type == 'module' ? 'theme_' : $name . '_') . $hook; } + // Collect previously set files for inclusion. if (isset($cache[$hook]['includes'])) { $result[$hook]['includes'] = $cache[$hook]['includes']; } @@ -489,7 +515,7 @@ function _theme_process_registry(&$cache, $name, $type, $theme, $path) { // 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. + // files. These files will be included once by theme() calls. if (isset($info['file'])) { $include_file = isset($info['path']) ? $info['path'] : $path; $include_file .= '/' . $info['file']; @@ -536,32 +562,21 @@ function _theme_process_registry(&$cache, $name, $type, $theme, $path) { // Allow variable processors for all theming hooks, whether the hook is // implemented as a template or as a function. foreach ($variable_process_phases as $phase_key => $phase) { - // Check for existing variable processors. Ensure arrayness. - if (!isset($info[$phase_key]) || !is_array($info[$phase_key])) { + // Check for existing variable processors set through hook_theme(). + if (!isset($info[$phase_key])) { $info[$phase_key] = array(); - $prefixes = array(); - if ($type == 'module') { - // Default variable processor prefix. - $prefixes[] = 'template'; - // Add all modules so they can intervene with their own variable - // processors. This allows them to provide variable processors even - // if they are not the owner of the current hook. - $prefixes += module_list(); + $current_prefixes = $prefixes; + if (isset($cache[$hook]['function']) && $cache[$hook]['type'] === 'module' && isset($info['template'])) { + // If the default implementation is a function, but a template + // overrides that default implementation, introduce the default + // template processors. It is assumed that when it is implemented as + // a theme function, it must run as quickly as possible so only the + // default varible processors are introduced. The ones provided by + // modules not directly related to this theming hook will be ignored. + $current_prefixes = array('template') + $current_prefixes; } - elseif ($type == 'theme_engine' || $type == 'base_theme_engine') { - // Theme engines get an extra set that come before the normally - // named variable processors. - $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 - // processors. - $prefixes[] = $name; - } - foreach ($prefixes as $prefix) { + + foreach ($current_prefixes as $prefix) { // Only use non-hook-specific variable processors for theming hooks // implemented as templates. See theme(). if (isset($info['template']) && function_exists($prefix . '_' . $phase)) { @@ -579,7 +594,7 @@ function _theme_process_registry(&$cache, $name, $type, $theme, $path) { // Flag not needed inside the registry. unset($result[$hook]['override ' . $phase_key]); } - elseif (isset($cache[$hook][$phase_key]) && is_array($cache[$hook][$phase_key])) { + elseif (isset($cache[$hook][$phase_key])) { $info[$phase_key] = array_merge($cache[$hook][$phase_key], $info[$phase_key]); } $result[$hook][$phase_key] = $info[$phase_key]; @@ -589,31 +604,90 @@ function _theme_process_registry(&$cache, $name, $type, $theme, $path) { // Merge the newly created theme hooks into the existing cache. $cache = $result + $cache; } +} - // Let themes have variable processors even if they didn't register a template. - if ($type == 'theme' || $type == 'base_theme') { - foreach ($cache as $hook => $info) { - // Check only if not registered by the theme or engine. - if (empty($result[$hook])) { - foreach ($variable_process_phases as $phase_key => $phase) { - if (!isset($info[$phase_key])) { - $cache[$hook][$phase_key] = array(); - } - // Only use non-hook-specific variable processors for theming hooks - // implemented as templates. See theme(). - if (isset($info['template']) && function_exists($name . '_' . $phase)) { - $cache[$hook][$phase_key][] = $name . '_' . $phase; - } - if (function_exists($name . '_' . $phase . '_' . $hook)) { - $cache[$hook][$phase_key][] = $name . '_' . $phase . '_' . $hook; - $cache[$hook]['theme path'] = $path; - } - // Ensure uniqueness. - $cache[$hook][$phase_key] = array_unique($cache[$hook][$phase_key]); +/** + * Completes the theme registry, adding missing functions and hooks. + */ +function _theme_post_process_registry(&$cache, $theme, $base_theme, $theme_engine) { + // Get all user defined functions. + list(, $user_func) = array_values(get_defined_functions()); + $user_func = array_combine($user_func, $user_func); + + // Gather prefixes. This will be used to limit the found functions to the + // expected naming conventions. + $prefixes = module_list(); + if ($theme_engine) { + $prefixes[] = $theme_engine . '_engine'; + } + foreach ($base_theme as $base) { + $prefixes[] = $base->name; + } + $prefixes[] = $theme->name; + + // Collect all known hooks. Discovered functions must be based on a known + // hook. + $hooks = implode('|', array_keys($cache)); + + // Collect all variable processor functions in the correct order. + $processors = array(); + foreach ($prefixes as $prefix) { + $processors += preg_grep("/^{$prefix}_(pre)?process_($hooks)(__)?/", $user_func); + } + + // Add missing variable processors. This is needed for hooks that do not + // explicitly register the hook. For example, when a theme contains a variable + // process 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. + foreach ($processors as $processor) { + $hook = substr($processor, strpos($processor, 'process_') + strlen('process_')); + $phase = strpos($processor, 'preprocess') ? 'preprocess functions' : 'process functions'; + + if (isset($cache[$hook][$phase]) && !in_array($processor, $cache[$hook][$phase])) { + // Add missing processor to existing hook. + $cache[$hook][$phase][] = $processor; + } + elseif (!isset($cache[$hook]) && strpos($hook, '__')) { + // Process non-existing hook and register it. + // Search for the base hook. + $base_hook = $hook; + while (!isset($cache[$base_hook]) && $pos = strrpos($base_hook, '__')) { + $base_hook = substr($base_hook, 0, $pos); + // If the current hook is based on a pattern, get the base hook. + if (isset($cache[$base_hook]['base hook'])) { + $base_hook = $cache[$base_hook]['base hook']; + } + } + if (isset($cache[$base_hook])) { + // Pull from the base hook and register the new + $cache[$hook] = $cache[$base_hook]; + if (isset($cache[$hook][$phase])) { + $cache[$hook][$phase][] = $processor; } } } } + + // Inherit all base hook variable processors into pattern hooks. This ensures + // that derivative hooks have a complete set of variable process functions. + foreach ($cache as $hook => $info) { + foreach (array('preprocess functions', 'process functions') as $phase) { + // 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['base hook']) && isset($cache[$info['base hook']][$phase])) { + $diff = array_diff($cache[$info['base hook']][$phase], $info[$phase]); + $cache[$hook][$phase] = array_merge($diff, $info[$phase]); + } + + // Optimize the registry. + if (isset($cache[$hook][$phase]) && empty($cache[$hook][$phase])) { + unset($cache[$hook][$phase]); + } + } + } } /** @@ -664,6 +738,9 @@ function _theme_build_registry($theme, $base_theme, $theme_engine) { // Finally, hooks provided by the theme itself. _theme_process_registry($cache, $theme->name, 'theme', $theme->name, dirname($theme->filename)); + // Finalize the build. + _theme_post_process_registry($cache, $theme, $base_theme, $theme_engine); + // Let modules alter the registry. drupal_alter('theme_registry', $cache); @@ -1010,16 +1087,35 @@ function theme($hook, $variables = array()) { } } + // Invoke the variable processors, if any. The processors may specify + // alternate suggestions for which hook's template/function to use. If the + // hook is a suggestion of a base hook, invoke the variable processors of + // the base hook, but retain the suggestion as a high priority suggestion to + // be used unless overridden by a variable processor function. + if (isset($info['base hook'])) { + $base_hook = $info['base hook']; + $base_hook_info = $hooks[$base_hook]; + // Include files required by the base hook, since its variable processors + // might reside there. + if (!empty($base_hook_info['includes'])) { + foreach ($base_hook_info['includes'] as $include_file) { + include_once DRUPAL_ROOT . '/' . $include_file; + } + if (isset($base_hook_info['preprocess functions']) || isset($base_hook_info['process functions'])) { + $variables['theme_hook_suggestion'] = $hook; + $hook = $base_hook; + $info = $base_hook_info; + } + } + } // If a renderable array is passed as $variables, then set $variables to // the arguments expected by the theme function. if (isset($variables['#theme']) || isset($variables['#theme_wrappers'])) { $element = $variables; $variables = array(); if (isset($info['variables'])) { - foreach (array_keys($info['variables']) as $name) { - if (isset($element["#$name"])) { - $variables[$name] = $element["#$name"]; - } + foreach ($info['variables'] as $name => $default) { + $variables[$name] = isset($element["#$name"]) ? $element["#$name"] : $default; } } else { @@ -1027,6 +1123,7 @@ function theme($hook, $variables = array()) { } } + // Direct theme() call. Not passed from a renderable array. // Merge in argument defaults. if (!empty($info['variables'])) { $variables += $info['variables']; @@ -1039,27 +1136,6 @@ function theme($hook, $variables = array()) { 'theme_hook_original' => $original_hook, ); - // Invoke the variable processors, if any. The processors may specify - // alternate suggestions for which hook's template/function to use. If the - // hook is a suggestion of a base hook, invoke the variable processors of - // the base hook, but retain the suggestion as a high priority suggestion to - // be used unless overridden by a variable processor function. - if (isset($info['base hook'])) { - $base_hook = $info['base hook']; - $base_hook_info = $hooks[$base_hook]; - // Include files required by the base hook, since its variable processors - // might reside there. - if (!empty($base_hook_info['includes'])) { - foreach ($base_hook_info['includes'] as $include_file) { - include_once DRUPAL_ROOT . '/' . $include_file; - } - } - if (isset($base_hook_info['preprocess functions']) || isset($base_hook_info['process functions'])) { - $variables['theme_hook_suggestion'] = $hook; - $hook = $base_hook; - $info = $base_hook_info; - } - } if (isset($info['preprocess functions']) || isset($info['process functions'])) { $variables['theme_hook_suggestions'] = array(); foreach (array('preprocess functions', 'process functions') as $phase) { @@ -1103,9 +1179,7 @@ function theme($hook, $variables = array()) { // Generate the output using either a function or a template. $output = ''; if (isset($info['function'])) { - if (function_exists($info['function'])) { - $output = $info['function']($variables); - } + $output = $info['function']($variables); } else { // Default render function and extension. @@ -1131,22 +1205,6 @@ function theme($hook, $variables = array()) { } } - // In some cases, a template implementation may not have had - // template_preprocess() run (for example, if the default implementation is - // a function, but a template overrides that default implementation). In - // these cases, a template should still be able to expect to have access to - // the variables provided by template_preprocess(), so we add them here if - // they don't already exist. We don't want to run template_preprocess() - // twice (it would be inefficient and mess up zebra striping), so we use the - // 'directory' variable to determine if it has already run, which while not - // completely intuitive, is reasonably safe, and allows us to save on the - // overhead of adding some new variable to track that. - if (!isset($variables['directory'])) { - $default_template_variables = array(); - template_preprocess($default_template_variables, $hook); - $variables += $default_template_variables; - } - // Render the output using the template file. $template_file = $info['template'] . $extension; if (isset($info['path'])) { diff --git a/core/modules/system/lib/Drupal/system/Tests/Theme/ThemeTest.php b/core/modules/system/lib/Drupal/system/Tests/Theme/ThemeTest.php index 5267300..fd7542a 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Theme/ThemeTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Theme/ThemeTest.php @@ -79,6 +79,22 @@ function testPreprocessForSuggestions() { } /** + * Ensures suggestion preprocess functions run even for default implementations. + * + * The theme hook used by this test has its base preprocess function in a + * separate file, so this test also ensures that that file is correctly loaded + * when needed. + */ + function testSuggestionPreprocessForDefaults() { + // Test with both an unprimed and primed theme registry. + drupal_theme_rebuild(); + for ($i = 0; $i < 2; $i++) { + $this->drupalGet('theme-test/preprocess-suggestion'); + $this->assertText('Theme hook implementor=test_theme_theme_test_preprocess__suggestion(). Foo=template_preprocess_theme_test_preprocess', 'Theme hook ran with data available from a preprocess function for the suggested hook.'); + } + } + + /** * Ensure page-front template suggestion is added when on front page. */ function testFrontPageThemeSuggestion() { diff --git a/core/modules/system/tests/modules/theme_test/theme_test.inc b/core/modules/system/tests/modules/theme_test/theme_test.inc index 6cde683..e43df50 100644 --- a/core/modules/system/tests/modules/theme_test/theme_test.inc +++ b/core/modules/system/tests/modules/theme_test/theme_test.inc @@ -13,3 +13,22 @@ function theme_theme_test($variables) { function template_preprocess_theme_test(&$variables) { $variables['foo'] = 'template_preprocess_theme_test'; } + +/** + * Returns HTML for the 'theme_test_preprocess' theme hook used by tests. + */ +function theme_theme_test_preprocess($variables) { + return 'Theme hook implementor=theme_theme_test_preprocess(). Foo=' . $variables['foo']; +} + +/** + * Implements hook_preprocess_HOOK() for theme_theme_test_preprocess(). + * + * Despite not having a corresponding theme function for this suggestion, the + * specific preprocess function should still be used. + */ +function template_preprocess_theme_test_preprocess(&$variables) { + $variables['foo'] = 'template_preprocess_theme_test_preprocess'; +} + + 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 73ff44b..d355dee 100644 --- a/core/modules/system/tests/modules/theme_test/theme_test.module +++ b/core/modules/system/tests/modules/theme_test/theme_test.module @@ -8,6 +8,10 @@ function theme_test_theme($existing, $type, $theme, $path) { 'file' => 'theme_test.inc', 'variables' => array('foo' => ''), ); + $items['theme_test_preprocess'] = array( + 'file' => 'theme_test.inc', + 'variables' => array('foo' => ''), + ); $items['theme_test_template_test'] = array( 'template' => 'theme_test.template_test', ); @@ -42,6 +46,13 @@ function theme_test_menu() { 'theme callback' => '_theme_custom_theme', 'type' => MENU_CALLBACK, ); + $items['theme-test/preprocess-suggestion'] = array( + 'title' => 'Preprocess suggestion', + 'page callback' => '_theme_test_preprocess_suggestion', + 'access callback' => TRUE, + 'theme callback' => '_theme_custom_theme', + 'type' => MENU_CALLBACK, + ); $items['theme-test/alter'] = array( 'title' => 'Suggestion', 'page callback' => '_theme_test_alter', @@ -158,6 +169,13 @@ function _theme_test_suggestion() { } /** + * Page callback, calls a theme hook suggestion. + */ +function _theme_test_preprocess_suggestion() { + return theme(array('theme_test_preprocess__suggestion', 'theme_test_preprocess'), array()); +} + +/** * Implements hook_preprocess_HOOK() for html.tpl.php. */ function theme_test_preprocess_html(&$variables) { diff --git a/core/modules/system/tests/themes/test_theme/template.php b/core/modules/system/tests/themes/test_theme/template.php index 8275818..4d2e947 100644 --- a/core/modules/system/tests/themes/test_theme/template.php +++ b/core/modules/system/tests/themes/test_theme/template.php @@ -8,6 +8,13 @@ function test_theme_theme_test__suggestion($variables) { } /** + * Tests a theme overriding a default hook with a suggestion. + */ +function test_theme_theme_test_preprocess__suggestion($variables) { + return 'Theme hook implementor=test_theme_theme_test_preprocess__suggestion(). Foo=' . $variables['foo']; +} + +/** * Tests a theme implementing an alter hook. * * The confusing function name here is due to this being an implementation of