Commit 50723539 authored by alexpott's avatar alexpott

Issue #939462 by lauriii, Antti J. Salminen, NROTC_Webmaster, mbrett5062,...

Issue #939462 by lauriii, Antti J. Salminen, NROTC_Webmaster, mbrett5062, tim.plunkett, tostinni, rteijeiro, dvessel, cilefen, barraponto, theapi, joelpittet, Xano, tuutti, drzraf, Fabianx, markcarver, xjm, catch, Cottser, sun, jenlampton, effulgentsia, davidhernandez, kscheirer, akalata, andypost, hass, rooby, jhodgdon, fubhy, becw, MXT, mlncn, lemunet, chriscalip, mike stewart, PavanL, kevinquillen, gleroux02: Specific preprocess functions for theme hook suggestions are not invoked
parent 7a6602c8
......@@ -115,7 +115,7 @@ function drupal_theme_rebuild() {
*/
function drupal_find_theme_functions($cache, $prefixes) {
$implementations = [];
$grouped_functions = drupal_group_functions_by_prefix();
$grouped_functions = \Drupal::service('theme.registry')->getPrefixGroupedUserFunctions();
foreach ($cache as $hook => $info) {
foreach ($prefixes as $prefix) {
......@@ -161,25 +161,6 @@ function drupal_find_theme_functions($cache, $prefixes) {
return $implementations;
}
/**
* Group all user functions by word before first underscore.
*
* @return array
* Functions grouped by the first prefix.
*/
function drupal_group_functions_by_prefix() {
$functions = get_defined_functions();
$grouped_functions = [];
// Splitting user defined functions into groups by the first prefix.
foreach ($functions['user'] as $function) {
list($first_prefix,) = explode('_', $function, 2);
$grouped_functions[$first_prefix][] = $function;
}
return $grouped_functions;
}
/**
* Allows themes and/or theme engines to easily discover overridden templates.
*
......
......@@ -99,10 +99,12 @@
* before the template file is invoked to modify the variables that are passed
* to the template. 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). Modules,
* themes, and theme engines can provide these functions to modify how the
* data is preprocessed, before it is passed to the theme template:
* HOOK indicates the hook being called or a less specific hook. For example, if
* '#theme' => 'node__article' is called, hook is node__article and node. MODULE
* indicates a module name, THEME indicates a theme name, and ENGINE indicates a
* theme engine name). Modules, themes, and theme engines can provide these
* functions to modify how the data is preprocessed, before it is passed to the
* theme template:
* - template_preprocess(&$variables, $hook): Creates a default set of variables
* for all theme hooks with template implementations. Provided by Drupal Core.
* - template_preprocess_HOOK(&$variables): Should be implemented by the module
......
......@@ -342,9 +342,12 @@ protected function build() {
$this->processExtension($cache, $this->theme->getEngine(), 'theme_engine', $this->theme->getName(), $this->theme->getPath());
}
// Finally, hooks provided by the theme itself.
// Hooks provided by the theme itself.
$this->processExtension($cache, $this->theme->getName(), 'theme', $this->theme->getName(), $this->theme->getPath());
// Discover and add all preprocess functions for theme hook suggestions.
$this->postProcessExtension($cache, $this->theme);
// Let modules and themes alter the registry.
$this->moduleHandler->alter('theme_registry', $cache);
$this->themeManager->alterForTheme($this->theme, 'theme_registry', $cache);
......@@ -420,7 +423,7 @@ protected function processExtension(array &$cache, $name, $type, $theme, $path)
'base hook' => TRUE,
);
$module_list = array_keys((array) $this->moduleHandler->getModuleList());
$module_list = array_keys($this->moduleHandler->getModuleList());
// Invoke the hook_theme() implementation, preprocess what is returned, and
// merge it into $cache.
......@@ -438,6 +441,12 @@ protected function processExtension(array &$cache, $name, $type, $theme, $path)
$result[$hook]['type'] = $type;
$result[$hook]['theme path'] = $path;
// If a theme hook has a base hook, mark its preprocess functions always
// incomplete in order to inherit the base hook's preprocess functions.
if (!empty($result[$hook]['base hook'])) {
$result[$hook]['incomplete preprocess functions'] = TRUE;
}
if (isset($cache[$hook]['includes'])) {
$result[$hook]['includes'] = $cache[$hook]['includes'];
}
......@@ -563,13 +572,141 @@ protected function processExtension(array &$cache, $name, $type, $theme, $path)
$cache[$hook]['preprocess functions'][] = $name . '_preprocess_' . $hook;
$cache[$hook]['theme path'] = $path;
}
// Ensure uniqueness.
$cache[$hook]['preprocess functions'] = array_unique($cache[$hook]['preprocess functions']);
}
}
}
}
/**
* Completes the definition of the requested suggestion hook.
*
* @param string $hook
* The name of the suggestion hook to complete.
* @param array $cache
* The theme registry, as documented in
* \Drupal\Core\Theme\Registry::processExtension().
*/
protected function completeSuggestion($hook, array &$cache) {
$previous_hook = $hook;
$incomplete_previous_hook = array();
while ((!isset($cache[$previous_hook]) || isset($cache[$previous_hook]['incomplete preprocess functions']))
&& $pos = strrpos($previous_hook, '__')) {
if (isset($cache[$previous_hook]) && !$incomplete_previous_hook && isset($cache[$previous_hook]['incomplete preprocess functions'])) {
$incomplete_previous_hook = $cache[$previous_hook];
unset($incomplete_previous_hook['incomplete preprocess functions']);
}
$previous_hook = substr($previous_hook, 0, $pos);
// If base hook exists clone of it for the preprocess function
// without a template.
// @see https://www.drupal.org/node/2457295
if (isset($cache[$previous_hook]) && !isset($cache[$previous_hook]['incomplete preprocess functions'])) {
$cache[$hook] = $incomplete_previous_hook + $cache[$previous_hook];
if (isset($incomplete_previous_hook['preprocess functions'])) {
$diff = array_diff($incomplete_previous_hook['preprocess functions'], $cache[$previous_hook]['preprocess functions']);
$cache[$hook]['preprocess functions'] = array_merge($cache[$previous_hook]['preprocess functions'], $diff);
}
// If a base hook isn't set, this is the actual base hook.
if (!isset($cache[$previous_hook]['base hook'])) {
$cache[$hook]['base hook'] = $previous_hook;
}
}
}
}
/**
* Completes the theme registry adding discovered functions and hooks.
*
* @param array $cache
* The theme registry as documented in
* \Drupal\Core\Theme\Registry::processExtension().
* @param \Drupal\Core\Theme\ActiveTheme $theme
* Current active theme.
*
* @see ::processExtension()
*/
protected function postProcessExtension(array &$cache, ActiveTheme $theme) {
$grouped_functions = $this->getPrefixGroupedUserFunctions();
// Gather prefixes. This will be used to limit the found functions to the
// expected naming conventions.
$prefixes = array_keys((array) $this->moduleHandler->getModuleList());
foreach (array_reverse($theme->getBaseThemes()) as $base) {
$prefixes[] = $base->getName();
}
if ($theme->getEngine()) {
$prefixes[] = $theme->getEngine() . '_engine';
}
$prefixes[] = $theme->getName();
// Collect all variable preprocess functions in the correct order.
$suggestion_level = [];
$matches = [];
// Look for functions named according to the pattern and add them if they
// have matching hooks in the registry.
foreach ($prefixes as $prefix) {
// Grep only the functions which are within the prefix group.
list($first_prefix,) = explode('_', $prefix, 2);
if (!isset($grouped_functions[$first_prefix])) {
continue;
}
// Add the function and the name of the associated theme hook to the list
// of preprocess functions grouped by suggestion specificity if a matching
// base hook is found.
foreach ($grouped_functions[$first_prefix] as $candidate) {
if (preg_match("/^{$prefix}_preprocess_(((?:[^_]++|_(?!_))+)__.*)/", $candidate, $matches)) {
if (isset($cache[$matches[2]])) {
$level = substr_count($matches[1], '__');
$suggestion_level[$level][$candidate] = $matches[1];
}
}
}
}
// Add missing variable preprocessors. This is needed for modules that do
// not explicitly register the hook. For example, when a theme contains a
// variable preprocess function but it does not implement a template, it
// will go missing. This will add the expected function. It also allows
// modules or themes to have a variable process function based on a pattern
// even if the hook does not exist.
for ($level = 1; $level <= count($suggestion_level); $level++) {
foreach ($suggestion_level[$level] as $preprocessor => $hook) {
if (isset($cache[$hook]['preprocess functions']) && !in_array($hook, $cache[$hook]['preprocess functions'])) {
// Add missing preprocessor to existing hook.
$cache[$hook]['preprocess functions'][] = $preprocessor;
}
elseif (!isset($cache[$hook]) && strpos($hook, '__')) {
// Process non-existing hook and register it.
// Look for a previously defined hook that is either a less specific
// suggestion hook or the base hook.
$this->completeSuggestion($hook, $cache);
$cache[$hook]['preprocess functions'][] = $preprocessor;
}
}
}
// Inherit all base hook variable preprocess functions into suggestion
// hooks. This ensures that derivative hooks have a complete set of variable
// preprocess functions.
foreach ($cache as $hook => $info) {
// The 'base hook' is only applied to derivative hooks already registered
// 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']);
}
// Optimize the registry.
if (isset($cache[$hook]['preprocess functions']) && empty($cache[$hook]['preprocess functions'])) {
unset($cache[$hook]['preprocess functions']);
}
// Ensure uniqueness.
if (isset($cache[$hook]['preprocess functions'])) {
$cache[$hook]['preprocess functions'] = array_unique($cache[$hook]['preprocess functions']);
}
}
}
/**
* Invalidates theme registry caches.
*
......@@ -596,6 +733,25 @@ public function destruct() {
}
}
/**
* Gets all user functions grouped by the word before the first underscore.
*
* @return array
* Functions grouped by the first prefix.
*/
public function getPrefixGroupedUserFunctions() {
$functions = get_defined_functions();
$grouped_functions = [];
// Splitting user defined functions into groups by the first prefix.
foreach ($functions['user'] as $function) {
list($first_prefix,) = explode('_', $function, 2);
$grouped_functions[$first_prefix][] = $function;
}
return $grouped_functions;
}
/**
* Wraps drupal_get_path().
*
......
......@@ -280,12 +280,10 @@ public function render($hook, array $variables) {
include_once $this->root . '/' . $include_file;
}
}
// Replace the preprocess functions with those from the base hook.
if (isset($base_hook_info['preprocess functions'])) {
// 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'])) {
......
......@@ -99,6 +99,46 @@ public function testMultipleSubThemes() {
'template_preprocess',
'test_basetheme_preprocess_theme_test_template_test',
], $preprocess_functions);
}
/**
* Tests the theme registry with suggestions.
*/
public function testSuggestionPreprocessFunctions() {
$theme_handler = \Drupal::service('theme_handler');
$theme_handler->install(['test_theme']);
$registry_theme = new Registry(\Drupal::root(), \Drupal::cache(), \Drupal::lock(), \Drupal::moduleHandler(), $theme_handler, \Drupal::service('theme.initialization'), 'test_theme');
$registry_theme->setThemeManager(\Drupal::theme());
$suggestions = ['__kitten', '__flamingo'];
$expected_preprocess_functions = [
'template_preprocess',
'theme_test_preprocess_theme_test_preprocess_suggestions',
];
$suggestion = '';
$hook = 'theme_test_preprocess_suggestions';
do {
$hook .= "$suggestion";
$expected_preprocess_functions[] = "test_theme_preprocess_$hook";
$preprocess_functions = $registry_theme->get()[$hook]['preprocess functions'];
$this->assertIdentical($expected_preprocess_functions, $preprocess_functions, "$hook has correct preprocess functions.");
} while ($suggestion = array_shift($suggestions));
$expected_preprocess_functions = [
'template_preprocess',
'theme_test_preprocess_theme_test_preprocess_suggestions',
'test_theme_preprocess_theme_test_preprocess_suggestions',
'test_theme_preprocess_theme_test_preprocess_suggestions__kitten',
];
$preprocess_functions = $registry_theme->get()['theme_test_preprocess_suggestions__kitten__meerkat']['preprocess functions'];
$this->assertIdentical($expected_preprocess_functions, $preprocess_functions, 'Suggestion implemented as a function correctly inherits preprocess functions.');
$preprocess_functions = $registry_theme->get()['theme_test_preprocess_suggestions__kitten__bearcat']['preprocess functions'];
$this->assertIdentical($expected_preprocess_functions, $preprocess_functions, 'Suggestion implemented as a template correctly inherits preprocess functions.');
}
/**
......
......@@ -285,7 +285,7 @@ function testPreprocessHtml() {
/**
* Tests that region attributes can be manipulated via preprocess functions.
*/
function testRegionClass() {
public function testRegionClass() {
\Drupal::service('module_installer')->install(array('block', 'theme_region_test'));
// Place a block.
......@@ -295,4 +295,31 @@ function testRegionClass() {
$this->assertEqual(count($elements), 1, 'New class found.');
}
/**
* Ensures suggestion preprocess functions run for default implementations.
*
* The theme hook used by this test has its base preprocess function in a
* separate file, so this test also ensures that that file is correctly loaded
* when needed.
*/
public function testSuggestionPreprocessForDefaults() {
\Drupal::service('theme_handler')->setDefault('test_theme');
// Test with both an unprimed and primed theme registry.
drupal_theme_rebuild();
for ($i = 0; $i < 2; $i++) {
$this->drupalGet('theme-test/preprocess-suggestions');
$items = $this->cssSelect('.suggestion');
$expected_values = [
'Suggestion',
'Kitten',
'Monkey',
'Kitten',
'Flamingo',
];
foreach ($expected_values as $key => $value) {
$this->assertEqual((string) $value, $items[$key]);
}
}
}
}
......@@ -143,4 +143,25 @@ public function nonHtml() {
return new JsonResponse(['theme_initialized' => $theme_initialized]);
}
/**
* Controller for testing preprocess functions with theme suggestions.
*/
public function preprocessSuggestions() {
return [
[
'#theme' => 'theme_test_preprocess_suggestions',
'#foo' => 'suggestion',
],
[
'#theme' => 'theme_test_preprocess_suggestions',
'#foo' => 'kitten',
],
[
'#theme' => 'theme_test_preprocess_suggestions',
'#foo' => 'monkey',
],
['#theme' => 'theme_test_preprocess_suggestions__kitten__flamingo'],
];
}
}
<div class="suggestion">{{ foo }}</div>
{% if bar %}
<div class="suggestion">{{ bar }}</div>
{% endif %}
......@@ -55,6 +55,12 @@ function theme_test_theme($existing, $type, $theme, $path) {
$info['test_theme_not_existing_function'] = array(
'function' => 'test_theme_not_existing_function',
);
$items['theme_test_preprocess_suggestions'] = [
'variables' => [
'foo' => '',
'bar' => '',
],
];
return $items;
}
......@@ -89,6 +95,27 @@ function theme_theme_test_function_template_override($variables) {
return 'theme_test_function_template_override test failed.';
}
/**
* Implements hook_theme_suggestions_HOOK().
*/
function theme_test_theme_suggestions_theme_test_preprocess_suggestions($variables) {
return ['theme_test_preprocess_suggestions__' . $variables['foo']];
}
/**
* Implements hook_preprocess_HOOK().
*/
function theme_test_preprocess_theme_test_preprocess_suggestions(&$variables) {
$variables['foo'] = 'Theme hook implementor=theme_theme_test_preprocess_suggestions().';
}
/**
* Tests a module overriding a default hook with a suggestion.
*/
function theme_test_preprocess_theme_test_preprocess_suggestions__monkey(&$variables) {
$variables['foo'] = 'Monkey';
}
/**
* Prepares variables for test render element templates.
*
......
......@@ -103,3 +103,10 @@ theme_test.non_html:
_controller: '\Drupal\theme_test\ThemeTestController::nonHtml'
requirements:
_access: 'TRUE'
theme_test.preprocess_suggestions:
path: '/theme-test/preprocess-suggestions'
defaults:
_controller: '\Drupal\theme_test\ThemeTestController::preprocessSuggestions'
requirements:
_access: 'TRUE'
<div class="suggestion">{{ foo }}</div>
{% if bar %}
<div class="suggestion">{{ bar }}</div>
{% endif %}
......@@ -105,3 +105,40 @@ function test_theme_theme_test_function_suggestions__module_override($variables)
function test_theme_theme_registry_alter(&$registry) {
$registry['theme_test_template_test']['variables']['additional'] = 'value';
}
/**
* Tests a theme overriding a suggestion of a base theme hook.
*/
function test_theme_theme_test_preprocess_suggestions__kitten__meerkat($variables) {
return 'Theme hook implementor=test_theme_theme_test__suggestion(). Foo=' . $variables['foo'];
}
/**
* Tests a theme overriding a default hook with a suggestion.
*
* Implements hook_preprocess_HOOK().
*/
function test_theme_preprocess_theme_test_preprocess_suggestions(&$variables) {
$variables['foo'] = 'Theme hook implementor=test_theme_preprocess_theme_test_preprocess_suggestions().';
}
/**
* Tests a theme overriding a default hook with a suggestion.
*/
function test_theme_preprocess_theme_test_preprocess_suggestions__suggestion(&$variables) {
$variables['foo'] = 'Suggestion';
}
/**
* Tests a theme overriding a default hook with a suggestion.
*/
function test_theme_preprocess_theme_test_preprocess_suggestions__kitten(&$variables) {
$variables['foo'] = 'Kitten';
}
/**
* Tests a theme overriding a default hook with a suggestion.
*/
function test_theme_preprocess_theme_test_preprocess_suggestions__kitten__flamingo(&$variables) {
$variables['bar'] = 'Flamingo';
}
......@@ -105,6 +105,9 @@ public function testGetRegistryForModule() {
->method('getImplementations')
->with('theme')
->will($this->returnValue(array('theme_test')));
$this->moduleHandler->expects($this->atLeastOnce())
->method('getModuleList')
->willReturn([]);
$registry = $this->registry->get();
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment