diff --git a/core/lib/Drupal/Core/Theme/Registry.php b/core/lib/Drupal/Core/Theme/Registry.php index 5e450ab..656f647 100644 --- a/core/lib/Drupal/Core/Theme/Registry.php +++ b/core/lib/Drupal/Core/Theme/Registry.php @@ -343,6 +343,8 @@ protected function build() { // Finally, hooks provided by the theme itself. $this->processExtension($cache, $this->theme->getName(), 'theme', $this->theme->getName(), $this->theme->getPath()); + $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); @@ -535,30 +537,105 @@ protected function processExtension(array &$cache, $name, $type, $theme, $path) // 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; + /** + * This completes the theme registry adding missing functions and hooks. + + * @param array $cache + * The theme registry that will eventually be cached; It is an associative + * array keyed by theme hooks, whose values are associative arrays + * describing the hook: + * - 'type': The passed-in $type. + * - 'theme path': The passed-in $path. + * - 'function': The name of the function generating output for this theme + * hook. Either defined explicitly in hook_theme() or, if neither + * 'function' nor 'template' is defined, then the default theme function + * name is used. The default theme function name is the theme hook + * prefixed by either 'theme_' for modules or '$name_' for everything + * else. If 'function' is defined, 'template' is not used. + * - 'template': The filename of the template generating output for this + * theme hook. The template is in the directory defined by the 'path' key + * of hook_theme() or defaults to "$path/templates". + * - 'variables': The variables for this theme hook as defined in + * hook_theme(). If there is more than one implementation and 'variables' + * is not specified in a later one, then the previous definition is kept. + * - 'render element': The renderable element for this theme hook as defined + * in hook_theme(). If there is more than one implementation and + * 'render element' is not specified in a later one, then the previous + * definition is kept. + * - 'preprocess functions': See _theme() for detailed documentation. + * @param object $theme + * current active theme. + */ + protected function postProcessExtension(array &$cache, $theme) { + + // 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 = array_keys((array) $this->moduleHandler->getModuleList()); + if ($theme->getEngine()) { + $prefixes[] = $theme->getEngine() . '_engine'; + } + foreach ($theme->getBaseThemes() as $base) { + $prefixes[] = $base->getName(); + } + $prefixes[] = $theme->getName(); + + // 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 = []; + foreach ($prefixes as $prefix) { + $processors += preg_grep("/^{$prefix}_preprocess_($hooks)(__)?/", $user_func); + } + + // Add missing variable processors. This is needed for hooks that do not + // explictly 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, 'preprocess_') + strlen('preprocess_')); + 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']; } - // Ensure uniqueness. - $cache[$hook]['preprocess functions'] = array_unique($cache[$hook]['preprocess functions']); } } } + // 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']); + } + } } /** diff --git a/core/modules/system/src/Tests/Theme/RegistryTest.php b/core/modules/system/src/Tests/Theme/RegistryTest.php index 8872f10..f4481b2 100644 --- a/core/modules/system/src/Tests/Theme/RegistryTest.php +++ b/core/modules/system/src/Tests/Theme/RegistryTest.php @@ -66,42 +66,6 @@ function testRaceCondition() { } /** - * Tests the theme registry with multiple subthemes. - */ - public function testMultipleSubThemes() { - $theme_handler = \Drupal::service('theme_handler'); - $theme_handler->install(['test_basetheme', 'test_subtheme', 'test_subsubtheme']); - - $registry_subsub_theme = new Registry(\Drupal::root(), \Drupal::cache(), \Drupal::lock(), \Drupal::moduleHandler(), $theme_handler, \Drupal::service('theme.initialization'), 'test_subsubtheme'); - $registry_subsub_theme->setThemeManager(\Drupal::theme()); - $registry_sub_theme = new Registry(\Drupal::root(), \Drupal::cache(), \Drupal::lock(), \Drupal::moduleHandler(), $theme_handler, \Drupal::service('theme.initialization'), 'test_subtheme'); - $registry_sub_theme->setThemeManager(\Drupal::theme()); - $registry_base_theme = new Registry(\Drupal::root(), \Drupal::cache(), \Drupal::lock(), \Drupal::moduleHandler(), $theme_handler, \Drupal::service('theme.initialization'), 'test_basetheme'); - $registry_base_theme->setThemeManager(\Drupal::theme()); - - $preprocess_functions = $registry_subsub_theme->get()['theme_test_template_test']['preprocess functions']; - $this->assertIdentical([ - 'template_preprocess', - 'test_basetheme_preprocess_theme_test_template_test', - 'test_subtheme_preprocess_theme_test_template_test', - 'test_subsubtheme_preprocess_theme_test_template_test', - ], $preprocess_functions); - - $preprocess_functions = $registry_sub_theme->get()['theme_test_template_test']['preprocess functions']; - $this->assertIdentical([ - 'template_preprocess', - 'test_basetheme_preprocess_theme_test_template_test', - 'test_subtheme_preprocess_theme_test_template_test', - ], $preprocess_functions); - - $preprocess_functions = $registry_base_theme->get()['theme_test_template_test']['preprocess functions']; - $this->assertIdentical([ - 'template_preprocess', - 'test_basetheme_preprocess_theme_test_template_test', - ], $preprocess_functions); - } - - /** * Tests that the theme registry can be altered by themes. */ public function testThemeRegistryAlterByTheme() { diff --git a/core/modules/system/src/Tests/Theme/RegistryTest.php.orig b/core/modules/system/src/Tests/Theme/RegistryTest.php.orig new file mode 100644 index 0000000..8872f10 --- /dev/null +++ b/core/modules/system/src/Tests/Theme/RegistryTest.php.orig @@ -0,0 +1,119 @@ +setMethod('GET'); + $cid = 'test_theme_registry'; + + // Directly instantiate the theme registry, this will cause a base cache + // entry to be written in __construct(). + $cache = \Drupal::cache(); + $lock_backend = \Drupal::lock(); + $registry = new ThemeRegistry($cid, $cache, $lock_backend, array('theme_registry'), $this->container->get('module_handler')->isLoaded()); + + $this->assertTrue(\Drupal::cache()->get($cid), 'Cache entry was created.'); + + // Trigger a cache miss for an offset. + $this->assertTrue($registry->get('theme_test_template_test'), 'Offset was returned correctly from the theme registry.'); + // This will cause the ThemeRegistry class to write an updated version of + // the cache entry when it is destroyed, usually at the end of the request. + // Before that happens, manually delete the cache entry we created earlier + // so that the new entry is written from scratch. + \Drupal::cache()->delete($cid); + + // Destroy the class so that it triggers a cache write for the offset. + $registry->destruct(); + + $this->assertTrue(\Drupal::cache()->get($cid), 'Cache entry was created.'); + + // Create a new instance of the class. Confirm that both the offset + // requested previously, and one that has not yet been requested are both + // available. + $registry = new ThemeRegistry($cid, $cache, $lock_backend, array('theme_registry'), $this->container->get('module_handler')->isLoaded()); + $this->assertTrue($registry->get('theme_test_template_test'), 'Offset was returned correctly from the theme registry'); + $this->assertTrue($registry->get('theme_test_template_test_2'), 'Offset was returned correctly from the theme registry'); + } + + /** + * Tests the theme registry with multiple subthemes. + */ + public function testMultipleSubThemes() { + $theme_handler = \Drupal::service('theme_handler'); + $theme_handler->install(['test_basetheme', 'test_subtheme', 'test_subsubtheme']); + + $registry_subsub_theme = new Registry(\Drupal::root(), \Drupal::cache(), \Drupal::lock(), \Drupal::moduleHandler(), $theme_handler, \Drupal::service('theme.initialization'), 'test_subsubtheme'); + $registry_subsub_theme->setThemeManager(\Drupal::theme()); + $registry_sub_theme = new Registry(\Drupal::root(), \Drupal::cache(), \Drupal::lock(), \Drupal::moduleHandler(), $theme_handler, \Drupal::service('theme.initialization'), 'test_subtheme'); + $registry_sub_theme->setThemeManager(\Drupal::theme()); + $registry_base_theme = new Registry(\Drupal::root(), \Drupal::cache(), \Drupal::lock(), \Drupal::moduleHandler(), $theme_handler, \Drupal::service('theme.initialization'), 'test_basetheme'); + $registry_base_theme->setThemeManager(\Drupal::theme()); + + $preprocess_functions = $registry_subsub_theme->get()['theme_test_template_test']['preprocess functions']; + $this->assertIdentical([ + 'template_preprocess', + 'test_basetheme_preprocess_theme_test_template_test', + 'test_subtheme_preprocess_theme_test_template_test', + 'test_subsubtheme_preprocess_theme_test_template_test', + ], $preprocess_functions); + + $preprocess_functions = $registry_sub_theme->get()['theme_test_template_test']['preprocess functions']; + $this->assertIdentical([ + 'template_preprocess', + 'test_basetheme_preprocess_theme_test_template_test', + 'test_subtheme_preprocess_theme_test_template_test', + ], $preprocess_functions); + + $preprocess_functions = $registry_base_theme->get()['theme_test_template_test']['preprocess functions']; + $this->assertIdentical([ + 'template_preprocess', + 'test_basetheme_preprocess_theme_test_template_test', + ], $preprocess_functions); + } + + /** + * Tests that the theme registry can be altered by themes. + */ + public function testThemeRegistryAlterByTheme() { + + /** @var \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler */ + $theme_handler = \Drupal::service('theme_handler'); + $theme_handler->install(['test_theme']); + $theme_handler->setDefault('test_theme'); + + $registry = new Registry(\Drupal::root(), \Drupal::cache(), \Drupal::lock(), \Drupal::moduleHandler(), $theme_handler, \Drupal::service('theme.initialization'), 'test_theme'); + $registry->setThemeManager(\Drupal::theme()); + $this->assertEqual('value', $registry->get()['theme_test_template_test']['variables']['additional']); + } + +} 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..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 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().'; +} + diff --git a/core/modules/system/tests/themes/test_theme/test_theme.theme.orig b/core/modules/system/tests/themes/test_theme/test_theme.theme.orig new file mode 100644 index 0000000..9350755 --- /dev/null +++ b/core/modules/system/tests/themes/test_theme/test_theme.theme.orig @@ -0,0 +1,107 @@ +alter('theme_test_alter'). + */ +function test_theme_theme_test_alter_alter(&$data) { + $data = 'test_theme_theme_test_alter_alter was invoked'; +} + +/** + * Implements hook_theme_suggestions_alter(). + */ +function test_theme_theme_suggestions_alter(array &$suggestions, array $variables, $hook) { + drupal_set_message(__FUNCTION__ . '() executed.'); + // Theme alter hooks run after module alter hooks, so add this theme + // suggestion to the beginning of the array so that the suggestion added by + // the theme_suggestions_test module can be picked up when that module is + // enabled. + if ($hook == 'theme_test_general_suggestions') { + array_unshift($suggestions, 'theme_test_general_suggestions__' . 'theme_override'); + } +} + +/** + * Implements hook_theme_suggestions_HOOK_alter(). + */ +function test_theme_theme_suggestions_theme_test_suggestions_alter(array &$suggestions, array $variables) { + drupal_set_message(__FUNCTION__ . '() executed.'); + // Theme alter hooks run after module alter hooks, so add this theme + // suggestion to the beginning of the array so that the suggestion added by + // the theme_suggestions_test module can be picked up when that module is + // enabled. + array_unshift($suggestions, 'theme_test_suggestions__' . 'theme_override'); +} + +/** + * Implements hook_theme_suggestions_HOOK_alter(). + */ +function test_theme_theme_suggestions_theme_test_function_suggestions_alter(array &$suggestions, array $variables) { + // Theme alter hooks run after module alter hooks, so add this theme + // suggestion to the beginning of the array so that the suggestion added by + // the theme_suggestions_test module can be picked up when that module is + // enabled. + array_unshift($suggestions, 'theme_test_function_suggestions__' . 'theme_override'); +} + +/** + * Returns HTML for a theme function suggestion test. + * + * Implements the theme_test_function_suggestions__theme_override suggestion. + */ +function test_theme_theme_test_function_suggestions__theme_override($variables) { + return 'Theme function overridden based on new theme suggestion provided by the test_theme theme.'; +} + +/** + * Returns HTML for a theme function suggestion test. + * + * Implements the theme_test_function_suggestions__module_override suggestion. + */ +function test_theme_theme_test_function_suggestions__module_override($variables) { + return 'Theme function overridden based on new theme suggestion provided by a module.'; +} + +/** + * Implements hook_theme_registry_alter(). + */ +function test_theme_theme_registry_alter(&$registry) { + $registry['theme_test_template_test']['variables']['additional'] = 'value'; +} 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 }}