diff --git a/core/includes/theme.inc b/core/includes/theme.inc index e49bb76..cd59f95 100644 --- a/core/includes/theme.inc +++ b/core/includes/theme.inc @@ -1055,6 +1055,43 @@ function theme($hook, $variables = array()) { $info['preprocess functions'] = $base_hook_info['preprocess functions']; } } + + // Provide immutable context for subsequent hooks. + $context_original = array( + 'hook' => $hook, + 'original_hook' => $original_hook, + 'suggestions' => $suggestions, + ); + + // A list of hooks that we'll fire for the given theme hook. + $prepare_hooks = array( + 'theme_prepare', + 'theme_prepare_' . $hook, + ); + // If the original theme hook is not the same as the base theme hook, then + // invoke the original theme hook suggestion after the base theme hook. + if ($hook !== $original_hook) { + $prepare_hooks[] = 'theme_prepare_' . $original_hook; + } + + // Invokes hook_theme_prepare(), hook_theme_prepare_THEME_ID() for enabled + // modules. + foreach ($prepare_hooks as $prepare_hook) { + $context = $context_original; + $variables = NestedArray::mergeDeep($variables, (array) Drupal::moduleHandler()->invokeAll($prepare_hook, array($context))); + } + + // Invokes hook_theme_prepare(), hook_theme_prepare_THEME_ID() for the active + // theme, which takes precedence over modules. + // @todo Replace with Drupal::themeHandler()->invoke() once implemented. + // @see https://drupal.org/node/2029819 + foreach ($prepare_hooks as $prepare_hook) { + $context = $context_original; + $variables = NestedArray::mergeDeep($variables, (array) Drupal::moduleHandler()->themeInvoke($prepare_hook, array($context))); + } + + // Invokes preprocess functions. + // @todo Deprecate @see https://drupal.org/node/2060773 if (isset($info['preprocess functions'])) { foreach ($info['preprocess functions'] as $preprocessor_function) { if (function_exists($preprocessor_function)) { @@ -1063,6 +1100,14 @@ function theme($hook, $variables = array()) { } } + // Invokes hook_theme_prepare_alter(), hook_theme_prepare_THEME_ID_alter() for enabled modules. + // @todo Add Drupal::themeHandler()->alter() invocations once implemented. + // @see https://drupal.org/node/2029819 + foreach ($prepare_hooks as $prepare_hook) { + $context = $context_original; + Drupal::moduleHandler()->alter($prepare_hook, $variables, $context); + } + // Generate the output using either a function or a template. $output = ''; if (isset($info['function'])) { diff --git a/core/lib/Drupal/Core/Extension/ModuleHandler.php b/core/lib/Drupal/Core/Extension/ModuleHandler.php index 92cf2a4..96b332f 100644 --- a/core/lib/Drupal/Core/Extension/ModuleHandler.php +++ b/core/lib/Drupal/Core/Extension/ModuleHandler.php @@ -273,6 +273,31 @@ public function invoke($module, $hook, $args = array()) { } /** + * Rudimentary and temporary method for invoking theme hooks. + * + * @todo Remove once a proper Drupal::themeHandler() is created. + * @see https://drupal.org/node/2029819 + */ + public function themeInvoke($hook, $args = array()) { + // Allow the active theme to invoke hooks after the theme system has been + // initialized. + global $theme, $base_theme_info; + if (isset($theme)) { + $theme_keys = array(); + foreach ($base_theme_info as $base) { + $theme_keys[] = $base->name; + } + $theme_keys[] = $theme; + foreach ($theme_keys as $theme_key) { + $function = $theme_key . '_' . $hook; + if (function_exists($function)) { + return call_user_func_array($function, $args); + } + } + } + } + + /** * Implements \Drupal\Core\Extension\ModuleHandlerInterface::invokeAll(). */ public function invokeAll($hook, $args = array()) { diff --git a/core/modules/system/lib/Drupal/system/Tests/Theme/ThemePrepareTest.php b/core/modules/system/lib/Drupal/system/Tests/Theme/ThemePrepareTest.php new file mode 100644 index 0000000..8b1ed5d --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/Theme/ThemePrepareTest.php @@ -0,0 +1,53 @@ + 'Theme Prepare API', + 'description' => 'Test theme prepare and alter functions.', + 'group' => 'Theme', + ); + } + + function setUp() { + parent::setUp(); + theme_enable(array('test_theme')); + } + + function testThemePrepare() { + $this->drupalGet('theme-test/prepare'); + $this->assertText('Inserted variable1 via hook_theme_prepare().'); + $this->assertText('Inserted variable2 via hook_theme_prepare_BASE_THEME_ID().'); + $this->assertText('Inserted themeVariable1 via theme hook_theme_prepare().'); + $this->assertText('Inserted themeVariable2 via theme hook_theme_prepare_BASE_THEME_ID().'); + } + + function testThemePrepareAlter() { + $this->drupalGet('theme-test/prepare-alter'); + $this->assertText('Inserted variable1 via hook_theme_prepare() was altered by hook_theme_prepare_alter().'); + $this->assertText('Inserted variable2 via hook_theme_prepare_BASE_THEME_ID() was altered by hook_theme_prepare_BASE_THEME_ID_alter().'); + $this->assertText('Inserted themeVariable1 via theme hook_theme_prepare() was altered by theme hook_theme_prepare_alter().'); + $this->assertText('Inserted themeVariable2 via theme hook_theme_prepare_BASE_THEME_ID() was altered by theme hook_theme_prepare_BASE_THEME_ID_alter().'); + } + +} diff --git a/core/modules/system/tests/modules/theme_test/lib/Drupal/theme_test/ThemeTestController.php b/core/modules/system/tests/modules/theme_test/lib/Drupal/theme_test/ThemeTestController.php index 8da6c62..1389cea 100644 --- a/core/modules/system/tests/modules/theme_test/lib/Drupal/theme_test/ThemeTestController.php +++ b/core/modules/system/tests/modules/theme_test/lib/Drupal/theme_test/ThemeTestController.php @@ -120,4 +120,22 @@ function functionSuggestionAlter() { return array('#theme' => 'theme_test_function_suggestions'); } + /** + * Menu callback for testing hook_theme_prepare(). + */ + function prepare() { + return array( + '#theme' => 'theme_test_prepare_variable', + ); + } + + /** + * Menu callback for testing hook_theme_prepare_alter(). + */ + function prepareAlter() { + return array( + '#theme' => 'theme_test_prepare_alter_variable', + ); + } + } 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 badea81..43872ad 100644 --- a/core/modules/system/tests/modules/theme_test/theme_test.module +++ b/core/modules/system/tests/modules/theme_test/theme_test.module @@ -42,6 +42,12 @@ function theme_test_theme($existing, $type, $theme, $path) { $items['theme_test_function_template_override'] = array( 'variables' => array(), ); + $items['theme_test_prepare_variable'] = array( + 'variables' => array('element' => array()), + ); + $items['theme_test_prepare_alter_variable'] = array( + 'variables' => array('element' => array()), + ); return $items; } @@ -74,6 +80,14 @@ function theme_test_menu() { 'theme callback' => '_theme_custom_theme', 'route_name' => 'theme_test.function_template_override', ); + $items['theme-test/prepare'] = array( + 'theme callback' => '_theme_custom_theme', + 'route_name' => 'theme_test.prepare', + ); + $items['theme-test/prepare-alter'] = array( + 'theme callback' => '_theme_custom_theme', + 'route_name' => 'theme_test.prepare_alter', + ); return $items; } /** @@ -166,3 +180,68 @@ function theme_theme_test_function_suggestions($variables) { function theme_test_theme_suggestions_theme_test_suggestion_provided(array $variables) { return array('theme_test_suggestion_provided__' . 'foo'); } + +function theme_theme_test_prepare_variable($variables) { + return drupal_render($variables['element']); +} + +function theme_theme_test_prepare_alter_variable($variables) { + return drupal_render($variables['element']); +} + +/** + * Implements hook_theme_prepare(). + */ +function theme_test_theme_prepare($context) { + $variables = array(); + if ($context['#base_theme_id'] == 'theme_test_prepare_variable' || $context['#base_theme_id'] == 'theme_test_prepare_alter_variable') { + $variables['element'] = array( + 'variable1' => array( + '#markup' => '

