diff --git a/core/lib/Drupal/Core/Render/Renderer.php b/core/lib/Drupal/Core/Render/Renderer.php index a7f7346..0b744b6 100644 --- a/core/lib/Drupal/Core/Render/Renderer.php +++ b/core/lib/Drupal/Core/Render/Renderer.php @@ -154,6 +154,17 @@ protected function doRender(&$elements, $is_root_call = FALSE) { return ''; } + if (isset($elements['#render_children']) && $elements['#theme'] != 'page') { + $children_keys = Element::children($elements); + $output = ''; + foreach ($children_keys as $key) { + if (!empty($elements[$key])) { + $output .= $this->doRender($elements[$key]); + } + } + return $output; + } + // Do not print elements twice. if (!empty($elements['#printed'])) { return ''; @@ -268,10 +279,8 @@ protected function doRender(&$elements, $is_root_call = FALSE) { } // Call the element's #theme function if it is set. Then any children of the - // element have to be rendered there. If the internal #render_children - // property is set, do not call the #theme function to prevent infinite - // recursion. - if ($theme_is_implemented && !isset($elements['#render_children'])) { + // element have to be rendered there. + if ($theme_is_implemented) { $elements['#children'] = $this->theme->render($elements['#theme'], $elements); // If ThemeManagerInterface::render() returns FALSE this means that the diff --git a/core/lib/Drupal/Core/Theme/Registry.php b/core/lib/Drupal/Core/Theme/Registry.php index 5e450ab..72ef9ac 100644 --- a/core/lib/Drupal/Core/Theme/Registry.php +++ b/core/lib/Drupal/Core/Theme/Registry.php @@ -347,13 +347,6 @@ protected function build() { $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 = $cache; return $this->registry; @@ -482,83 +475,19 @@ protected function processExtension(array &$cache, $name, $type, $theme, $path) $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'] = array(); - $prefixes = array(); - 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 _theme(). - 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. + // @todo 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']; } // Merge the newly created theme hooks into the existing cache. $cache = $result + $cache; } - - // Let themes have variable preprocessors 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])) { - if (!isset($info['preprocess functions'])) { - $cache[$hook]['preprocess functions'] = array(); - } - // Only use non-hook-specific variable preprocessors for theme hooks - // implemented as templates. See _theme(). - if (isset($info['template']) && function_exists($name . '_preprocess')) { - $cache[$hook]['preprocess functions'][] = $name . '_preprocess'; - } - if (function_exists($name . '_preprocess_' . $hook)) { - $cache[$hook]['preprocess functions'][] = $name . '_preprocess_' . $hook; - $cache[$hook]['theme path'] = $path; - } - // Ensure uniqueness. - $cache[$hook]['preprocess functions'] = array_unique($cache[$hook]['preprocess functions']); - } - } - } } /** diff --git a/core/lib/Drupal/Core/Theme/ThemeManager.php b/core/lib/Drupal/Core/Theme/ThemeManager.php index 6850fcb..45ee78a 100644 --- a/core/lib/Drupal/Core/Theme/ThemeManager.php +++ b/core/lib/Drupal/Core/Theme/ThemeManager.php @@ -286,16 +286,12 @@ protected function theme($hook, $variables = array()) { include_once $this->root . '/' . $include_file; } } - // Replace the preprocess functions with those from the base hook. - if (isset($base_hook_info['preprocess functions'])) { - // Set a variable for the 'theme_hook_suggestion'. This is used to - // maintain backwards compatibility with template engines. - $theme_hook_suggestion = $hook; - $info['preprocess functions'] = $base_hook_info['preprocess functions']; - } + $theme_hook_suggestion = $hook; } - if (isset($info['preprocess functions'])) { - foreach ($info['preprocess functions'] as $preprocessor_function) { + + $preprocess_functions = $this->buildPreprocess($info, $hook, $suggestions); + if (!empty($preprocess_functions)) { + foreach ($preprocess_functions as $preprocessor_function) { if (function_exists($preprocessor_function)) { $preprocessor_function($variables, $hook, $info); } @@ -389,6 +385,69 @@ protected function theme($hook, $variables = array()) { } /** + * Create a list of preprocess functions to be run for a single theme output. + * + * @param $info + * @param $hook + * @param $suggesitons + * + */ + protected function buildPreprocess($info, $hook, $suggestions) { + $module_list = array_keys((array) $this->moduleHandler->getModuleList()); + $theme = $this->getActiveTheme(); + + // Add base hook and the hook that is being called to suggestions. + $suggestions[] = $hook; + if (isset($info['base hook'])) { + $suggestions[] = $info['base hook']; + } + + $prefixes = []; + // 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); + + foreach (array_reverse($theme->getBaseThemes()) as $base) { + if ($base->getEngine()) { + $prefixes[] = $base->getEngine() . '_engine'; + } + $prefixes[] = $base->getName(); + } + + if ($theme->getEngine()) { + $prefixes[] = $theme->getEngine() . '_engine'; + $prefixes[] = $theme->getName(); + } + + foreach (array_reverse($theme->getBaseThemes()) as $base) { + // This applies when the theme manually registers their own variable + // preprocessors. + $prefixes[] = $base->getName(); + } + + $prefixes[] = $theme->getName(); + + $preprocess_functions = []; + foreach (array_reverse($suggestions) as $suggestion) { + foreach ($prefixes as $prefix) { + // Only use non-hook-specific variable preprocessors for theming + // hooks implemented as templates. See _theme(). + if (isset($info['template']) && function_exists($prefix . '_preprocess')) { + $preprocess_functions[] = $prefix . '_preprocess'; + } + if (function_exists($prefix . '_preprocess_' . $suggestion)) { + $preprocess_functions[] = $prefix . '_preprocess_' . $suggestion; + } + } + } + + return array_unique($preprocess_functions); + } + + /** * Initializes the active theme for a given route match. * * @param \Drupal\Core\Routing\RouteMatchInterface $route_match diff --git a/core/modules/system/src/Tests/Theme/ThemeTest.php b/core/modules/system/src/Tests/Theme/ThemeTest.php index 0cd4e4e..bb6c822 100644 --- a/core/modules/system/src/Tests/Theme/ThemeTest.php +++ b/core/modules/system/src/Tests/Theme/ThemeTest.php @@ -287,4 +287,19 @@ function testRegionClass() { $this->assertEqual(count($elements), 1, 'New class found.'); } + /** + * 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-suggestions'); + $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.'); + } + } } diff --git a/core/modules/system/tests/modules/theme_test/src/.ThemeTestController.php.swp b/core/modules/system/tests/modules/theme_test/src/.ThemeTestController.php.swp new file mode 100644 index 0000000..e69de29 diff --git a/core/modules/system/tests/modules/theme_test/src/ThemeTestController.php b/core/modules/system/tests/modules/theme_test/src/ThemeTestController.php index 335ed83..c82b8ec 100644 --- a/core/modules/system/tests/modules/theme_test/src/ThemeTestController.php +++ b/core/modules/system/tests/modules/theme_test/src/ThemeTestController.php @@ -143,4 +143,12 @@ public function nonHtml() { return new JsonResponse(['theme_initialized' => $theme_initialized]); } + /** + * Menu callback for testing preprocess functions are being run for theme + * suggestions. + */ + function preprocessSuggestions() { + return array('#theme' => 'theme_test_preprocess_suggestions__suggestion'); + } + } diff --git a/core/modules/system/tests/modules/theme_test/templates/theme-test-preprocess-suggestions.html.twig b/core/modules/system/tests/modules/theme_test/templates/theme-test-preprocess-suggestions.html.twig new file mode 100644 index 0000000..db8a8a4 --- /dev/null +++ b/core/modules/system/tests/modules/theme_test/templates/theme-test-preprocess-suggestions.html.twig @@ -0,0 +1 @@ +{{ foo }} 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 7ba5c2d..64d45e7 100644 --- a/core/modules/system/tests/modules/theme_test/theme_test.module +++ b/core/modules/system/tests/modules/theme_test/theme_test.module @@ -55,6 +55,9 @@ function theme_test_theme($existing, $type, $theme, $path) { $info['test_theme_not_existing_function'] = array( 'function' => 'test_theme_not_existing_function', ); + $items['theme_test_preprocess_suggestions'] = array( + 'variables' => array('foo' => ''), + ); return $items; } @@ -90,6 +93,17 @@ function theme_theme_test_function_template_override($variables) { } /** + * Implements hook_theme_suggestions_HOOK(). + */ +function theme_test_theme_suggestions_theme_test_preprocess_suggestions($variables) { + return array('theme_test_preprocess_suggestions__' . 'suggestion'); +} + +function theme_test_preprocess_theme_test_preprocess_suggestions(&$variables) { + $variables['foo'] = 'Theme hook implementor=theme_theme_test_preprocess_suggestions().'; +} + +/** * Prepares variables for test render element templates. * * Default template: theme-test-render-element.html.twig. diff --git a/core/modules/system/tests/modules/theme_test/theme_test.routing.yml b/core/modules/system/tests/modules/theme_test/theme_test.routing.yml index 2dbd188..1ff61cf 100644 --- a/core/modules/system/tests/modules/theme_test/theme_test.routing.yml +++ b/core/modules/system/tests/modules/theme_test/theme_test.routing.yml @@ -103,3 +103,10 @@ theme_test.non_html: _controller: '\Drupal\theme_test\ThemeTestController::nonHtml' requirements: _access: 'TRUE' + +theme_test.preprocess_suggestions: + path: '/theme-test/preprocess-suggestions' + defaults: + _controller: '\Drupal\theme_test\ThemeTestController::preprocessSuggestions' + requirements: + _access: 'TRUE' diff --git a/core/modules/system/tests/themes/test_theme/test_theme.theme b/core/modules/system/tests/themes/test_theme/test_theme.theme index 951fd58..541f0bf 100644 --- a/core/modules/system/tests/themes/test_theme/test_theme.theme +++ b/core/modules/system/tests/themes/test_theme/test_theme.theme @@ -101,3 +101,18 @@ function test_theme_theme_test_function_suggestions__module_override($variables) function test_theme_theme_registry_alter(&$registry) { $registry['theme_test_template_test']['variables']['additional'] = 'value'; } + +/** + * Tests a theme overriding a default hook with a suggestion. + */ +function test_theme_preprocess_theme_test_preprocess_suggestions(&$variables) { + $variables['foo'] = 'Theme hook implementor=test_theme_preprocess_theme_test_preprocess_suggestions().'; +} + +/** + * Tests a theme overriding a default hook with a suggestion. + */ +function test_theme_preprocess_theme_test_preprocess_suggestions__suggestion(&$variables) { + $variables['foo'] = 'Theme hook implementor=test_theme_preprocess_theme_test_preprocess_suggestions__suggestion().'; +} + diff --git a/core/themes/bartik/templates/theme-test-preprocess-suggestions--suggestion.html.twig b/core/themes/bartik/templates/theme-test-preprocess-suggestions--suggestion.html.twig new file mode 100644 index 0000000..db8a8a4 --- /dev/null +++ b/core/themes/bartik/templates/theme-test-preprocess-suggestions--suggestion.html.twig @@ -0,0 +1 @@ +{{ foo }}