diff --git a/core/lib/Drupal/Core/Theme/Registry.php b/core/lib/Drupal/Core/Theme/Registry.php index 3498991..916597a 100644 --- a/core/lib/Drupal/Core/Theme/Registry.php +++ b/core/lib/Drupal/Core/Theme/Registry.php @@ -620,6 +620,31 @@ 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; + unset($cache[$hook]['incomplete preprocess functions']); + } + + /** * Completes the theme registry adding discovered functions and hooks. * * @param array $cache @@ -698,8 +723,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); - unset($cache[$hook]['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); + } } // Optimize the registry. diff --git a/core/tests/Drupal/Tests/Core/Theme/RegistryTest.php b/core/tests/Drupal/Tests/Core/Theme/RegistryTest.php index 1243118..df39966 100644 --- a/core/tests/Drupal/Tests/Core/Theme/RegistryTest.php +++ b/core/tests/Drupal/Tests/Core/Theme/RegistryTest.php @@ -5,172 +5,324 @@ * Contains \Drupal\Tests\Core\Theme\RegistryTest. */ -namespace Drupal\Tests\Core\Theme; +namespace Drupal\Tests\Core\Theme { -use Drupal\Core\Theme\ActiveTheme; -use Drupal\Core\Theme\Registry; -use Drupal\Tests\UnitTestCase; + use Drupal\Core\Theme\ActiveTheme; + use Drupal\Core\Theme\Registry; + use Drupal\Tests\UnitTestCase; -/** - * @coversDefaultClass \Drupal\Core\Theme\Registry - * @group Theme - */ -class RegistryTest extends UnitTestCase { /** - * The tested theme registry. - * - * @var \Drupal\Tests\Core\Theme\TestRegistry + * @coversDefaultClass \Drupal\Core\Theme\Registry + * @group Theme */ - protected $registry; + class RegistryTest extends UnitTestCase { - /** - * The mocked cache backend. - * - * @var \Drupal\Core\Cache\CacheBackendInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $cache; + /** + * The tested theme registry. + * + * @var \Drupal\Tests\Core\Theme\TestRegistry + */ + protected $registry; - /** - * The mocked lock backend. - * - * @var \Drupal\Core\Lock\LockBackendInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $lock; + /** + * The mocked cache backend. + * + * @var \Drupal\Core\Cache\CacheBackendInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected $cache; - /** - * The mocked module handler. - * - * @var \Drupal\Core\Extension\ModuleHandlerInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $moduleHandler; + /** + * The mocked lock backend. + * + * @var \Drupal\Core\Lock\LockBackendInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected $lock; - /** - * The mocked theme handler. - * - * @var \Drupal\Core\Extension\ThemeHandlerInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $themeHandler; + /** + * The mocked module handler. + * + * @var \Drupal\Core\Extension\ModuleHandlerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected $moduleHandler; - /** - * The mocked theme initialization. - * - * @var \Drupal\Core\Theme\ThemeInitializationInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $themeInitialization; + /** + * The mocked theme handler. + * + * @var \Drupal\Core\Extension\ThemeHandlerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected $themeHandler; - /** - * The theme manager. - * - * @var \Drupal\Core\Theme\ThemeManagerInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $themeManager; + /** + * The mocked theme initialization. + * + * @var \Drupal\Core\Theme\ThemeInitializationInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected $themeInitialization; - /** - * {@inheritdoc} - */ - protected function setUp() { - parent::setUp(); + /** + * The theme manager. + * + * @var \Drupal\Core\Theme\ThemeManagerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected $themeManager; - $this->cache = $this->getMock('Drupal\Core\Cache\CacheBackendInterface'); - $this->lock = $this->getMock('Drupal\Core\Lock\LockBackendInterface'); - $this->moduleHandler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface'); - $this->themeHandler = $this->getMock('Drupal\Core\Extension\ThemeHandlerInterface'); - $this->themeInitialization = $this->getMock('Drupal\Core\Theme\ThemeInitializationInterface'); - $this->themeManager = $this->getMock('Drupal\Core\Theme\ThemeManagerInterface'); + /** + * The theme. + * + * @var \Drupal\Core\Theme\ThemeManagerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected $theme; - $this->setupTheme(); - } - /** - * Tests getting the theme registry defined by a module. - */ - public function testGetRegistryForModule() { - $test_theme = new ActiveTheme([ - 'name' => 'test_theme', - 'path' => 'core/modules/system/tests/themes/test_theme/test_theme.info.yml', - 'engine' => 'twig', - 'owner' => 'twig', - 'stylesheets_remove' => [], - 'libraries_override' => [], - 'libraries_extend' => [], - 'libraries' => [], - 'extension' => '.twig', - 'base_themes' => [], - ]); - - $test_stable = new ActiveTheme([ - 'name' => 'test_stable', - 'path' => 'core/modules/system/tests/themes/test_stable/test_stable.info.yml', - 'engine' => 'twig', - 'owner' => 'twig', - 'stylesheets_remove' => [], - 'libraries_override' => [], - 'libraries_extend' => [], - 'libraries' => [], - 'extension' => '.twig', - 'base_themes' => [], - ]); - - $this->themeManager->expects($this->exactly(2)) - ->method('getActiveTheme') - ->willReturnOnConsecutiveCalls($test_theme, $test_stable); - - // Include the module and theme files so that hook_theme can be called. - include_once $this->root . '/core/modules/system/tests/modules/theme_test/theme_test.module'; - include_once $this->root . '/core/modules/system/tests/themes/test_stable/test_stable.theme'; - $this->moduleHandler->expects($this->exactly(2)) + /** + * The list of functions that get_defined_functions() should provide. + * + * @var array + */ + public static $functions; + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + $this->cache = $this->getMock('Drupal\Core\Cache\CacheBackendInterface'); + $this->lock = $this->getMock('Drupal\Core\Lock\LockBackendInterface'); + $this->moduleHandler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface'); + $this->themeHandler = $this->getMock('Drupal\Core\Extension\ThemeHandlerInterface'); + $this->themeInitialization = $this->getMock('Drupal\Core\Theme\ThemeInitializationInterface'); + $this->themeManager = $this->getMock('Drupal\Core\Theme\ThemeManagerInterface'); + self::$functions = []; + $this->setupTheme(); + } + + /** + * {@inheritdoc} + */ + protected function tearDown() { + parent::tearDown(); + RegistryTest::$functions = NULL; + } + + /** + * Tests getting the theme registry defined by a module. + */ + public function testGetRegistryForModule() { + $test_theme = new ActiveTheme([ + 'name' => 'test_theme', + 'path' => 'core/modules/system/tests/themes/test_theme/test_theme.info.yml', + 'engine' => 'twig', + 'owner' => 'twig', + 'stylesheets_remove' => [], + 'libraries_override' => [], + 'libraries_extend' => [], + 'libraries' => [], + 'extension' => '.twig', + 'base_themes' => [], + ]); + + $test_stable = new ActiveTheme([ + 'name' => 'test_stable', + 'path' => 'core/modules/system/tests/themes/test_stable/test_stable.info.yml', + 'engine' => 'twig', + 'owner' => 'twig', + 'stylesheets_remove' => [], + 'libraries_override' => [], + 'libraries_extend' => [], + 'libraries' => [], + 'extension' => '.twig', + 'base_themes' => [], + ]); + + $this->themeManager->expects($this->exactly(2)) + ->method('getActiveTheme') + ->willReturnOnConsecutiveCalls($test_theme, $test_stable); + + // Include the module and theme files so that hook_theme can be called. + include_once $this->root . '/core/modules/system/tests/modules/theme_test/theme_test.module'; + include_once $this->root . '/core/modules/system/tests/themes/test_stable/test_stable.theme'; + $this->moduleHandler->expects($this->exactly(2)) + ->method('getImplementations') ->with('theme') ->will($this->returnValue(array('theme_test'))); - $this->moduleHandler->expects($this->atLeastOnce()) + $this->moduleHandler->expects($this->atLeastOnce()) ->method('getModuleList') ->willReturn([]); - $registry = $this->registry->get(); - - // Ensure that the registry entries from the module are found. - $this->assertArrayHasKey('theme_test', $registry); - $this->assertArrayHasKey('theme_test_template_test', $registry); - $this->assertArrayHasKey('theme_test_template_test_2', $registry); - $this->assertArrayHasKey('theme_test_suggestion_provided', $registry); - $this->assertArrayHasKey('theme_test_specific_suggestions', $registry); - $this->assertArrayHasKey('theme_test_suggestions', $registry); - $this->assertArrayHasKey('theme_test_function_suggestions', $registry); - $this->assertArrayHasKey('theme_test_foo', $registry); - $this->assertArrayHasKey('theme_test_render_element', $registry); - $this->assertArrayHasKey('theme_test_render_element_children', $registry); - $this->assertArrayHasKey('theme_test_function_template_override', $registry); - - $this->assertArrayNotHasKey('test_theme_not_existing_function', $registry); - $this->assertFalse(in_array('test_stable_preprocess_theme_test_render_element', $registry['theme_test_render_element']['preprocess functions'])); - - $info = $registry['theme_test_function_suggestions']; - $this->assertEquals('module', $info['type']); - $this->assertEquals('core/modules/system/tests/modules/theme_test', $info['theme path']); - $this->assertEquals('theme_theme_test_function_suggestions', $info['function']); - $this->assertEquals(array(), $info['variables']); - - // The second call will initialize with the second theme. Ensure that this - // returns a different object and the discovery for the second theme's - // preprocess function worked. - $other_registry = $this->registry->get(); - $this->assertNotSame($registry, $other_registry); - $this->assertTrue(in_array('test_stable_preprocess_theme_test_render_element', $other_registry['theme_test_render_element']['preprocess functions'])); + $registry = $this->registry->get(); + + // Ensure that the registry entries from the module are found. + $this->assertArrayHasKey('theme_test', $registry); + $this->assertArrayHasKey('theme_test_template_test', $registry); + $this->assertArrayHasKey('theme_test_template_test_2', $registry); + $this->assertArrayHasKey('theme_test_suggestion_provided', $registry); + $this->assertArrayHasKey('theme_test_specific_suggestions', $registry); + $this->assertArrayHasKey('theme_test_suggestions', $registry); + $this->assertArrayHasKey('theme_test_function_suggestions', $registry); + $this->assertArrayHasKey('theme_test_foo', $registry); + $this->assertArrayHasKey('theme_test_render_element', $registry); + $this->assertArrayHasKey('theme_test_render_element_children', $registry); + $this->assertArrayHasKey('theme_test_function_template_override', $registry); + + $this->assertArrayNotHasKey('test_theme_not_existing_function', $registry); + $this->assertFalse(in_array('test_stable_preprocess_theme_test_render_element', $registry['theme_test_render_element']['preprocess functions'])); + + $info = $registry['theme_test_function_suggestions']; + $this->assertEquals('module', $info['type']); + $this->assertEquals('core/modules/system/tests/modules/theme_test', $info['theme path']); + $this->assertEquals('theme_theme_test_function_suggestions', $info['function']); + $this->assertEquals(array(), $info['variables']); + + // The second call will initialize with the second theme. Ensure that this + // returns a different object and the discovery for the second theme's + // preprocess function worked. + $other_registry = $this->registry->get(); + $this->assertNotSame($registry, $other_registry); + $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('\Drupal\Core\Theme\ActiveTheme')->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([]); + + $class = new \ReflectionClass('Drupal\Tests\Core\Theme\TestRegistry'); + $this->reflectionMethod = $class->getMethod('postProcessExtension'); + $this->reflectionMethod->setAccessible(TRUE); + } + + /** + * Tests that preprocess functions are added when base hook is specified. + */ + public function testManualBaseHook() { + + $functions = ['cat', 'mouse', 'cheese']; + self::$functions['user'] = $functions; + // 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(); + + $this->reflectionMethod->invokeArgs($this->registry, [ &$hooks, $this->theme]); + + $this->assertArrayEquals($hooks, $expected); + + } + + /** + * Tests that a suggestion defined in hook_theme gets preprocess functions. + */ + public function testManualSuggestion() { + + $this->setUpPostProcessExtension(); + + $functions = ['cat', 'mouse', 'cheese']; + + $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', ] + ]; + + $functions = ['cat', 'mouse', 'cheese', 'bread', + 'test_preprocess_test_hook__suggestion', + 'test_preprocess_test_hook__suggestion__another']; + self::$functions['user'] = $functions; + + $this->reflectionMethod->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); + } + } - protected function setupTheme() { - $this->registry = new TestRegistry($this->root, $this->cache, $this->lock, $this->moduleHandler, $this->themeHandler, $this->themeInitialization); - $this->registry->setThemeManager($this->themeManager); + class TestRegistry extends Registry { + + protected $functions = []; + + protected function getPath($module) { + if ($module == 'theme_test') { + return 'core/modules/system/tests/modules/theme_test'; + } + } + } } -class TestRegistry extends Registry { +namespace Drupal\Core\Theme { - protected function getPath($module) { - if ($module == 'theme_test') { - return 'core/modules/system/tests/modules/theme_test'; + /** + * Override get_defined_functions with a configurable mock. + * + * Registry::getPrefixGroupedUserFunctions calls get_defined_functions and + * that is overridden here to allow testing postProcessExtension. + */ + function get_defined_functions() { + if (\Drupal\Tests\Core\Theme\RegistryTest::$functions) { + return \Drupal\Tests\Core\Theme\RegistryTest::$functions; + } + else { + return \get_defined_functions(); } }