diff --git a/core/lib/Drupal/Core/Theme/Registry.php b/core/lib/Drupal/Core/Theme/Registry.php index 5e450ab..38ab0b7 100644 --- a/core/lib/Drupal/Core/Theme/Registry.php +++ b/core/lib/Drupal/Core/Theme/Registry.php @@ -340,9 +340,12 @@ protected function build() { $this->processExtension($cache, $this->theme->getEngine(), 'theme_engine', $this->theme->getName(), $this->theme->getPath()); } - // Finally, hooks provided by the theme itself. + // 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); @@ -554,14 +557,106 @@ protected function processExtension(array &$cache, $name, $type, $theme, $path) $cache[$hook]['preprocess functions'][] = $name . '_preprocess_' . $hook; $cache[$hook]['theme path'] = $path; } - // Ensure uniqueness. - $cache[$hook]['preprocess functions'] = array_unique($cache[$hook]['preprocess functions']); } } } } /** + * This completes the theme registry adding discovered functions and hooks. + * + * @param array $cache + * The theme registry. + * @param \Drupal\Core\Theme\ActiveTheme $theme + * Current active theme. + * + * @see ::processExtension() + */ + protected function postProcessExtension(array &$cache, ActiveTheme $theme) { + $grouped_functions = drupal_group_functions_by_prefix(); + + // 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 processor functions in the correct order. + $processors = []; + $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 processors 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]])) { + $processors[$candidate] = $matches[1]; + } + } + } + } + + // 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) { + if (isset($cache[$hook]['preprocess functions']) && !in_array($hook, $cache[$hook]['preprocess functions'])) { + // Add missing processor to existing hook. + $cache[$hook]['preprocess functions'][] = $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); + $cache[$base_hook]['preprocess functions'][] = $processor; + // 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']; + } + } + } + } + // 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) { + // 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']]['preprocess functions'])) { + $diff = array_diff($cache[$info['base hook']]['preprocess functions'], $info['preprocess functions']); + $cache[$hook]['preprocess functions'] = array_merge($diff, $info['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']); + } + } + } + + /** * Invalidates theme registry caches. * * To be called when the list of enabled extensions is changed. diff --git a/core/modules/system/src/Tests/Theme/ThemeTest.php b/core/modules/system/src/Tests/Theme/ThemeTest.php index e62a199..1655c69 100644 --- a/core/modules/system/src/Tests/Theme/ThemeTest.php +++ b/core/modules/system/src/Tests/Theme/ThemeTest.php @@ -287,4 +287,23 @@ 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() { + $this->config('system.theme') + ->set('default', 'test_theme') + ->save(); + // 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_preprocess_theme_test_preprocess_suggestions__suggestion().', '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 b/core/modules/system/tests/modules/theme_test/src/ThemeTestController.php index a8f16c6..8ae644a 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..7eeee9d 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,20 @@ 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'); +} + +/** + * Implements hook_preprocess_HOOK(). + */ +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 9350755..7a0fced 100644 --- a/core/modules/system/tests/themes/test_theme/test_theme.theme +++ b/core/modules/system/tests/themes/test_theme/test_theme.theme @@ -105,3 +105,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().'; +} +