diff --git a/core/includes/theme.inc b/core/includes/theme.inc index 9545be7..ccfa76f 100644 --- a/core/includes/theme.inc +++ b/core/includes/theme.inc @@ -430,37 +430,20 @@ function drupal_find_base_themes($themes, $key) { * engine, it overrides the default implementation of the 'page' theme hook by * containing a 'page.html.twig' file within its folder structure). * - * @subsection sub_preprocess_templates Preprocessing for Template Files - * If the implementation is a template file, several functions are called before - * the template file is invoked to modify the $variables array. These make up - * the "preprocessing" phase, and are executed (if they exist), in the following - * order (note that in the following list, HOOK indicates the theme hook name, - * MODULE indicates a module name, THEME indicates a theme name, and ENGINE - * indicates a theme engine name): - * - template_preprocess(&$variables, $hook): Creates a default set of variables - * for all theme hooks with template implementations. - * - template_preprocess_HOOK(&$variables): Should be implemented by the module - * that registers the theme hook, to set up default variables. - * - MODULE_preprocess(&$variables, $hook): hook_preprocess() is invoked on all - * implementing modules. - * - MODULE_preprocess_HOOK(&$variables): hook_preprocess_HOOK() is invoked on - * all implementing modules, so that modules that didn't define the theme hook - * can alter the variables. - * - ENGINE_engine_preprocess(&$variables, $hook): Allows the theme engine to - * set necessary variables for all theme hooks with template implementations. - * - ENGINE_engine_preprocess_HOOK(&$variables): Allows the theme engine to set - * necessary variables for the particular theme hook. - * - THEME_preprocess(&$variables, $hook): Allows the theme to set necessary - * variables for all theme hooks with template implementations. - * - THEME_preprocess_HOOK(&$variables): Allows the theme to set necessary - * variables specific to the particular theme hook. - * - * @subsection sub_preprocess_theme_funcs Preprocessing for Theme Functions - * If the implementation is a function, only the theme-hook-specific preprocess - * functions (the ones ending in _HOOK) are called from the list above. This is - * because theme hooks with function implementations need to be fast, and - * calling the non-theme-hook-specific preprocess functions for them would incur - * a noticeable performance penalty. + * @subsection sub_prepare_variables Preparing Variables for Theme Hooks + * Several functions are invoked before theme functions or template files are + * provided their available $variables. These functions make up the "prepare" + * phase, and are executed (if they exist), in the following + * order (HOOK indicates the profile, module, theme engine or theme name, + * THEME_HOOK indicates the name of the specific theme hook called): + * - HOOK_theme_prepare(&$variables, $hook): Prepare a set of variables + * for all theme hook implementations. + * - HOOK_theme_prepare_THEME_HOOK(&$variables): Prepare a set of + * variables for a specific theme hook implementation. + * - HOOK_theme_prepare_alter(&$variables, $hook): Alter a set of prepared + * variables for all theme hook implementations. + * - HOOK_theme_prepare_THEME_HOOK_alter(&$variables, $context): Alter a set of + * prepared variables for a specific theme hook implementation. * * @subsection sub_alternate_suggestions Suggesting Alternate Hooks * Alternate hooks can be suggested by implementing the hook-specific @@ -476,11 +459,11 @@ function drupal_find_base_themes($themes, $key) { * * @param $hook * The name of the theme hook to call. If the name contains a - * double-underscore ('__') and there isn't an implementation for the full - * name, the part before the '__' is checked. This allows a fallback to a + * double-underscore ('__') and there is not an implementation for the full + * name, the name before the '__' is checked. This allows a fallback to a * more generic implementation. For example, if theme('links__node', ...) is * called, but there is no implementation of that theme hook, then the - * 'links' implementation is used. This process is iterative, so if + * 'links' implementation is used instead. This process is iterative, so if * theme('links__contextual__node', ...) is called, theme() checks for the * following implementations, and uses the first one that exists: * - links__contextual__node @@ -494,7 +477,7 @@ function drupal_find_base_themes($themes, $key) { * convention is not desired or is insufficient. * @param $variables * An associative array of variables to merge with defaults from the theme - * registry, pass to preprocess functions for modification, and finally, pass + * registry, pass to prepare functions for modification, and finally, pass * to the function or template implementing the theme hook. Alternatively, * this can be a renderable array, in which case, its properties are mapped to * variables expected by the theme hook implementations. @@ -507,6 +490,8 @@ function drupal_find_base_themes($themes, $key) { * @see themeable * @see hook_theme() * @see template_preprocess() + * + * @throws Exception */ function theme($hook, $variables = array()) { static $default_attributes; @@ -598,10 +583,6 @@ function theme($hook, $variables = array()) { elseif (!empty($info['render element'])) { $variables += array($info['render element'] => array()); } - // Supply original caller info. - $variables += array( - 'theme_hook_original' => $original_hook, - ); // Set base hook for later use. For example if '#theme' => 'node__article' // is called, we run hook_theme_suggestions_node_alter() rather than @@ -614,6 +595,12 @@ function theme($hook, $variables = array()) { $base_theme_hook = $hook; } + // Supply original caller info. + $variables += array( + 'theme_hook' => $base_theme_hook, + 'theme_hook_original' => $original_hook, + ); + // Invoke hook_theme_suggestions_HOOK(). $suggestions = Drupal::moduleHandler()->invokeAll('theme_suggestions_' . $base_theme_hook, array($variables)); // If theme() was invoked with a direct theme suggestion like @@ -656,6 +643,43 @@ function theme($hook, $variables = array()) { $info['preprocess functions'] = $base_hook_info['preprocess functions']; } } + + // A list of hooks that will be invoked for the given theme hook. + $prepare_hooks = array( + 'theme_prepare', + 'theme_prepare_' . $base_theme_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; + } + + // Send prepare arguments to invoke hooks with $variables passed by reference. + $prepare_args = array(&$variables, $base_theme_hook); + + // Invokes hook_theme_prepare(), hook_theme_prepare_THEME_HOOK() for enabled + // modules. + foreach ($prepare_hooks as $prepare_hook) { + foreach (\Drupal::moduleHandler()->getImplementations($prepare_hook) as $module) { + $prepare_function = $module . '_' . $prepare_hook; + if (function_exists($prepare_function)) { + call_user_func_array($prepare_function, $prepare_args); + } + } + } + + // Invokes hook_theme_prepare(), hook_theme_prepare_THEME_HOOK() 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) { + theme_invoke($prepare_hook, $prepare_args); + } + + // Invokes hook_preprocess() and hook_preprocess_HOOK() implementations. This + // phase is deprecated and is run in between hook_theme_prepare[_THEME_HOOK]() + // and hook_theme_prepare[_THEME_HOOK]_alter(). if (isset($info['preprocess functions'])) { foreach ($info['preprocess functions'] as $preprocessor_function) { if (function_exists($preprocessor_function)) { @@ -664,6 +688,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) { + Drupal::moduleHandler()->alter($prepare_hook, $variables, $base_theme_hook); + } + // Generate the output using either a function or a template. $output = ''; if (isset($info['function'])) { @@ -1097,6 +1129,39 @@ function theme_disable($theme_list) { } /** + * Rudimentary and temporary method for invoking theme hooks. + * + * @param string $hook + * The hook name to invoke. + * @param array $args + * The array of arguments to pass to hook invocation (by reference). + * + * @return mixed + * The value returned varies on which hook is invoked. + * + * @todo Remove once a proper Drupal::themeHandler() is created. + * @see https://drupal.org/node/2029819 + */ +function theme_invoke($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)) { + call_user_func_array($function, $args); + } + } + } +} + +/** * @addtogroup themeable * @{ */ 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..5ca1d0f --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/Theme/ThemePrepareTest.php @@ -0,0 +1,260 @@ + 'Theme Prepare API', + 'description' => 'Test theme prepare and alter functions.', + 'group' => 'Theme', + ); + } + + public function setUp() { + parent::setUp(); + theme_enable(array('test_theme', 'test_basetheme', 'test_subtheme')); + } + + /** + * Ensures prepare hooks work and are invoked and in the correct order. + */ + public function testThemePrepare() { + \Drupal::config('system.theme') + ->set('default', 'test_theme') + ->save(); + $this->drupalGet('theme-test/prepare'); + + $this->assertRaw('

moduleVariable1: Drupal\theme_test\ThemeTestController::prepare
+test_theme_theme_prepare
+test_theme_theme_prepare_theme_test_prepare
+theme_test_theme_prepare_alter
+test_theme_theme_prepare_alter
+test_theme_theme_prepare_theme_test_prepare_alter

'); + + $this->assertRaw('

moduleVariable2: Drupal\theme_test\ThemeTestController::prepare
+test_theme_theme_prepare
+test_theme_theme_prepare_theme_test_prepare
+theme_test_theme_prepare_alter
+test_theme_theme_prepare_alter
+test_theme_theme_prepare_theme_test_prepare_alter

'); + + $this->assertRaw('

moduleVariableSuggestion1: Drupal\theme_test\ThemeTestController::prepare
+test_theme_theme_prepare
+test_theme_theme_prepare_theme_test_prepare
+theme_test_theme_prepare_alter
+test_theme_theme_prepare_alter
+test_theme_theme_prepare_theme_test_prepare_alter

'); + + $this->assertRaw('

moduleVariableSuggestion2: Drupal\theme_test\ThemeTestController::prepare
+theme_test_theme_prepare_theme_test_prepare__implemented
+test_theme_theme_prepare
+test_theme_theme_prepare_theme_test_prepare
+test_theme_theme_prepare_theme_test_prepare__implemented
+theme_test_theme_prepare_alter
+test_theme_theme_prepare_alter
+test_theme_theme_prepare_theme_test_prepare_alter
+theme_test_theme_prepare_theme_test_prepare__implemented_alter
+test_theme_theme_prepare_theme_test_prepare__implemented_alter

'); + + $this->assertRaw('

moduleVariableSuggestion3: Drupal\theme_test\ThemeTestController::prepare
+theme_test_theme_prepare_theme_test_prepare__implemented__extended
+test_theme_theme_prepare
+test_theme_theme_prepare_theme_test_prepare
+test_theme_theme_prepare_theme_test_prepare__implemented__extended
+theme_test_theme_prepare_alter
+test_theme_theme_prepare_alter
+test_theme_theme_prepare_theme_test_prepare_alter
+theme_test_theme_prepare_theme_test_prepare__implemented__extended_alter
+test_theme_theme_prepare_theme_test_prepare__implemented__extended_alter

'); + + $this->assertRaw('

moduleElement1: Drupal\theme_test\ThemeTestController::prepare
+test_theme_theme_prepare
+test_theme_theme_prepare_theme_test_prepare
+theme_test_theme_prepare_alter
+test_theme_theme_prepare_alter
+test_theme_theme_prepare_theme_test_prepare_alter

'); + + $this->assertRaw('

moduleInjectedVariable1: theme_test_theme_prepare
+test_theme_theme_prepare
+test_theme_theme_prepare_theme_test_prepare
+theme_test_theme_prepare_alter
+test_theme_theme_prepare_alter
+test_theme_theme_prepare_theme_test_prepare_alter

'); + + $this->assertRaw('

moduleInjectedVariable2: theme_test_theme_prepare_theme_test_prepare_element
+test_theme_theme_prepare
+test_theme_theme_prepare_theme_test_prepare
+theme_test_theme_prepare_alter
+test_theme_theme_prepare_alter
+test_theme_theme_prepare_theme_test_prepare_alter

'); + + $this->assertRaw('

themeInjectedVariable1: test_theme_theme_prepare
+test_theme_theme_prepare
+test_theme_theme_prepare_theme_test_prepare
+theme_test_theme_prepare_alter
+test_theme_theme_prepare_alter
+test_theme_theme_prepare_theme_test_prepare_alter

'); + + $this->assertRaw('

moduleInjectedVariable3: theme_test_theme_prepare_theme_test_prepare_element_alter
+test_theme_theme_prepare
+test_theme_theme_prepare_theme_test_prepare
+theme_test_theme_prepare_alter
+test_theme_theme_prepare_alter
+test_theme_theme_prepare_theme_test_prepare_alter

'); + + $this->assertRaw('

moduleElementSuggestion1: Drupal\theme_test\ThemeTestController::prepare
+test_theme_theme_prepare
+test_theme_theme_prepare_theme_test_prepare
+theme_test_theme_prepare_alter
+test_theme_theme_prepare_alter
+test_theme_theme_prepare_theme_test_prepare_alter

'); + + $this->assertRaw('

moduleInjectedVariable1: theme_test_theme_prepare
+test_theme_theme_prepare
+test_theme_theme_prepare_theme_test_prepare
+theme_test_theme_prepare_alter
+test_theme_theme_prepare_alter
+test_theme_theme_prepare_theme_test_prepare_alter

'); + + $this->assertRaw('

moduleInjectedVariable2: theme_test_theme_prepare_theme_test_prepare_element
+test_theme_theme_prepare
+test_theme_theme_prepare_theme_test_prepare
+theme_test_theme_prepare_alter
+test_theme_theme_prepare_alter
+test_theme_theme_prepare_theme_test_prepare_alter

'); + + $this->assertRaw('

themeInjectedVariable1: test_theme_theme_prepare
+test_theme_theme_prepare
+test_theme_theme_prepare_theme_test_prepare
+theme_test_theme_prepare_alter
+test_theme_theme_prepare_alter
+test_theme_theme_prepare_theme_test_prepare_alter

'); + + $this->assertRaw('

moduleInjectedVariable3: theme_test_theme_prepare_theme_test_prepare_element_alter
+test_theme_theme_prepare
+test_theme_theme_prepare_theme_test_prepare
+theme_test_theme_prepare_alter
+test_theme_theme_prepare_alter
+test_theme_theme_prepare_theme_test_prepare_alter

'); + + $this->assertRaw('

moduleElementSuggestion2: Drupal\theme_test\ThemeTestController::prepare
+test_theme_theme_prepare
+test_theme_theme_prepare_theme_test_prepare
+theme_test_theme_prepare_alter
+test_theme_theme_prepare_alter
+test_theme_theme_prepare_theme_test_prepare_alter

'); + + $this->assertRaw('

moduleInjectedVariable1: theme_test_theme_prepare
+test_theme_theme_prepare
+test_theme_theme_prepare_theme_test_prepare
+theme_test_theme_prepare_alter
+test_theme_theme_prepare_alter
+test_theme_theme_prepare_theme_test_prepare_alter

'); + + $this->assertRaw('

moduleInjectedVariable2: theme_test_theme_prepare_theme_test_prepare_element
+test_theme_theme_prepare
+test_theme_theme_prepare_theme_test_prepare
+theme_test_theme_prepare_alter
+test_theme_theme_prepare_alter
+test_theme_theme_prepare_theme_test_prepare_alter

'); + + $this->assertRaw('

themeInjectedVariable1: test_theme_theme_prepare
+test_theme_theme_prepare
+test_theme_theme_prepare_theme_test_prepare
+theme_test_theme_prepare_alter
+test_theme_theme_prepare_alter
+test_theme_theme_prepare_theme_test_prepare_alter

'); + + $this->assertRaw('

moduleInjectedVariable3: theme_test_theme_prepare_theme_test_prepare_element_alter
+test_theme_theme_prepare
+test_theme_theme_prepare_theme_test_prepare
+theme_test_theme_prepare_alter
+test_theme_theme_prepare_alter
+test_theme_theme_prepare_theme_test_prepare_alter

'); + + $this->assertRaw('

moduleElementSuggestion3: Drupal\theme_test\ThemeTestController::prepare
+test_theme_theme_prepare
+test_theme_theme_prepare_theme_test_prepare
+theme_test_theme_prepare_alter
+test_theme_theme_prepare_alter
+test_theme_theme_prepare_theme_test_prepare_alter

'); + + $this->assertRaw('

moduleInjectedVariable1: theme_test_theme_prepare
+test_theme_theme_prepare
+test_theme_theme_prepare_theme_test_prepare
+theme_test_theme_prepare_alter
+test_theme_theme_prepare_alter
+test_theme_theme_prepare_theme_test_prepare_alter

'); + + $this->assertRaw('

moduleInjectedVariable2: theme_test_theme_prepare_theme_test_prepare_element
+test_theme_theme_prepare
+test_theme_theme_prepare_theme_test_prepare
+theme_test_theme_prepare_alter
+test_theme_theme_prepare_alter
+test_theme_theme_prepare_theme_test_prepare_alter

'); + + $this->assertRaw('

themeInjectedVariable1: test_theme_theme_prepare
+test_theme_theme_prepare
+test_theme_theme_prepare_theme_test_prepare
+theme_test_theme_prepare_alter
+test_theme_theme_prepare_alter
+test_theme_theme_prepare_theme_test_prepare_alter

'); + + $this->assertRaw('

moduleInjectedVariable3: theme_test_theme_prepare_theme_test_prepare_element_alter
+test_theme_theme_prepare
+test_theme_theme_prepare_theme_test_prepare
+theme_test_theme_prepare_alter
+test_theme_theme_prepare_alter
+test_theme_theme_prepare_theme_test_prepare_alter

'); + } + + /** + * Ensures prepare hooks for subthemes work. + */ + function testThemePrepareSubtheme() { + \Drupal::config('system.theme') + ->set('default', 'test_subtheme') + ->save(); + $this->drupalGet('theme-test/prepare-subtheme'); + + $this->assertRaw('

subThemePrepare: Drupal\theme_test\ThemeTestController::prepareSubtheme
+test_basetheme_theme_prepare_theme_test_prepare
+test_subtheme_theme_prepare_theme_test_prepare
+theme_test_theme_prepare_alter
+test_basetheme_theme_prepare_theme_test_prepare_alter
+test_subtheme_theme_prepare_theme_test_prepare_alter

'); + + $this->assertRaw('

subThemePrepareSuggestion: Drupal\theme_test\ThemeTestController::prepareSubtheme
+theme_test_theme_prepare_theme_test_prepare__implemented
+test_basetheme_theme_prepare_theme_test_prepare
+test_subtheme_theme_prepare_theme_test_prepare
+test_basetheme_theme_prepare_theme_test_prepare__implemented
+test_subtheme_theme_prepare_theme_test_prepare__implemented
+theme_test_theme_prepare_alter
+test_basetheme_theme_prepare_theme_test_prepare_alter
+test_subtheme_theme_prepare_theme_test_prepare_alter
+theme_test_theme_prepare_theme_test_prepare__implemented_alter
+test_basetheme_theme_prepare_theme_test_prepare__implemented_alter
+test_subtheme_theme_prepare_theme_test_prepare__implemented_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..34392d2 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,87 @@ function functionSuggestionAlter() { return array('#theme' => 'theme_test_function_suggestions'); } + /** + * Menu callback for testing hook_theme[_THEME_HOOK]_prepare[_alter](). + */ + function prepare() { + return array( + 'moduleVariable1' => array( + '#theme' => 'theme_test_prepare', + '#title' => 'moduleVariable1', + '#value' => __METHOD__, + ), + 'moduleVariable2' => array( + '#theme' => 'theme_test_prepare', + '#title' => 'moduleVariable2', + '#value' => __METHOD__, + ), + 'moduleVariableSuggestion1' => array( + '#theme' => 'theme_test_prepare__fallback', + '#title' => 'moduleVariableSuggestion1', + '#value' => __METHOD__, + ), + 'moduleVariableSuggestion2' => array( + '#theme' => 'theme_test_prepare__implemented', + '#title' => 'moduleVariableSuggestion2', + '#value' => __METHOD__, + ), + 'moduleVariableSuggestion3' => array( + '#theme' => 'theme_test_prepare__implemented__extended', + '#title' => 'moduleVariableSuggestion3', + '#value' => __METHOD__, + ), + 'moduleElement1' => array( + '#theme' => 'theme_test_prepare_element', + 'moduleElement1' => array( + '#theme' => 'theme_test_prepare', + '#title' => 'moduleElement1', + '#value' => __METHOD__, + ), + ), + 'moduleElementSuggestion1' => array( + '#theme' => 'theme_test_prepare_element__fallback', + 'moduleElementSuggestion1' => array( + '#theme' => 'theme_test_prepare', + '#title' => 'moduleElementSuggestion1', + '#value' => __METHOD__, + ), + ), + 'moduleElementSuggestion2' => array( + '#theme' => 'theme_test_prepare_element__implemented', + 'moduleElementSuggestion2' => array( + '#theme' => 'theme_test_prepare', + '#title' => 'moduleElementSuggestion2', + '#value' => __METHOD__, + ), + ), + 'moduleElementSuggestion3' => array( + '#theme' => 'theme_test_prepare_element__implemented__extended', + 'moduleElementSuggestion3' => array( + '#theme' => 'theme_test_prepare', + '#title' => 'moduleElementSuggestion3', + '#value' => __METHOD__, + ), + ), + ); + } + + /** + * Menu callback for testing prepare hooks with subthemes. + */ + function prepareSubtheme() { + return array( + 'subThemePrepare' => array( + '#theme' => 'theme_test_prepare', + '#title' => 'subThemePrepare', + '#value' => __METHOD__, + ), + 'subThemePrepareSuggestion' => array( + '#theme' => 'theme_test_prepare__implemented', + '#title' => 'subThemePrepareSuggestion', + '#value' => __METHOD__, + ), + ); + } + } 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 52bff21..259778e 100644 --- a/core/modules/system/tests/modules/theme_test/theme_test.module +++ b/core/modules/system/tests/modules/theme_test/theme_test.module @@ -45,6 +45,13 @@ function theme_test_theme($existing, $type, $theme, $path) { $info['test_theme_not_existing_function'] = array( 'function' => 'test_theme_not_existing_function', ); + // hook_theme_prepare() theme hooks. + $items['theme_test_prepare'] = array( + 'variables' => array('hook' => NULL, 'title' => NULL, 'value' => NULL), + ); + $items['theme_test_prepare_element'] = array( + 'render element' => 'element', + ); return $items; } @@ -77,6 +84,10 @@ 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', + ); return $items; } @@ -150,8 +161,97 @@ function theme_theme_test_function_suggestions($variables) { } /** + * Returns HTML for theme_test_prepare theme hook. + */ +function theme_theme_test_prepare($variables) { + $title = $variables['title']; + $value = $variables['value']; + return "

$title: $value

"; +} + +/** + * Theme function for testing rendering of theme_test_prepare_element(). + */ +function theme_theme_test_prepare_element($variables) { + return drupal_render($variables['element']); +} + +/** * Implements hook_theme_suggestions_HOOK(). */ function theme_test_theme_suggestions_theme_test_suggestion_provided(array $variables) { return array('theme_test_suggestion_provided__' . 'foo'); } + +/** + * Implements hook_theme_prepare(). + */ +function theme_test_theme_prepare(&$variables, $hook) { + // Only prepare variables for a specific base theme hook. + if ($hook === 'theme_test_prepare_element') { + $variables['element']['moduleInjectedVariable1'] = array( + '#theme' => 'theme_test_prepare__element', + '#title' => 'moduleInjectedVariable1', + '#value' => __FUNCTION__, + ); + } +} + +/** + * Implements hook_theme_prepare_alter(). + */ +function theme_test_theme_prepare_alter(&$variables, $hook) { + if ($hook === 'theme_test_prepare') { + $variables['value'] .= "
\n" . __FUNCTION__; + } +} + +/** + * Implements hook_theme_prepare_THEME_HOOK__SUGGESTION(). + */ +function theme_test_theme_prepare_theme_test_prepare__implemented(&$variables) { + $variables['value'] .= "
\n" . __FUNCTION__; +} + +/** + * Implements hook_theme_prepare_THEME_HOOK__SUGGESTION_alter(). + */ +function theme_test_theme_prepare_theme_test_prepare__implemented_alter(&$variables) { + $variables['value'] .= "
\n" . __FUNCTION__; +} + +/** + * Implements hook_theme_prepare_THEME_HOOK__SUGGESTION(). + */ +function theme_test_theme_prepare_theme_test_prepare__implemented__extended(&$variables) { + $variables['value'] .= "
\n" . __FUNCTION__; +} + +/** + * Implements hook_theme_prepare_THEME_HOOK__SUGGESTION_alter(). + */ +function theme_test_theme_prepare_theme_test_prepare__implemented__extended_alter(&$variables) { + $variables['value'] .= "
\n" . __FUNCTION__; +} + +/** + * Implements hook_theme_prepare_THEME_HOOK(). + */ +function theme_test_theme_prepare_theme_test_prepare_element(&$variables) { + $variables['element']['moduleInjectedVariable2'] = array( + '#theme' => 'theme_test_prepare__element', + '#title' => 'moduleInjectedVariable2', + '#value' => __FUNCTION__, + ); +} + +/** + * Implements hook_theme_prepare_THEME_HOOK_alter(). + */ +function theme_test_theme_prepare_theme_test_prepare_element_alter(&$variables) { + $variables['element']['moduleInjectedVariable3'] = array( + '#theme' => 'theme_test_prepare__element', + '#title' => 'moduleInjectedVariable3', + '#value' => __FUNCTION__, + ); +} 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 9f08d5b..7a4bc58 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 @@ -58,30 +58,44 @@ theme_test.request_listener: requirements: _access: 'TRUE' -suggestion_alter: +theme_test.suggestion_alter: path: '/theme-test/suggestion-alter' defaults: _content: '\Drupal\theme_test\ThemeTestController::suggestionAlter' requirements: _permission: 'access content' -suggestion_provided: +theme_test.suggestion_provided: path: '/theme-test/suggestion-provided' defaults: _content: '\Drupal\theme_test\ThemeTestController::suggestionProvided' requirements: _permission: 'access content' -specific_suggestion_alter: +theme_test.specific_suggestion_alter: path: '/theme-test/specific-suggestion-alter' defaults: _content: '\Drupal\theme_test\ThemeTestController::specificSuggestionAlter' requirements: _permission: 'access content' -function_suggestion_alter: +theme_test.function_suggestion_alter: path: '/theme-test/function-suggestion-alter' defaults: _content: '\Drupal\theme_test\ThemeTestController::functionSuggestionAlter' requirements: _permission: 'access content' + +theme_test.prepare: + path: '/theme-test/prepare' + defaults: + _content: '\Drupal\theme_test\ThemeTestController::prepare' + requirements: + _permission: 'access content' + +theme_test.prepare_alter: + path: '/theme-test/prepare-subtheme' + defaults: + _content: '\Drupal\theme_test\ThemeTestController::prepareSubtheme' + requirements: + _permission: 'access content' diff --git a/core/modules/system/tests/themes/test_basetheme/test_basetheme.theme b/core/modules/system/tests/themes/test_basetheme/test_basetheme.theme new file mode 100644 index 0000000..7def511 --- /dev/null +++ b/core/modules/system/tests/themes/test_basetheme/test_basetheme.theme @@ -0,0 +1,34 @@ +\n" . __FUNCTION__; +} + +/** + * Implements hook_theme_prepare_THEME_HOOK_alter(). + */ +function test_basetheme_theme_prepare_theme_test_prepare_alter(&$variables) { + $variables['value'] .= "
\n" . __FUNCTION__; +} + +/** + * Implements hook_theme_prepare_THEME_HOOK__SUGGESTION(). + */ +function test_basetheme_theme_prepare_theme_test_prepare__implemented(&$variables) { + $variables['value'] .= "
\n" . __FUNCTION__; +} + +/** + * Implements hook_theme_prepare_THEME_HOOK__SUGGESTION_alter(). + */ +function test_basetheme_theme_prepare_theme_test_prepare__implemented_alter(&$variables) { + $variables['value'] .= "
\n" . __FUNCTION__; +} diff --git a/core/modules/system/tests/themes/test_subtheme/test_subtheme.theme b/core/modules/system/tests/themes/test_subtheme/test_subtheme.theme new file mode 100644 index 0000000..affb5fb --- /dev/null +++ b/core/modules/system/tests/themes/test_subtheme/test_subtheme.theme @@ -0,0 +1,34 @@ +\n" . __FUNCTION__; +} + +/** + * Implements hook_theme_prepare_THEME_HOOK_alter(). + */ +function test_subtheme_theme_prepare_theme_test_prepare_alter(&$variables) { + $variables['value'] .= "
\n" . __FUNCTION__; +} + +/** + * Implements hook_theme_prepare_THEME_HOOK__SUGGESTION(). + */ +function test_subtheme_theme_prepare_theme_test_prepare__implemented(&$variables) { + $variables['value'] .= "
\n" . __FUNCTION__; +} + +/** + * Implements hook_theme_prepare_THEME_HOOK__SUGGESTION_alter(). + */ +function test_subtheme_theme_prepare_theme_test_prepare__implemented_alter(&$variables) { + $variables['value'] .= "
\n" . __FUNCTION__; +} 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..acf63bd 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,70 @@ 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(&$variables, $hook) { + if ($hook === 'theme_test_prepare') { + $variables['value'] .= "
\n" . __FUNCTION__; + } + elseif ($hook === 'theme_test_prepare_element') { + $variables['element']['themeInjectedVariable1'] = array( + '#theme' => 'theme_test_prepare__element', + '#title' => 'themeInjectedVariable1', + '#value' => __FUNCTION__, + ); + } +} + +/** + * Implements hook_theme_prepare_alter(). + */ +function test_theme_theme_prepare_alter(&$variables, $hook) { + if ($hook === 'theme_test_prepare') { + $variables['value'] .= "
\n" . __FUNCTION__; + } +} + +/** + * Implements hook_theme_prepare_THEME_HOOK(). + */ +function test_theme_theme_prepare_theme_test_prepare(&$variables) { + $variables['value'] .= "
\n" . __FUNCTION__; +} + +/** + * Implements hook_theme_prepare_THEME_HOOK_alter(). + */ +function test_theme_theme_prepare_theme_test_prepare_alter(&$variables) { + $variables['value'] .= "
\n" . __FUNCTION__; +} + +/** + * Implements hook_theme_prepare_THEME_HOOK__SUGGESTION(). + */ +function test_theme_theme_prepare_theme_test_prepare__implemented(&$variables) { + $variables['value'] .= "
\n" . __FUNCTION__; +} + +/** + * Implements hook_theme_prepare_THEME_HOOK__SUGGESTION_alter(). + */ +function test_theme_theme_prepare_theme_test_prepare__implemented_alter(&$variables) { + $variables['value'] .= "
\n" . __FUNCTION__; +} + +/** + * Implements hook_theme_prepare_THEME_HOOK__SUGGESTION(). + */ +function test_theme_theme_prepare_theme_test_prepare__implemented__extended(&$variables) { + $variables['value'] .= "
\n" . __FUNCTION__; +} + +/** + * Implements hook_theme_prepare_THEME_HOOK__SUGGESTION_alter(). + */ +function test_theme_theme_prepare_theme_test_prepare__implemented__extended_alter(&$variables) { + $variables['value'] .= "
\n" . __FUNCTION__; +} diff --git a/core/modules/system/theme.api.php b/core/modules/system/theme.api.php index 34758da..b8e24fe 100644 --- a/core/modules/system/theme.api.php +++ b/core/modules/system/theme.api.php @@ -104,9 +104,15 @@ function hook_form_system_theme_settings_alter(&$form, &$form_state) { * The variables array (modify in place). * @param $hook * The name of the theme hook. + * + * @deprecated + * @see hook_theme_prepare() */ function hook_preprocess(&$variables, $hook) { - static $hooks; + // This hook has been deprecated, do not use it. Use the following instead: + // hook_theme_prepare(). + + static $hooks; // Add contextual links to the variables, if the user has permission. @@ -150,14 +156,150 @@ function hook_preprocess(&$variables, $hook) { * * @param $variables * The variables array (modify in place). + * + * @deprecated + * @see hook_theme_prepare_THEME_HOOK() */ function hook_preprocess_HOOK(&$variables) { + // This hook has been deprecated, do not use it. Use the following instead: + // hook_theme_prepare_THEME_HOOK(). + + // This example is from rdf_preprocess_image(). It adds an RDF attribute + // to the image hook's variables. + $variables['attributes']['typeof'] = array('foaf:Image'); +} + +/** + * Prepares variables for theme functions and templates. + * + * This hook allows any module or theme to prepare variables for any theme hook. + * hook_theme_prepare_THEME_HOOK() can be used to prepare variables for a + * specific theme hook. + * + * For more detailed information, see theme(). + * + * @param array $variables + * The associative array of variables, passed by reference (modify in place). + * @param string $hook + * The name of the theme hook called. + * + * @see hook_theme_prepare_alter() + * @see hook_theme_prepare_THEME_HOOK() + * @see hook_theme_prepare_THEME_HOOK_alter() + */ +function hook_theme_prepare(&$variables, $hook) { + static $hooks; + + // Add contextual links to the variables, if the user has permission. + if (!user_access('access contextual links')) { + return; + } + + if (!isset($hooks)) { + $hooks = theme_get_registry(); + } + + // Determine the primary theme function argument. + if (isset($hooks[$hook]['variables'])) { + $keys = array_keys($hooks[$hook]['variables']); + $key = $keys[0]; + } + else { + $key = $hooks[$hook]['render element']; + } + + if (isset($key) && !empty($variables[$key])) { + $element = $variables[$key]; + } + + if (isset($element) && is_array($element) && !empty($element['#contextual_links'])) { + $variables['title_suffix']['contextual_links'] = contextual_links_view($element); + if (!empty($variables['title_suffix']['contextual_links'])) { + $variables['attributes']['class'][] = 'contextual-links-region'; + } + } +} + +/** + * Alters the prepared variables used in theme functions and templates. + * + * This hook allows any module or theme to alter existing variables provided by + * earlier invocations of hook_theme_prepare() or + * hook_theme_prepare_THEME_HOOK(). This hook should only be used when it is + * needed to override or add to the $variables array for a theme hook it did not + * define. + * + * For more detailed information, see theme(). + * + * @param array $variables + * The associative array of variables, passed by reference (modify in place). + * @param string $hook + * The name of the theme hook called. + * + * @see hook_theme_prepare() + * @see hook_theme_prepare_THEME_HOOK() + * @see hook_theme_prepare_THEME_HOOK_alter() + */ +function hook_theme_prepare_alter(&$variables, $hook) { + // Replace the name of the user whom authored article nodes. + if ($hook === 'node__article') { + $variables['name'] = t('Anonymous Unicorn'); + } +} + +/** + * Prepares variables for a specific theme hook or theme hook suggestion. + * + * This hook allow any module or theme to prepare theme variables for a specific + * theme hook or theme hook suggestion. + * + * THEME_HOOK can be either the base theme hook or a more specific suggestion + * using the double-underscore ('__') notation. + * + * For more detailed information, see theme(). + * + * @param array $variables + * The associative array of variables, passed by reference (modify in place). + * + * @see rdf_preprocess_image() + * @see hook_theme_prepare_THEME_HOOK_alter() + * @see hook_theme_prepare_alter() + * @see hook_theme_prepare() + */ +function hook_theme_prepare_THEME_HOOK(&$variables) { // This example is from rdf_preprocess_image(). It adds an RDF attribute // to the image hook's variables. $variables['attributes']['typeof'] = array('foaf:Image'); } /** + * Alters prepared variables for a specific theme hook or theme hook suggestion. + * + * This hook allows any module or theme to alter existing variables provided by + * earlier invocations of hook_theme_prepare() or + * hook_theme_prepare_THEME_HOOK(). It should only be used when it is needed + * to override or add to the theme variable for a theme hook it did not define. + * + * THEME_HOOK can be either the base theme hook or a more specific suggestion + * using the double-underscore ('__') notation. + * + * For more detailed information, see theme(). + * + * @param array $variables + * The associative array of variables, passed by reference (modify in place). + * + * @see hook_theme_prepare_THEME_HOOK() + * @see hook_theme_prepare_alter() + * @see hook_theme_prepare() + */ +function hook_theme_prepare_THEME_HOOK_alter(&$variables) { + // Replace the name of the user whom authored article nodes; assuming + // THEME_HOOK was "node__article" and placed in node.module, the function name + // would read: node_theme_prepare_node__article_alter. + $variables['name'] = t('Anonymous Unicorn'); +} + +/** * Provides alternate named suggestions for a specific theme hook. * * This hook allows the module implementing hook_theme() for a theme hook to @@ -173,8 +315,8 @@ function hook_preprocess_HOOK(&$variables) { * @todo Add @code sample. * * @param array $variables - * An array of variables passed to the theme hook. Note that this hook is - * invoked before any preprocessing. + * An array of variables passed to the theme hook. NOTE: this hook is invoked + * prior to hook_theme_prepare(). Some variables may not always be available. * * @return array * An array of theme suggestions.