Commit 7e163dbb authored by catch's avatar catch

Issue #1751194 by Cottser, mikl, effulgentsia, benjifisher: Introduce...

Issue #1751194 by Cottser, mikl, effulgentsia, benjifisher: Introduce hook_theme_suggestions_HOOK() and hook_theme_suggestions_HOOK_alter().
parent 4bdcb12b
......@@ -862,17 +862,16 @@ function drupal_find_base_themes($themes, $key, $used_keys = array()) {
* a noticeable performance penalty.
*
* @subsection sub_alternate_suggestions Suggesting Alternate Hooks
* There are two special variables that these preprocess functions can set:
* 'theme_hook_suggestion' and 'theme_hook_suggestions'. These will be merged
* together to form a list of 'suggested' alternate theme hooks to use, in
* reverse order of priority. theme_hook_suggestion will always be a higher
* priority than items in theme_hook_suggestions. theme() will use the highest
* priority implementation that exists. If none exists, theme() will use the
* implementation for the theme hook it was called with. These suggestions are
* similar to, and are used for similar reasons as, calling theme() with an
* array as the $hook parameter (see below). The difference is whether the
* suggestions are determined by the code that calls theme() or by a preprocess
* function.
* Alternate hooks can be suggested by implementing the hook-specific
* hook_theme_suggestions_HOOK_alter() or the generic
* hook_theme_suggestions_alter(). These alter hooks are used to manipulate an
* array of suggested alternate theme hooks to use, in reverse order of
* priority. theme() will use the highest priority implementation that exists.
* If none exists, theme() will use the implementation for the theme hook it was
* called with. These suggestions are similar to and are used for similar
* reasons as calling theme() with an array as the $hook parameter (see below).
* The difference is whether the suggestions are determined by the code that
* calls theme() or by altering the suggestions via the suggestion alter hooks.
*
* @param $hook
* The name of the theme hook to call. If the name contains a
......@@ -1002,11 +1001,41 @@ function theme($hook, $variables = array()) {
'theme_hook_original' => $original_hook,
);
// Invoke the variable preprocessors, if any. The preprocessors may specify
// alternate suggestions for which hook's template/function to use. If the
// hook is a suggestion of a base hook, invoke the variable preprocessors of
// the base hook, but retain the suggestion as a high priority suggestion to
// be used unless overridden by a variable preprocessor function.
// Set base hook for later use. For example if '#theme' => 'node__article'
// is called, we run hook_theme_suggestions_node_alter() rather than
// hook_theme_suggestions_node__article_alter(), and also pass in the base
// hook as the last parameter to the suggestions alter hooks.
if (isset($info['base hook'])) {
$base_theme_hook = $info['base hook'];
}
else {
$base_theme_hook = $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
// '#theme' => 'node__article', add it to the suggestions array before
// invoking suggestion alter hooks.
if (isset($info['base hook'])) {
$suggestions[] = $hook;
}
// Allow suggestions to be altered via hook_theme_suggestions_HOOK_alter().
Drupal::moduleHandler()->alter('theme_suggestions_' . $base_theme_hook, $suggestions, $variables);
// Check if each suggestion exists in the theme registry, and if so,
// use it instead of the hook that theme() was called with. For example, a
// function may call theme('node', ...), but a module can add
// 'node__article' as a suggestion via hook_theme_suggestions_HOOK_alter(),
// enabling a theme to have an alternate template file for article nodes.
foreach (array_reverse($suggestions) as $suggestion) {
if (isset($hooks[$suggestion])) {
$info = $hooks[$suggestion];
break;
}
}
// Invoke the variable preprocessors, if any.
if (isset($info['base hook'])) {
$base_hook = $info['base hook'];
$base_hook_info = $hooks[$base_hook];
......@@ -1017,44 +1046,20 @@ function theme($hook, $variables = array()) {
include_once DRUPAL_ROOT . '/' . $include_file;
}
}
// Replace the preprocess functions with those from the base hook.
if (isset($base_hook_info['preprocess functions'])) {
$variables['theme_hook_suggestion'] = $hook;
$hook = $base_hook;
$info = $base_hook_info;
// Set a variable for the 'theme_hook_suggestion'. This is used to
// maintain backwards compatibility with template engines.
$theme_hook_suggestion = $hook;
$info['preprocess functions'] = $base_hook_info['preprocess functions'];
}
}
if (isset($info['preprocess functions'])) {
$variables['theme_hook_suggestions'] = array();
foreach ($info['preprocess functions'] as $preprocessor_function) {
if (function_exists($preprocessor_function)) {
$preprocessor_function($variables, $hook, $info);
}
}
// If the preprocess functions specified hook suggestions, and the
// suggestion exists in the theme registry, use it instead of the hook that
// theme() was called with. This allows the preprocess step to route to a
// more specific theme hook. For example, a function may call
// theme('node', ...), but a preprocess function can add 'node__article' as
// a suggestion, enabling a theme to have an alternate template file for
// article nodes. Suggestions are checked in the following order:
// - The 'theme_hook_suggestion' variable is checked first. It overrides
// all others.
// - The 'theme_hook_suggestions' variable is checked in FILO order, so the
// last suggestion added to the array takes precedence over suggestions
// added earlier.
$suggestions = array();
if (!empty($variables['theme_hook_suggestions'])) {
$suggestions = $variables['theme_hook_suggestions'];
}
if (!empty($variables['theme_hook_suggestion'])) {
$suggestions[] = $variables['theme_hook_suggestion'];
}
foreach (array_reverse($suggestions) as $suggestion) {
if (isset($hooks[$suggestion])) {
$info = $hooks[$suggestion];
break;
}
}
}
// Generate the output using either a function or a template.
......@@ -1117,6 +1122,16 @@ function theme($hook, $variables = array()) {
if (isset($info['path'])) {
$template_file = $info['path'] . '/' . $template_file;
}
// Add the theme suggestions to the variables array just before rendering
// the template for backwards compatibility with template engines.
$variables['theme_hook_suggestions'] = $suggestions;
// For backwards compatibility, pass 'theme_hook_suggestion' on to the
// template engine. This is only set when calling a direct suggestion like
// '#theme' => 'menu_tree__shortcut_default' when the template exists in the
// current theme.
if (isset($theme_hook_suggestion)) {
$variables['theme_hook_suggestion'] = $theme_hook_suggestion;
}
$output = $render_function($template_file, $variables);
}
......@@ -2593,11 +2608,6 @@ function template_preprocess_html(&$variables) {
drupal_add_html_head($element, $name);
}
// Populate the page template suggestions.
if ($suggestions = theme_get_suggestions(arg(), 'html')) {
$variables['theme_hook_suggestions'] = $suggestions;
}
drupal_add_library('system', 'html5shiv', TRUE);
// Render page_top and page_bottom into top level variables.
......@@ -2706,11 +2716,6 @@ function template_preprocess_page(&$variables) {
$variables['node'] = $node;
}
// Populate the page template suggestions.
if ($suggestions = theme_get_suggestions(arg(), 'page')) {
$variables['theme_hook_suggestions'] = $suggestions;
}
// Prepare render array for messages. drupal_get_messages() is called later,
// when this variable is rendered in a theme function or template file.
$variables['messages'] = array(
......@@ -2731,9 +2736,10 @@ function template_preprocess_page(&$variables) {
/**
* Generate an array of suggestions from path arguments.
*
* This is typically called for adding to the 'theme_hook_suggestions' or
* 'attributes' class key variables from within preprocess functions, when
* wanting to base the additional suggestions on the path of the current page.
* This is typically called for adding to the suggestions in
* hook_theme_suggestions_HOOK_alter() or adding to 'attributes' class key
* variables from within preprocess functions, when wanting to base the
* additional suggestions or classes on the path of the current page.
*
* @param $args
* An array of path arguments, such as from function arg().
......@@ -2747,9 +2753,8 @@ function template_preprocess_page(&$variables) {
*
* @return
* An array of suggestions, suitable for adding to
* $variables['theme_hook_suggestions'] within a preprocess function or to
* $variables['attributes']['class'] if the suggestions represent extra CSS
* classes.
* hook_theme_suggestions_HOOK_alter() or to $variables['attributes']['class']
* if the suggestions represent extra CSS classes.
*/
function theme_get_suggestions($args, $base, $delimiter = '__') {
......@@ -2923,12 +2928,6 @@ function template_preprocess_maintenance_page(&$variables) {
$variables['attributes']['class'][] = 'sidebar-' . $variables['layout'];
}
// Dead databases will show error messages so supplying this template will
// allow themers to override the page and the content completely.
if (isset($variables['db_is_active']) && !$variables['db_is_active']) {
$variables['theme_hook_suggestion'] = 'maintenance_page__offline';
}
$variables['head'] = drupal_get_html_head();
// While this code is used in the installer, the language module may not be
......@@ -2989,7 +2988,6 @@ function template_preprocess_region(&$variables) {
$variables['attributes']['class'][] = 'region';
$variables['attributes']['class'][] = drupal_html_class('region-' . $variables['region']);
$variables['theme_hook_suggestions'][] = 'region__' . $variables['region'];
}
/**
......
......@@ -24,6 +24,11 @@ public function getImplementations($hook) {
if (substr($hook, -6) === '_alter') {
return array();
}
// theme() is called during updates and fires hooks, so whitelist the
// system module.
if (substr($hook, 0, 6) == 'theme_') {
return array('system');
}
switch ($hook) {
// hook_requirements is necessary for updates to work.
case 'requirements':
......
......@@ -484,6 +484,39 @@ function block_rebuild() {
}
}
/**
* Implements hook_theme_suggestions_HOOK().
*/
function block_theme_suggestions_block(array $variables) {
$suggestions = array();
$suggestions[] = 'block__' . $variables['elements']['#configuration']['module'];
// Hyphens (-) and underscores (_) play a special role in theme suggestions.
// Theme suggestions should only contain underscores, because within
// drupal_find_theme_templates(), underscores are converted to hyphens to
// match template file names, and then converted back to underscores to match
// pre-processing and other function names. So if your theme suggestion
// contains a hyphen, it will end up as an underscore after this conversion,
// and your function names won't be recognized. So, we need to convert
// hyphens to underscores in block deltas for the theme suggestions.
// We can safely explode on : because we know the Block plugin type manager
// enforces that delimiter for all derivatives.
$parts = explode(':', $variables['elements']['#plugin_id']);
$suggestion = 'block';
while ($part = array_shift($parts)) {
$suggestions[] = $suggestion .= '__' . strtr($part, '-', '_');
}
if ($id = $variables['elements']['#block']->id()) {
$config_id = explode('.', $id);
$machine_name = array_pop($config_id);
$suggestions[] = 'block__' . $machine_name;
}
return $suggestions;
}
/**
* Prepares variables for block templates.
*
......@@ -527,29 +560,11 @@ function template_preprocess_block(&$variables) {
// Add default class for block content.
$variables['content_attributes']['class'][] = 'content';
$variables['theme_hook_suggestions'][] = 'block__' . $variables['configuration']['module'];
// Hyphens (-) and underscores (_) play a special role in theme suggestions.
// Theme suggestions should only contain underscores, because within
// drupal_find_theme_templates(), underscores are converted to hyphens to
// match template file names, and then converted back to underscores to match
// pre-processing and other function names. So if your theme suggestion
// contains a hyphen, it will end up as an underscore after this conversion,
// and your function names won't be recognized. So, we need to convert
// hyphens to underscores in block deltas for the theme suggestions.
// We can safely explode on : because we know the Block plugin type manager
// enforces that delimiter for all derivatives.
$parts = explode(':', $variables['plugin_id']);
$suggestion = 'block';
while ($part = array_shift($parts)) {
$variables['theme_hook_suggestions'][] = $suggestion .= '__' . strtr($part, '-', '_');
}
// Create a valid HTML ID and make sure it is unique.
if ($id = $variables['elements']['#block']->id()) {
$config_id = explode('.', $id);
$machine_name = array_pop($config_id);
$variables['attributes']['id'] = drupal_html_id('block-' . $machine_name);
$variables['theme_hook_suggestions'][] = 'block__' . $machine_name;
}
}
......
<?php
/**
* @file
* Contains \Drupal\block\Tests\BlockPreprocessUnitTest.
*/
namespace Drupal\block\Tests;
use Drupal\simpletest\WebTestBase;
/**
* Unit tests for template_preprocess_block().
*/
class BlockPreprocessUnitTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('block');
public static function getInfo() {
return array(
'name' => 'Block preprocess',
'description' => 'Test the template_preprocess_block() function.',
'group' => 'Block',
);
}
/**
* Tests block classes with template_preprocess_block().
*/
function testBlockClasses() {
// Define a block with a derivative to be preprocessed, which includes both
// an underscore (not transformed) and a hyphen (transformed to underscore),
// and generates possibilities for each level of derivative.
// @todo Clarify this comment.
$block = entity_create('block', array(
'plugin' => 'system_menu_block:admin',
'region' => 'footer',
'id' => \Drupal::config('system.theme')->get('default') . '.machinename',
));
$variables = array();
$variables['elements']['#block'] = $block;
$variables['elements']['#configuration'] = $block->getPlugin()->getConfiguration();
$variables['elements']['#plugin_id'] = $block->get('plugin');
$variables['elements']['content'] = array();
// Test adding a class to the block content.
$variables['content_attributes']['class'][] = 'test-class';
template_preprocess_block($variables);
$this->assertEqual($variables['content_attributes']['class'], array('test-class', 'content'), 'Default .content class added to block content_attributes');
}
}
......@@ -10,7 +10,7 @@
use Drupal\simpletest\WebTestBase;
/**
* Unit tests for template_preprocess_block().
* Unit tests for block_theme_suggestions_block().
*/
class BlockTemplateSuggestionsUnitTest extends WebTestBase {
......@@ -24,13 +24,13 @@ class BlockTemplateSuggestionsUnitTest extends WebTestBase {
public static function getInfo() {
return array(
'name' => 'Block template suggestions',
'description' => 'Test the template_preprocess_block() function.',
'description' => 'Test the block_theme_suggestions_block() function.',
'group' => 'Block',
);
}
/**
* Test if template_preprocess_block() handles the suggestions right.
* Tests template suggestions from block_theme_suggestions_block().
*/
function testBlockThemeHookSuggestions() {
// Define a block with a derivative to be preprocessed, which includes both
......@@ -48,11 +48,8 @@ function testBlockThemeHookSuggestions() {
$variables['elements']['#configuration'] = $block->getPlugin()->getConfiguration();
$variables['elements']['#plugin_id'] = $block->get('plugin');
$variables['elements']['content'] = array();
// Test adding a class to the block content.
$variables['content_attributes']['class'][] = 'test-class';
template_preprocess_block($variables);
$this->assertEqual($variables['theme_hook_suggestions'], array('block__system', 'block__system_menu_block', 'block__system_menu_block__admin', 'block__machinename'));
$this->assertEqual($variables['content_attributes']['class'], array('test-class', 'content'), 'Default .content class added to block content_attributes');
$suggestions = block_theme_suggestions_block($variables);
$this->assertEqual($suggestions, array('block__system', 'block__system_menu_block', 'block__system_menu_block__admin', 'block__machinename'));
}
}
......@@ -659,6 +659,22 @@ function field_page_build(&$page) {
$page['#attached']['css'][$path . '/css/field.module.css'] = array('every_page' => TRUE);
}
/**
* Implements hook_theme_suggestions_HOOK().
*/
function field_theme_suggestions_field(array $variables) {
$suggestions = array();
$element = $variables['element'];
$suggestions[] = 'field__' . $element['#field_type'];
$suggestions[] = 'field__' . $element['#field_name'];
$suggestions[] = 'field__' . $element['#entity_type'] . '__' . $element['#bundle'];
$suggestions[] = 'field__' . $element['#entity_type'] . '__' . $element['#field_name'];
$suggestions[] = 'field__' . $element['#entity_type'] . '__' . $element['#field_name'] . '__' . $element['#bundle'];
return $suggestions;
}
/**
* Prepares variables for field templates.
*
......@@ -716,15 +732,6 @@ function template_preprocess_field(&$variables, $hook) {
$variables['attributes']['class'][] = 'clearfix';
}
// Add specific suggestions that can override the default implementation.
$variables['theme_hook_suggestions'] = array(
'field__' . $element['#field_type'],
'field__' . $element['#field_name'],
'field__' . $element['#entity_type'] . '__' . $element['#bundle'],
'field__' . $element['#entity_type'] . '__' . $element['#field_name'],
'field__' . $element['#entity_type'] . '__' . $element['#field_name'] . '__' . $element['#bundle'],
);
static $default_attributes;
if (!isset($default_attributes)) {
$default_attributes = new Attribute;
......
......@@ -588,6 +588,33 @@ function forum_preprocess_block(&$variables) {
}
}
/**
* Implements hook_theme_suggestions_HOOK().
*/
function forum_theme_suggestions_forums(array $variables) {
$suggestions = array();
$tid = $variables['term']->id();
// Provide separate template suggestions based on what's being output. Topic
// ID is also accounted for. Check both variables to be safe then the inverse.
// Forums with topic IDs take precedence.
if ($variables['forums'] && !$variables['topics']) {
$suggestions[] = 'forums__containers';
$suggestions[] = 'forums__' . $tid;
$suggestions[] = 'forums__containers__' . $tid;
}
elseif (!$variables['forums'] && $variables['topics']) {
$suggestions[] = 'forums__topics';
$suggestions[] = 'forums__' . $tid;
$suggestions[] = 'forums__topics__' . $tid;
}
else {
$suggestions[] = 'forums__' . $tid;
}
return $suggestions;
}
/**
* Prepares variables for forums templates.
*
......@@ -635,23 +662,6 @@ function template_preprocess_forums(&$variables) {
else {
$variables['topics'] = array();
}
// Provide separate template suggestions based on what's being output. Topic id is also accounted for.
// Check both variables to be safe then the inverse. Forums with topic ID's take precedence.
if ($variables['forums'] && !$variables['topics']) {
$variables['theme_hook_suggestions'][] = 'forums__containers';
$variables['theme_hook_suggestions'][] = 'forums__' . $variables['tid'];
$variables['theme_hook_suggestions'][] = 'forums__containers__' . $variables['tid'];
}
elseif (!$variables['forums'] && $variables['topics']) {
$variables['theme_hook_suggestions'][] = 'forums__topics';
$variables['theme_hook_suggestions'][] = 'forums__' . $variables['tid'];
$variables['theme_hook_suggestions'][] = 'forums__topics__' . $variables['tid'];
}
else {
$variables['theme_hook_suggestions'][] = 'forums__' . $variables['tid'];
}
}
else {
$variables['forums'] = array();
......
......@@ -641,6 +641,19 @@ function node_preprocess_block(&$variables) {
}
}
/**
* Implements hook_theme_suggestions_HOOK().
*/
function node_theme_suggestions_node(array $variables) {
$suggestions = array();
$node = $variables['elements']['#node'];
$suggestions[] = 'node__' . $node->bundle();
$suggestions[] = 'node__' . $node->id();
return $suggestions;
}
/**
* Prepares variables for node templates.
*
......@@ -730,11 +743,6 @@ function template_preprocess_node(&$variables) {
if (isset($variables['preview'])) {
$variables['attributes']['class'][] = 'preview';
}
// Clean up name so there are no underscores.
$variables['theme_hook_suggestions'][] = 'node__' . $node->bundle();
$variables['theme_hook_suggestions'][] = 'node__' . $node->id();
$variables['content_attributes']['class'][] = 'content';
}
......
......@@ -74,6 +74,13 @@ function search_view($plugin_id = NULL, $keys = '') {
return $build;
}
/**
* Implements hook_theme_suggestions_HOOK().
*/
function search_theme_suggestions_search_results(array $variables) {
return array('search_results__' . $variables['plugin_id']);
}
/**
* Prepares variables for search results templates.
*
......@@ -100,7 +107,13 @@ function template_preprocess_search_results(&$variables) {
// @todo Revisit where this help text is added, see also
// http://drupal.org/node/1918856.
$variables['help'] = search_help('search#noresults', drupal_help_arg());
$variables['theme_hook_suggestions'][] = 'search_results__' . $variables['plugin_id'];
}
/**
* Implements hook_theme_suggestions_HOOK().
*/
function search_theme_suggestions_search_result(array $variables) {
return array('search_result__' . $variables['plugin_id']);
}
/**
......@@ -148,7 +161,6 @@ function template_preprocess_search_result(&$variables) {
// Provide separated and grouped meta information..
$variables['info_split'] = $info;
$variables['info'] = implode(' - ', $info);
$variables['theme_hook_suggestions'][] = 'search_result__' . $variables['plugin_id'];
}
/**
......
<?php
/**
* @file
* Contains \Drupal\system\Tests\Theme\ThemeSuggestionsAlterTest.
*/
namespace Drupal\system\Tests\Theme;
use Drupal\simpletest\WebTestBase;
/**
* Tests theme suggestion alter hooks.
*/
class ThemeSuggestionsAlterTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('theme_test');
public static function getInfo() {
return array(
'name' => 'Theme suggestions alter',
'description' => 'Test theme suggestion alter hooks.',
'group' => 'Theme',
);
}
function setUp() {
parent::setUp();
theme_enable(array('test_theme'));
}
/**
* Tests that hooks to provide theme suggestions work.
*/
function testTemplateSuggestions() {
$this->drupalGet('theme-test/suggestion-provided');
$this->assertText('Template for testing suggestions provided by the module declaring the theme hook.');
// Enable test_theme, it contains a template suggested by theme_test.module
// in theme_test_theme_suggestions_theme_test_suggestion_provided().
config('system.theme')
->set('default', 'test_theme')
->save();
$this->drupalGet('theme-test/suggestion-provided');
$this->assertText('Template overridden based on suggestion provided by the module declaring the theme hook.');
}
/**
* Tests that theme suggestion alter hooks work for templates.
*/
function testTemplateSuggestionsAlter() {
$this->drupalGet('theme-test/suggestion-alter');
$this->assertText('Original template.');
// Enable test_theme and test that themes can alter template suggestions.
config('system.theme')
->set('default', 'test_theme')
->save();
$this->drupalGet('theme-test/suggestion-alter');
$this->assertText('Template overridden based on new theme suggestion provided by the test_theme theme.');
// Enable the theme_suggestions_test module to test modules implementing
// suggestions alter hooks.
\Drupal::moduleHandler()->install(array('theme_suggestions_test'));
$this->drupalGet('theme-test/suggestion-alter');
$this->assertText('Template overridden based on new theme suggestion provided by a module.');
}