diff --git a/core/lib/Drupal/Core/Theme/Registry.php b/core/lib/Drupal/Core/Theme/Registry.php index 0cb00a6..6fd9ce5 100644 --- a/core/lib/Drupal/Core/Theme/Registry.php +++ b/core/lib/Drupal/Core/Theme/Registry.php @@ -632,6 +632,30 @@ protected function completeSuggestion($hook, array &$cache) { } /** + * Merges the preceding hook's preprocess functions into the current hook's. + * + * @param string $hook + * The name of the suggestion hook to complete. + * @param string $complete_hook + * The name of the hook to merge preprocess functions from. + * @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']); + } + else { + $preprocess_functions = $cache[$complete_hook]['preprocess functions']; + } + + $cache[$hook]['preprocess functions'] = $preprocess_functions; + } + + /** * Completes the theme registry adding discovered functions and hooks. * * @param array $cache @@ -710,7 +734,15 @@ 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'])) { - $this->completeSuggestion($hook, $cache); + // 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); + } 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 1243118..79ca932 100644 --- a/core/tests/Drupal/Tests/Core/Theme/RegistryTest.php +++ b/core/tests/Drupal/Tests/Core/Theme/RegistryTest.php @@ -67,6 +67,20 @@ class RegistryTest extends UnitTestCase { protected $themeManager; /** + * The theme. + * + * @var \Drupal\Core\Theme\ThemeManagerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected $theme; + + /** + * The list of functions that get_defined_functions() should provide. + * + * @var array + */ + public static $functions = []; + + /** * {@inheritdoc} */ protected function setUp() { @@ -83,6 +97,14 @@ protected function setUp() { } /** + * {@inheritdoc} + */ + protected function tearDown() { + parent::tearDown(); + static::$functions = []; + } + + /** * Tests getting the theme registry defined by a module. */ public function testGetRegistryForModule() { @@ -159,6 +181,137 @@ public function testGetRegistryForModule() { $this->assertTrue(in_array('test_stable_preprocess_theme_test_render_element', $other_registry['theme_test_render_element']['preprocess functions'])); } + /** + * Sets up the mocks needed for testing Registry::postProcessExtension. + */ + public function setUpPostProcessExtension() { + $this->theme = $this->getMockBuilder(ActiveTheme::class) + ->disableOriginalConstructor() + ->getMock(); + $this->theme->expects($this->atLeastOnce()) + ->method('getBaseThemes') + ->will($this->returnValue([])); + $this->theme->expects($this->atLeastOnce()) + ->method('getName') + ->will($this->returnValue('test')); + + $this->moduleHandler->expects($this->atLeastOnce()) + ->method('getModuleList') + ->willReturn([]); + } + + /** + * Tests that preprocess functions are added when base hook is specified. + * + * @covers ::postProcessExtension + */ + public function testManualBaseHook() { + static::$functions['user'] = ['cat', 'mouse', 'cheese']; + + // Define a test case registry with mock data. + $hooks = [ + 'test_hook' => [ + 'preprocess functions' => ['cat', 'mouse'], + ], + 'test_hook_manual' => [ + 'preprocess functions' => ['cheese'], + 'base hook' => 'test_hook', + 'incomplete preprocess functions' => TRUE, + ], + ]; + + $expected = [ + 'test_hook' => [ + 'preprocess functions' => ['cat', 'mouse'], + ], + 'test_hook_manual' => [ + 'preprocess functions' => ['cat', 'mouse', 'cheese'], + 'base hook' => 'test_hook', + ], + ]; + + $this->setUpPostProcessExtension(); + + $class = new \ReflectionClass(TestRegistry::class); + $reflection_method = $class->getMethod('postProcessExtension'); + $reflection_method->setAccessible(TRUE); + $reflection_method->invokeArgs($this->registry, [ &$hooks, $this->theme]); + + $this->assertArrayEquals($hooks, $expected); + } + + /** + * Tests that a suggestion defined in hook_theme gets preprocess functions. + * + * @covers ::postProcessExtension + */ + public function testManualSuggestion() { + $this->setUpPostProcessExtension(); + + $hooks = [ + 'test_hook' => [ + 'preprocess functions' => ['cat', 'mouse'], + 'template' => 'test-hook', + ], + 'test_hook__suggestion' => [ + 'preprocess functions' => ['bread'], + 'incomplete preprocess functions' => TRUE, + ], + 'test_hook__suggestion__another' => [ + 'preprocess functions' => ['cheese'], + 'incomplete preprocess functions' => TRUE, + ], + ]; + + $expected = [ + 'test_hook' => [ + 'preprocess functions' => [ + 'cat', + 'mouse', + ], + 'template' => 'test-hook', + ], + 'test_hook__suggestion' => [ + 'preprocess functions' => [ + 'cat', + 'mouse', + 'bread', + 'test_preprocess_test_hook__suggestion', + ], + 'base hook' => 'test_hook', + 'template' => 'test-hook', + ], + 'test_hook__suggestion__another' => [ + 'preprocess functions' => [ + 'cat', + 'mouse', + 'bread', + 'test_preprocess_test_hook__suggestion', + 'cheese', + 'test_preprocess_test_hook__suggestion__another', + ], + 'base hook' => 'test_hook', + 'template' => 'test-hook', + ], + ]; + + static::$functions['user'] = [ + 'cat', + 'mouse', + 'cheese', + 'bread', + 'test_preprocess_test_hook__suggestion', + 'test_preprocess_test_hook__suggestion__another', + ]; + + $class = new \ReflectionClass(TestRegistry::class); + $reflection_method = $class->getMethod('postProcessExtension'); + $reflection_method->setAccessible(TRUE); + $reflection_method->invokeArgs($this->registry, [&$hooks, $this->theme]); + + $this->assertArrayEquals($hooks, $expected); + } + protected function setupTheme() { $this->registry = new TestRegistry($this->root, $this->cache, $this->lock, $this->moduleHandler, $this->themeHandler, $this->themeInitialization); $this->registry->setThemeManager($this->themeManager); @@ -175,3 +328,14 @@ protected function getPath($module) { } } + +namespace Drupal\Core\Theme; + +use Drupal\Tests\Core\Theme\RegistryTest; + +/** + * Overrides get_defined_functions() with a configurable mock. + */ +function get_defined_functions() { + return RegistryTest::$functions ?: \get_defined_functions(); +}