Inserted variable1 via hook_theme_prepare().

', + ), + ); + } + return $variables; +} + +/** + * Implements hook_theme_prepare_BASE_THEME_ID(). + */ +function theme_test_theme_prepare_theme_test_prepare_variable() { + $variables = array(); + $variables['element'] = array( + 'variable2' => array( + '#markup' => '

Inserted variable2 via hook_theme_prepare_BASE_THEME_ID().

', + ), + ); + return $variables; +} + +/** + * Implements hook_theme_prepare_BASE_THEME_ID(). + */ +function theme_test_theme_prepare_theme_test_prepare_alter_variable() { + $variables = array(); + $variables['element'] = array( + 'variable2' => array( + '#markup' => '

Inserted variable2 via hook_theme_prepare_BASE_THEME_ID().

', + ), + ); + return $variables; +} + +/** + * Implements hook_theme_prepare_alter(). + */ +function theme_test_theme_prepare_alter(&$variables, $context) { + if ($context['#base_theme_id'] == 'theme_test_prepare_alter_variable') { + $variables['element']['variable1']['#markup'] = '

Inserted variable1 via hook_theme_prepare() was altered by hook_theme_prepare_alter().

'; + } +} + +/** + * Implements hook_theme_prepare_BASE_THEME_ID_alter(). + */ +function theme_test_theme_prepare_theme_test_prepare_alter_variable_alter(&$variables) { + $variables['element']['variable2']['#markup'] = '

