diff --git a/core/lib/Drupal/Core/Theme/Registry.php b/core/lib/Drupal/Core/Theme/Registry.php index 6fd9ce5..0c1c25e 100644 --- a/core/lib/Drupal/Core/Theme/Registry.php +++ b/core/lib/Drupal/Core/Theme/Registry.php @@ -605,54 +605,59 @@ protected function processExtension(array &$cache, $name, $type, $theme, $path) */ protected function completeSuggestion($hook, array &$cache) { $previous_hook = $hook; - $incomplete_previous_hook = array(); - while ((!isset($cache[$previous_hook]) || isset($cache[$previous_hook]['incomplete preprocess functions'])) - && $pos = strrpos($previous_hook, '__')) { - if (isset($cache[$previous_hook]) && !$incomplete_previous_hook && isset($cache[$previous_hook]['incomplete preprocess functions'])) { - $incomplete_previous_hook = $cache[$previous_hook]; - unset($incomplete_previous_hook['incomplete preprocess functions']); + $first_valid_hook = ''; + // Continue looping if the candidate hook doesn't exist or if the candidate + // hook has incomplete preprocess functions, and if the candidate hook is a + // suggestion (has a double underscore). + while ((!isset($cache[$previous_hook]) || isset($cache[$previous_hook]['incomplete preprocess functions'])) && $pos = strrpos($previous_hook, '__')) { + // Find the first existing candidate hook that has incomplete preprocess + // functions. + if (isset($cache[$previous_hook]) && !$first_valid_hook && isset($cache[$previous_hook]['incomplete preprocess functions'])) { + $first_valid_hook = $previous_hook; } $previous_hook = substr($previous_hook, 0, $pos); + $this->mergePreprocessFunctions($hook, $previous_hook, $first_valid_hook, $cache); + } - // If base hook exists clone of it for the preprocess function - // without a template. - // @see https://www.drupal.org/node/2457295 - if (isset($cache[$previous_hook]) && !isset($cache[$previous_hook]['incomplete preprocess functions'])) { - $cache[$hook] = $incomplete_previous_hook + $cache[$previous_hook]; - if (isset($incomplete_previous_hook['preprocess functions'])) { - $diff = array_diff($incomplete_previous_hook['preprocess functions'], $cache[$previous_hook]['preprocess functions']); - $cache[$hook]['preprocess functions'] = array_merge($cache[$previous_hook]['preprocess functions'], $diff); - } - // If a base hook isn't set, this is the actual base hook. - if (!isset($cache[$previous_hook]['base hook'])) { - $cache[$hook]['base hook'] = $previous_hook; - } - } + // In addition to processing suggestions, include base hooks. + if (isset($cache[$hook]['base hook']) && isset($cache[$cache[$hook]['base hook']])) { + // Pass the current hook as the first valid hook if a base hook exists. + $this->mergePreprocessFunctions($hook, $cache[$hook]['base hook'], $hook, $cache); } + + // Remove the incompleteness flag. + unset($cache[$hook]['incomplete preprocess functions']); } /** - * Merges the preceding hook's preprocess functions into the current hook's. + * Merges the source hook's preprocess functions into the destination hook's. * - * @param string $hook + * @param string $destination_hook_name * The name of the suggestion hook to complete. - * @param string $complete_hook + * @param string $source_hook_name * The name of the hook to merge preprocess functions from. + * @param string $parent_hook_name + * The name of the parent hook if it exists. * @param array $cache * The theme registry, as documented in * \Drupal\Core\Theme\Registry::processExtension(). */ - protected function mergePreprocessFunctions($hook, $complete_hook, array &$cache) { - // A suggestion might be defined in a hook_theme implementation and - // include preprocess functions not following the pattern so we merge. - if (isset($cache[$hook]['preprocess functions'])) { - $preprocess_functions = array_merge($cache[$complete_hook]['preprocess functions'], $cache[$hook]['preprocess functions']); + protected function mergePreprocessFunctions($destination_hook_name, $source_hook_name, $parent_hook_name, array &$cache) { + $parent_hook = isset($cache[$parent_hook_name]['incomplete preprocess functions']) ? $cache[$parent_hook_name] : []; + // If base hook exists clone of it for the preprocess function + // without a template. + // @see https://www.drupal.org/node/2457295 + if (isset($cache[$source_hook_name]) && !isset($cache[$source_hook_name]['incomplete preprocess functions'])) { + $cache[$destination_hook_name] = $parent_hook + $cache[$source_hook_name]; + if (isset($parent_hook['preprocess functions'])) { + $diff = array_diff($parent_hook['preprocess functions'], $cache[$source_hook_name]['preprocess functions']); + $cache[$destination_hook_name]['preprocess functions'] = array_merge($cache[$source_hook_name]['preprocess functions'], $diff); + } + // If a base hook isn't set, this is the actual base hook. + if (!isset($cache[$source_hook_name]['base hook'])) { + $cache[$destination_hook_name]['base hook'] = $source_hook_name; + } } - else { - $preprocess_functions = $cache[$complete_hook]['preprocess functions']; - } - - $cache[$hook]['preprocess functions'] = $preprocess_functions; } /** @@ -734,15 +739,7 @@ protected function postProcessExtension(array &$cache, ActiveTheme $theme) { // from a pattern. This is typically set from // drupal_find_theme_functions() and drupal_find_theme_templates(). if (isset($info['incomplete preprocess functions'])) { - // Suggestion hooks inherit from their preceding suggestion hooks first - // whereas hooks with a manually set "base hook" key can just be - // completed based on the hook given. - if (strpos($hook, '__')) { - $this->completeSuggestion($hook, $cache); - } - else { - $this->mergePreprocessFunctions($hook, $info['base hook'], $cache); - } + $this->completeSuggestion($hook, $cache); unset($cache[$hook]['incomplete preprocess functions']); } diff --git a/core/tests/Drupal/Tests/Core/Theme/RegistryTest.php b/core/tests/Drupal/Tests/Core/Theme/RegistryTest.php index c57ff91..1454311 100644 --- a/core/tests/Drupal/Tests/Core/Theme/RegistryTest.php +++ b/core/tests/Drupal/Tests/Core/Theme/RegistryTest.php @@ -176,6 +176,8 @@ public function testGetRegistryForModule() { /** * @covers ::postProcessExtension + * @covers ::completeSuggestion + * @covers ::mergePreprocessFunctions * * @dataProvider providerTestPostProcessExtension * @@ -203,7 +205,7 @@ public function testPostProcessExtension($defined_functions, $hooks, $expected) $reflection_method->setAccessible(TRUE); $reflection_method->invokeArgs($this->registry, [&$hooks, $theme->reveal()]); - $this->assertArrayEquals($hooks, $expected); + $this->assertArrayEquals($expected, $hooks); } /** @@ -233,6 +235,28 @@ public function providerTestPostProcessExtension() { ], ], ]; + $data['missing_base_hook'] = [ + 'defined_functions' => ['cat', 'mouse', 'cheese'], + 'hooks' => [ + 'test_hook' => [ + 'preprocess functions' => ['cat', 'mouse'], + ], + 'test_hook_manual' => [ + 'preprocess functions' => ['cheese'], + 'base hook' => 'missing_hook', + 'incomplete preprocess functions' => TRUE, + ], + ], + 'expected' => [ + 'test_hook' => [ + 'preprocess functions' => ['cat', 'mouse'], + ], + 'test_hook_manual' => [ + 'preprocess functions' => ['cheese'], + 'base hook' => 'missing_hook', + ], + ], + ]; $data['manual_suggestion'] = [ 'defined_functions' => [ 'cat', @@ -241,8 +265,12 @@ public function providerTestPostProcessExtension() { 'bread', 'test_preprocess_test_hook__suggestion', 'test_preprocess_test_hook__suggestion__another', + 'and_now_for_something_completely_different', ], 'hooks' => [ + 'unrelated_hook' => [ + 'preprocess functions' => ['and_now_for_something_completely_different'], + ], 'test_hook' => [ 'preprocess functions' => ['cat', 'mouse'], 'template' => 'test-hook', @@ -255,8 +283,16 @@ public function providerTestPostProcessExtension() { 'preprocess functions' => ['cheese'], 'incomplete preprocess functions' => TRUE, ], + 'test_hook__suggestion__with_base_hook' => [ + 'base hook' => 'unrelated_hook', + 'preprocess functions' => ['something_new'], + 'incomplete preprocess functions' => TRUE, + ], ], 'expected' => [ + 'unrelated_hook' => [ + 'preprocess functions' => ['and_now_for_something_completely_different'], + ], 'test_hook' => [ 'preprocess functions' => [ 'cat', @@ -286,6 +322,18 @@ public function providerTestPostProcessExtension() { 'base hook' => 'test_hook', 'template' => 'test-hook', ], + 'test_hook__suggestion__with_base_hook' => [ + 'preprocess functions' => [ + 'and_now_for_something_completely_different', + 'cat', + 'mouse', + 'bread', + 'test_preprocess_test_hook__suggestion', + 'something_new', + ], + 'base hook' => 'unrelated_hook', + 'template' => 'test-hook', + ], ], ]; return $data;