Inserted variable2 via hook_theme_prepare_BASE_THEME_ID() was altered by hook_theme_prepare_BASE_THEME_ID_alter().

'; +} 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 b4a7fd6..446f780 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 @@ -69,3 +69,17 @@ function_suggestion_alter: _content: '\Drupal\theme_test\ThemeTestController::functionSuggestionAlter' requirements: _permission: 'access content' + +theme_test.prepare: + pattern: '/theme-test/prepare' + defaults: + _content: '\Drupal\theme_test\ThemeTestController::prepare' + requirements: + _permission: 'access content' + +theme_test.prepare_alter: + pattern: '/theme-test/prepare-alter' + defaults: + _content: '\Drupal\theme_test\ThemeTestController::prepareAlter' + requirements: + _permission: 'access content' 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 9b10b2b..5f07789 100644 --- a/core/modules/system/tests/themes/test_theme/test_theme.theme +++ b/core/modules/system/tests/themes/test_theme/test_theme.theme @@ -69,3 +69,60 @@ function test_theme_theme_test_function_suggestions__theme_override($variables) 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_prepare(). + */ +function test_theme_theme_prepare($context) { + $variables = array(); + if ($context['#base_theme_id'] == 'theme_test_prepare_variable' || $context['#base_theme_id'] == 'theme_test_prepare_alter_variable') { + $variables['element'] = array( + 'themeVariable1' => array( + '#markup' => '

Inserted themeVariable1 via theme hook_theme_prepare().

', + ), + ); + } + return $variables; +} + +/** + * Implements hook_theme_prepare_BASE_THEME_ID(). + */ +function test_theme_theme_prepare_theme_test_prepare_variable() { + $variables = array(); + $variables['element'] = array( + 'themeVariable2' => array( + '#markup' => '

Inserted themeVariable2 via theme hook_theme_prepare_BASE_THEME_ID().

', + ), + ); + return $variables; +} + +/** + * Implements hook_theme_prepare_BASE_THEME_ID(). + */ +function test_theme_theme_prepare_theme_test_prepare_alter_variable() { + $variables = array(); + $variables['element'] = array( + 'themeVariable2' => array( + '#markup' => '

Inserted themeVariable2 via theme hook_theme_prepare_BASE_THEME_ID().

', + ), + ); + return $variables; +} + +/** + * Implements hook_theme_prepare_alter(). + */ +function test_theme_theme_prepare_alter(&$variables, $context) { + if ($context['#base_theme_id'] == 'theme_test_prepare_alter_variable') { + $variables['element']['themeVariable1']['#markup'] = '

Inserted themeVariable1 via theme hook_theme_prepare() was altered by theme hook_theme_prepare_alter().

'; + } +} + +/** + * Implements hook_theme_prepare_BASE_THEME_ID_alter(). + */ +function test_theme_theme_prepare_theme_test_prepare_alter_variable_alter(&$variables) { + $variables['element']['themeVariable2']['#markup'] = '

Inserted themeVariable2 via theme hook_theme_prepare_BASE_THEME_ID() was altered by theme hook_theme_prepare_BASE_THEME_ID_alter().

'; +}