Mercurial > hg > cmmr2012-drupal-site
comparison core/lib/Drupal/Core/Theme/Registry.php @ 0:c75dbcec494b
Initial commit from drush-created site
author | Chris Cannam |
---|---|
date | Thu, 05 Jul 2018 14:24:15 +0000 |
parents | |
children | 12f9dff5fda9 |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:c75dbcec494b |
---|---|
1 <?php | |
2 | |
3 namespace Drupal\Core\Theme; | |
4 | |
5 use Drupal\Component\Utility\NestedArray; | |
6 use Drupal\Core\Cache\Cache; | |
7 use Drupal\Core\Cache\CacheBackendInterface; | |
8 use Drupal\Core\DestructableInterface; | |
9 use Drupal\Core\Extension\ModuleHandlerInterface; | |
10 use Drupal\Core\Extension\ThemeHandlerInterface; | |
11 use Drupal\Core\Lock\LockBackendInterface; | |
12 use Drupal\Core\Utility\ThemeRegistry; | |
13 | |
14 /** | |
15 * Defines the theme registry service. | |
16 * | |
17 * @internal | |
18 * | |
19 * Theme registry is expected to be used only internally since every | |
20 * hook_theme() implementation depends on the way this class is built. This | |
21 * class may get new features in minor releases so this class should be | |
22 * considered internal. | |
23 * | |
24 * @todo Replace local $registry variables in methods with $this->registry. | |
25 */ | |
26 class Registry implements DestructableInterface { | |
27 | |
28 /** | |
29 * The theme object representing the active theme for this registry. | |
30 * | |
31 * @var \Drupal\Core\Theme\ActiveTheme | |
32 */ | |
33 protected $theme; | |
34 | |
35 /** | |
36 * The lock backend that should be used. | |
37 * | |
38 * @var \Drupal\Core\Lock\LockBackendInterface | |
39 */ | |
40 protected $lock; | |
41 | |
42 /** | |
43 * The complete theme registry. | |
44 * | |
45 * @var array | |
46 * An array of theme registries, keyed by the theme name. Each registry is | |
47 * an associative array keyed by theme hook names, whose values are | |
48 * associative arrays containing the aggregated hook definition: | |
49 * - type: The type of the extension the original theme hook originates | |
50 * from; e.g., 'module' for theme hook 'node' of Node module. | |
51 * - name: The name of the extension the original theme hook originates | |
52 * from; e.g., 'node' for theme hook 'node' of Node module. | |
53 * - theme path: The effective \Drupal\Core\Theme\ActiveTheme::getPath() | |
54 * during \Drupal\Core\Theme\ThemeManagerInterface::render(), available | |
55 * as 'directory' variable in templates. For functions, it should point | |
56 * to the respective theme. For templates, it should point to the | |
57 * directory that contains the template. | |
58 * - includes: (optional) An array of include files to load when the theme | |
59 * hook is executed by \Drupal\Core\Theme\ThemeManagerInterface::render(). | |
60 * - file: (optional) A filename to add to 'includes', either prefixed with | |
61 * the value of 'path', or the path of the extension implementing | |
62 * hook_theme(). | |
63 * In case of a theme base hook, one of the following: | |
64 * - variables: An associative array whose keys are variable names and whose | |
65 * values are default values of the variables to use for this theme hook. | |
66 * - render element: A string denoting the name of the variable name, in | |
67 * which the render element for this theme hook is provided. | |
68 * In case of a theme template file: | |
69 * - path: The path to the template file to use. Defaults to the | |
70 * subdirectory 'templates' of the path of the extension implementing | |
71 * hook_theme(); e.g., 'core/modules/node/templates' for Node module. | |
72 * - template: The basename of the template file to use, without extension | |
73 * (as the extension is specific to the theme engine). The template file | |
74 * is in the directory defined by 'path'. | |
75 * - template_file: A full path and file name to a template file to use. | |
76 * Allows any extension to override the effective template file. | |
77 * - engine: The theme engine to use for the template file. | |
78 * In case of a theme function: | |
79 * - function: The function name to call to generate the output. | |
80 * For any registered theme hook, including theme hook suggestions: | |
81 * - preprocess: An array of theme variable preprocess callbacks to invoke | |
82 * before invoking final theme variable processors. | |
83 * - process: An array of theme variable process callbacks to invoke | |
84 * before invoking the actual theme function or template. | |
85 */ | |
86 protected $registry = []; | |
87 | |
88 /** | |
89 * The cache backend to use for the complete theme registry data. | |
90 * | |
91 * @var \Drupal\Core\Cache\CacheBackendInterface | |
92 */ | |
93 protected $cache; | |
94 | |
95 /** | |
96 * The module handler to use to load modules. | |
97 * | |
98 * @var \Drupal\Core\Extension\ModuleHandlerInterface | |
99 */ | |
100 protected $moduleHandler; | |
101 | |
102 /** | |
103 * An array of incomplete, runtime theme registries, keyed by theme name. | |
104 * | |
105 * @var \Drupal\Core\Utility\ThemeRegistry[] | |
106 */ | |
107 protected $runtimeRegistry = []; | |
108 | |
109 /** | |
110 * Stores whether the registry was already initialized. | |
111 * | |
112 * @var bool | |
113 */ | |
114 protected $initialized = FALSE; | |
115 | |
116 /** | |
117 * The name of the theme for which to construct the registry, if given. | |
118 * | |
119 * @var string|null | |
120 */ | |
121 protected $themeName; | |
122 | |
123 /** | |
124 * The app root. | |
125 * | |
126 * @var string | |
127 */ | |
128 protected $root; | |
129 | |
130 /** | |
131 * The theme handler. | |
132 * | |
133 * @var \Drupal\Core\Extension\ThemeHandlerInterface | |
134 */ | |
135 protected $themeHandler; | |
136 | |
137 /** | |
138 * The theme manager. | |
139 * | |
140 * @var \Drupal\Core\Theme\ThemeManagerInterface | |
141 */ | |
142 protected $themeManager; | |
143 | |
144 /** | |
145 * The runtime cache. | |
146 * | |
147 * @var \Drupal\Core\Cache\CacheBackendInterface | |
148 */ | |
149 protected $runtimeCache; | |
150 | |
151 /** | |
152 * Constructs a \Drupal\Core\Theme\Registry object. | |
153 * | |
154 * @param string $root | |
155 * The app root. | |
156 * @param \Drupal\Core\Cache\CacheBackendInterface $cache | |
157 * The cache backend interface to use for the complete theme registry data. | |
158 * @param \Drupal\Core\Lock\LockBackendInterface $lock | |
159 * The lock backend. | |
160 * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler | |
161 * The module handler to use to load modules. | |
162 * @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler | |
163 * The theme handler. | |
164 * @param \Drupal\Core\Theme\ThemeInitializationInterface $theme_initialization | |
165 * The theme initialization. | |
166 * @param string $theme_name | |
167 * (optional) The name of the theme for which to construct the registry. | |
168 * @param \Drupal\Core\Cache\CacheBackendInterface $runtime_cache | |
169 * The cache backend interface to use for the runtime theme registry data. | |
170 */ | |
171 public function __construct($root, CacheBackendInterface $cache, LockBackendInterface $lock, ModuleHandlerInterface $module_handler, ThemeHandlerInterface $theme_handler, ThemeInitializationInterface $theme_initialization, $theme_name = NULL, CacheBackendInterface $runtime_cache = NULL) { | |
172 $this->root = $root; | |
173 $this->cache = $cache; | |
174 $this->lock = $lock; | |
175 $this->moduleHandler = $module_handler; | |
176 $this->themeName = $theme_name; | |
177 $this->themeHandler = $theme_handler; | |
178 $this->themeInitialization = $theme_initialization; | |
179 $this->runtimeCache = $runtime_cache; | |
180 } | |
181 | |
182 /** | |
183 * Sets the theme manager. | |
184 * | |
185 * @param \Drupal\Core\Theme\ThemeManagerInterface $theme_manager | |
186 * The theme manager. | |
187 */ | |
188 public function setThemeManager(ThemeManagerInterface $theme_manager) { | |
189 $this->themeManager = $theme_manager; | |
190 } | |
191 | |
192 /** | |
193 * Initializes a theme with a certain name. | |
194 * | |
195 * This function does to much magic, so it should be replaced by another | |
196 * services which holds the current active theme information. | |
197 * | |
198 * @param string $theme_name | |
199 * (optional) The name of the theme for which to construct the registry. | |
200 */ | |
201 protected function init($theme_name = NULL) { | |
202 if ($this->initialized) { | |
203 return; | |
204 } | |
205 // Unless instantiated for a specific theme, use globals. | |
206 if (!isset($theme_name)) { | |
207 $this->theme = $this->themeManager->getActiveTheme(); | |
208 } | |
209 // Instead of the active theme, a specific theme was requested. | |
210 else { | |
211 $this->theme = $this->themeInitialization->getActiveThemeByName($theme_name); | |
212 $this->themeInitialization->loadActiveTheme($this->theme); | |
213 } | |
214 } | |
215 | |
216 /** | |
217 * Returns the complete theme registry from cache or rebuilds it. | |
218 * | |
219 * @return array | |
220 * The complete theme registry data array. | |
221 * | |
222 * @see Registry::$registry | |
223 */ | |
224 public function get() { | |
225 $this->init($this->themeName); | |
226 if (isset($this->registry[$this->theme->getName()])) { | |
227 return $this->registry[$this->theme->getName()]; | |
228 } | |
229 if ($cache = $this->cache->get('theme_registry:' . $this->theme->getName())) { | |
230 $this->registry[$this->theme->getName()] = $cache->data; | |
231 } | |
232 else { | |
233 $this->build(); | |
234 // Only persist it if all modules are loaded to ensure it is complete. | |
235 if ($this->moduleHandler->isLoaded()) { | |
236 $this->setCache(); | |
237 } | |
238 } | |
239 return $this->registry[$this->theme->getName()]; | |
240 } | |
241 | |
242 /** | |
243 * Returns the incomplete, runtime theme registry. | |
244 * | |
245 * @return \Drupal\Core\Utility\ThemeRegistry | |
246 * A shared instance of the ThemeRegistry class, provides an ArrayObject | |
247 * that allows it to be accessed with array syntax and isset(), and is more | |
248 * lightweight than the full registry. | |
249 */ | |
250 public function getRuntime() { | |
251 $this->init($this->themeName); | |
252 if (!isset($this->runtimeRegistry[$this->theme->getName()])) { | |
253 $this->runtimeRegistry[$this->theme->getName()] = new ThemeRegistry('theme_registry:runtime:' . $this->theme->getName(), $this->runtimeCache ?: $this->cache, $this->lock, ['theme_registry'], $this->moduleHandler->isLoaded()); | |
254 } | |
255 return $this->runtimeRegistry[$this->theme->getName()]; | |
256 } | |
257 | |
258 /** | |
259 * Persists the theme registry in the cache backend. | |
260 */ | |
261 protected function setCache() { | |
262 $this->cache->set('theme_registry:' . $this->theme->getName(), $this->registry[$this->theme->getName()], Cache::PERMANENT, ['theme_registry']); | |
263 } | |
264 | |
265 /** | |
266 * Returns the base hook for a given hook suggestion. | |
267 * | |
268 * @param string $hook | |
269 * The name of a theme hook whose base hook to find. | |
270 * | |
271 * @return string|false | |
272 * The name of the base hook or FALSE. | |
273 */ | |
274 public function getBaseHook($hook) { | |
275 $this->init($this->themeName); | |
276 $base_hook = $hook; | |
277 // Iteratively strip everything after the last '__' delimiter, until a | |
278 // base hook definition is found. Recursive base hooks of base hooks are | |
279 // not supported, so the base hook must be an original implementation that | |
280 // points to a theme function or template. | |
281 while ($pos = strrpos($base_hook, '__')) { | |
282 $base_hook = substr($base_hook, 0, $pos); | |
283 if (isset($this->registry[$base_hook]['exists'])) { | |
284 break; | |
285 } | |
286 } | |
287 if ($pos !== FALSE && $base_hook !== $hook) { | |
288 return $base_hook; | |
289 } | |
290 return FALSE; | |
291 } | |
292 | |
293 /** | |
294 * Builds the theme registry cache. | |
295 * | |
296 * Theme hook definitions are collected in the following order: | |
297 * - Modules | |
298 * - Base theme engines | |
299 * - Base themes | |
300 * - Theme engine | |
301 * - Theme | |
302 * | |
303 * All theme hook definitions are essentially just collated and merged in the | |
304 * above order. However, various extension-specific default values and | |
305 * customizations are required; e.g., to record the effective file path for | |
306 * theme template. Therefore, this method first collects all extensions per | |
307 * type, and then dispatches the processing for each extension to | |
308 * processExtension(). | |
309 * | |
310 * After completing the collection, modules are allowed to alter it. Lastly, | |
311 * any derived and incomplete theme hook definitions that are hook suggestions | |
312 * for base hooks (e.g., 'block__node' for the base hook 'block') need to be | |
313 * determined based on the full registry and classified as 'base hook'. | |
314 * | |
315 * See the @link themeable Default theme implementations topic @endlink for | |
316 * details. | |
317 * | |
318 * @return \Drupal\Core\Utility\ThemeRegistry | |
319 * The build theme registry. | |
320 * | |
321 * @see hook_theme_registry_alter() | |
322 */ | |
323 protected function build() { | |
324 $cache = []; | |
325 // First, preprocess the theme hooks advertised by modules. This will | |
326 // serve as the basic registry. Since the list of enabled modules is the | |
327 // same regardless of the theme used, this is cached in its own entry to | |
328 // save building it for every theme. | |
329 if ($cached = $this->cache->get('theme_registry:build:modules')) { | |
330 $cache = $cached->data; | |
331 } | |
332 else { | |
333 foreach ($this->moduleHandler->getImplementations('theme') as $module) { | |
334 $this->processExtension($cache, $module, 'module', $module, $this->getPath($module)); | |
335 } | |
336 // Only cache this registry if all modules are loaded. | |
337 if ($this->moduleHandler->isLoaded()) { | |
338 $this->cache->set("theme_registry:build:modules", $cache, Cache::PERMANENT, ['theme_registry']); | |
339 } | |
340 } | |
341 | |
342 // Process each base theme. | |
343 // Ensure that we start with the root of the parents, so that both CSS files | |
344 // and preprocess functions comes first. | |
345 foreach (array_reverse($this->theme->getBaseThemes()) as $base) { | |
346 // If the base theme uses a theme engine, process its hooks. | |
347 $base_path = $base->getPath(); | |
348 if ($this->theme->getEngine()) { | |
349 $this->processExtension($cache, $this->theme->getEngine(), 'base_theme_engine', $base->getName(), $base_path); | |
350 } | |
351 $this->processExtension($cache, $base->getName(), 'base_theme', $base->getName(), $base_path); | |
352 } | |
353 | |
354 // And then the same thing, but for the theme. | |
355 if ($this->theme->getEngine()) { | |
356 $this->processExtension($cache, $this->theme->getEngine(), 'theme_engine', $this->theme->getName(), $this->theme->getPath()); | |
357 } | |
358 | |
359 // Hooks provided by the theme itself. | |
360 $this->processExtension($cache, $this->theme->getName(), 'theme', $this->theme->getName(), $this->theme->getPath()); | |
361 | |
362 // Discover and add all preprocess functions for theme hook suggestions. | |
363 $this->postProcessExtension($cache, $this->theme); | |
364 | |
365 // Let modules and themes alter the registry. | |
366 $this->moduleHandler->alter('theme_registry', $cache); | |
367 $this->themeManager->alterForTheme($this->theme, 'theme_registry', $cache); | |
368 | |
369 // @todo Implement more reduction of the theme registry entry. | |
370 // Optimize the registry to not have empty arrays for functions. | |
371 foreach ($cache as $hook => $info) { | |
372 if (empty($info['preprocess functions'])) { | |
373 unset($cache[$hook]['preprocess functions']); | |
374 } | |
375 } | |
376 $this->registry[$this->theme->getName()] = $cache; | |
377 | |
378 return $this->registry[$this->theme->getName()]; | |
379 } | |
380 | |
381 /** | |
382 * Process a single implementation of hook_theme(). | |
383 * | |
384 * @param array $cache | |
385 * The theme registry that will eventually be cached; It is an associative | |
386 * array keyed by theme hooks, whose values are associative arrays | |
387 * describing the hook: | |
388 * - 'type': The passed-in $type. | |
389 * - 'theme path': The passed-in $path. | |
390 * - 'function': The name of the function generating output for this theme | |
391 * hook. Either defined explicitly in hook_theme() or, if neither | |
392 * 'function' nor 'template' is defined, then the default theme function | |
393 * name is used. The default theme function name is the theme hook | |
394 * prefixed by either 'theme_' for modules or '$name_' for everything | |
395 * else. If 'function' is defined, 'template' is not used. | |
396 * - 'template': The filename of the template generating output for this | |
397 * theme hook. The template is in the directory defined by the 'path' key | |
398 * of hook_theme() or defaults to "$path/templates". | |
399 * - 'variables': The variables for this theme hook as defined in | |
400 * hook_theme(). If there is more than one implementation and 'variables' | |
401 * is not specified in a later one, then the previous definition is kept. | |
402 * - 'render element': The renderable element for this theme hook as defined | |
403 * in hook_theme(). If there is more than one implementation and | |
404 * 'render element' is not specified in a later one, then the previous | |
405 * definition is kept. | |
406 * - See the @link themeable Theme system overview topic @endlink for | |
407 * detailed documentation. | |
408 * @param string $name | |
409 * The name of the module, theme engine, base theme engine, theme or base | |
410 * theme implementing hook_theme(). | |
411 * @param string $type | |
412 * One of 'module', 'theme_engine', 'base_theme_engine', 'theme', or | |
413 * 'base_theme'. Unlike regular hooks that can only be implemented by | |
414 * modules, each of these can implement hook_theme(). This function is | |
415 * called in aforementioned order and new entries override older ones. For | |
416 * example, if a theme hook is both defined by a module and a theme, then | |
417 * the definition in the theme will be used. | |
418 * @param string $theme | |
419 * The actual name of theme, module, etc. that is being processed. | |
420 * @param string $path | |
421 * The directory where $name is. For example, modules/system or | |
422 * themes/bartik. | |
423 * | |
424 * @see \Drupal\Core\Theme\ThemeManagerInterface::render() | |
425 * @see hook_theme() | |
426 * @see \Drupal\Core\Extension\ThemeHandler::listInfo() | |
427 * @see twig_render_template() | |
428 * | |
429 * @throws \BadFunctionCallException | |
430 */ | |
431 protected function processExtension(array &$cache, $name, $type, $theme, $path) { | |
432 $result = []; | |
433 | |
434 $hook_defaults = [ | |
435 'variables' => TRUE, | |
436 'render element' => TRUE, | |
437 'pattern' => TRUE, | |
438 'base hook' => TRUE, | |
439 ]; | |
440 | |
441 $module_list = array_keys($this->moduleHandler->getModuleList()); | |
442 | |
443 // Invoke the hook_theme() implementation, preprocess what is returned, and | |
444 // merge it into $cache. | |
445 $function = $name . '_theme'; | |
446 if (function_exists($function)) { | |
447 $result = $function($cache, $type, $theme, $path); | |
448 foreach ($result as $hook => $info) { | |
449 // When a theme or engine overrides a module's theme function | |
450 // $result[$hook] will only contain key/value pairs for information being | |
451 // overridden. Pull the rest of the information from what was defined by | |
452 // an earlier hook. | |
453 | |
454 // Fill in the type and path of the module, theme, or engine that | |
455 // implements this theme function. | |
456 $result[$hook]['type'] = $type; | |
457 $result[$hook]['theme path'] = $path; | |
458 | |
459 // If a theme hook has a base hook, mark its preprocess functions always | |
460 // incomplete in order to inherit the base hook's preprocess functions. | |
461 if (!empty($result[$hook]['base hook'])) { | |
462 $result[$hook]['incomplete preprocess functions'] = TRUE; | |
463 } | |
464 | |
465 if (isset($cache[$hook]['includes'])) { | |
466 $result[$hook]['includes'] = $cache[$hook]['includes']; | |
467 } | |
468 | |
469 // Load the includes, as they may contain preprocess functions. | |
470 if (isset($info['includes'])) { | |
471 foreach ($info['includes'] as $include_file) { | |
472 include_once $this->root . '/' . $include_file; | |
473 } | |
474 } | |
475 | |
476 // If the theme implementation defines a file, then also use the path | |
477 // that it defined. Otherwise use the default path. This allows | |
478 // system.module to declare theme functions on behalf of core .include | |
479 // files. | |
480 if (isset($info['file'])) { | |
481 $include_file = isset($info['path']) ? $info['path'] : $path; | |
482 $include_file .= '/' . $info['file']; | |
483 include_once $this->root . '/' . $include_file; | |
484 $result[$hook]['includes'][] = $include_file; | |
485 } | |
486 | |
487 // A template file is the default implementation for a theme hook, but | |
488 // if the theme hook specifies a function callback instead, check to | |
489 // ensure the function actually exists. | |
490 if (isset($info['function'])) { | |
491 if (!function_exists($info['function'])) { | |
492 throw new \BadFunctionCallException(sprintf( | |
493 'Theme hook "%s" refers to a theme function callback that does not exist: "%s"', | |
494 $hook, | |
495 $info['function'] | |
496 )); | |
497 } | |
498 } | |
499 // Provide a default naming convention for 'template' based on the | |
500 // hook used. If the template does not exist, the theme engine used | |
501 // should throw an exception at runtime when attempting to include | |
502 // the template file. | |
503 elseif (!isset($info['template'])) { | |
504 $info['template'] = strtr($hook, '_', '-'); | |
505 $result[$hook]['template'] = $info['template']; | |
506 } | |
507 | |
508 // Prepend the current theming path when none is set. This is required | |
509 // for the default theme engine to know where the template lives. | |
510 if (isset($result[$hook]['template']) && !isset($info['path'])) { | |
511 $result[$hook]['path'] = $path . '/templates'; | |
512 } | |
513 | |
514 // If the default keys are not set, use the default values registered | |
515 // by the module. | |
516 if (isset($cache[$hook])) { | |
517 $result[$hook] += array_intersect_key($cache[$hook], $hook_defaults); | |
518 } | |
519 | |
520 // Preprocess variables for all theming hooks, whether the hook is | |
521 // implemented as a template or as a function. Ensure they are arrays. | |
522 if (!isset($info['preprocess functions']) || !is_array($info['preprocess functions'])) { | |
523 $info['preprocess functions'] = []; | |
524 $prefixes = []; | |
525 if ($type == 'module') { | |
526 // Default variable preprocessor prefix. | |
527 $prefixes[] = 'template'; | |
528 // Add all modules so they can intervene with their own variable | |
529 // preprocessors. This allows them to provide variable preprocessors | |
530 // even if they are not the owner of the current hook. | |
531 $prefixes = array_merge($prefixes, $module_list); | |
532 } | |
533 elseif ($type == 'theme_engine' || $type == 'base_theme_engine') { | |
534 // Theme engines get an extra set that come before the normally | |
535 // named variable preprocessors. | |
536 $prefixes[] = $name . '_engine'; | |
537 // The theme engine registers on behalf of the theme using the | |
538 // theme's name. | |
539 $prefixes[] = $theme; | |
540 } | |
541 else { | |
542 // This applies when the theme manually registers their own variable | |
543 // preprocessors. | |
544 $prefixes[] = $name; | |
545 } | |
546 foreach ($prefixes as $prefix) { | |
547 // Only use non-hook-specific variable preprocessors for theming | |
548 // hooks implemented as templates. See the @defgroup themeable | |
549 // topic. | |
550 if (isset($info['template']) && function_exists($prefix . '_preprocess')) { | |
551 $info['preprocess functions'][] = $prefix . '_preprocess'; | |
552 } | |
553 if (function_exists($prefix . '_preprocess_' . $hook)) { | |
554 $info['preprocess functions'][] = $prefix . '_preprocess_' . $hook; | |
555 } | |
556 } | |
557 } | |
558 // Check for the override flag and prevent the cached variable | |
559 // preprocessors from being used. This allows themes or theme engines | |
560 // to remove variable preprocessors set earlier in the registry build. | |
561 if (!empty($info['override preprocess functions'])) { | |
562 // Flag not needed inside the registry. | |
563 unset($result[$hook]['override preprocess functions']); | |
564 } | |
565 elseif (isset($cache[$hook]['preprocess functions']) && is_array($cache[$hook]['preprocess functions'])) { | |
566 $info['preprocess functions'] = array_merge($cache[$hook]['preprocess functions'], $info['preprocess functions']); | |
567 } | |
568 $result[$hook]['preprocess functions'] = $info['preprocess functions']; | |
569 | |
570 // If a theme implementation definition provides both 'template' and | |
571 // 'function', the 'function' will be used. In this case, if the new | |
572 // result provides a 'template' value, any existing 'function' value | |
573 // must be removed for the override to be called. | |
574 if (isset($result[$hook]['template'])) { | |
575 unset($cache[$hook]['function']); | |
576 } | |
577 } | |
578 | |
579 // Merge the newly created theme hooks into the existing cache. | |
580 $cache = NestedArray::mergeDeep($cache, $result); | |
581 } | |
582 | |
583 // Let themes have variable preprocessors even if they didn't register a | |
584 // template. | |
585 if ($type == 'theme' || $type == 'base_theme') { | |
586 foreach ($cache as $hook => $info) { | |
587 // Check only if not registered by the theme or engine. | |
588 if (empty($result[$hook])) { | |
589 if (!isset($info['preprocess functions'])) { | |
590 $cache[$hook]['preprocess functions'] = []; | |
591 } | |
592 // Only use non-hook-specific variable preprocessors for theme hooks | |
593 // implemented as templates. See the @defgroup themeable topic. | |
594 if (isset($info['template']) && function_exists($name . '_preprocess')) { | |
595 $cache[$hook]['preprocess functions'][] = $name . '_preprocess'; | |
596 } | |
597 if (function_exists($name . '_preprocess_' . $hook)) { | |
598 $cache[$hook]['preprocess functions'][] = $name . '_preprocess_' . $hook; | |
599 $cache[$hook]['theme path'] = $path; | |
600 } | |
601 } | |
602 } | |
603 } | |
604 } | |
605 | |
606 /** | |
607 * Completes the definition of the requested suggestion hook. | |
608 * | |
609 * @param string $hook | |
610 * The name of the suggestion hook to complete. | |
611 * @param array $cache | |
612 * The theme registry, as documented in | |
613 * \Drupal\Core\Theme\Registry::processExtension(). | |
614 */ | |
615 protected function completeSuggestion($hook, array &$cache) { | |
616 $previous_hook = $hook; | |
617 $incomplete_previous_hook = []; | |
618 // Continue looping if the candidate hook doesn't exist or if the candidate | |
619 // hook has incomplete preprocess functions, and if the candidate hook is a | |
620 // suggestion (has a double underscore). | |
621 while ((!isset($cache[$previous_hook]) || isset($cache[$previous_hook]['incomplete preprocess functions'])) | |
622 && $pos = strrpos($previous_hook, '__')) { | |
623 // Find the first existing candidate hook that has incomplete preprocess | |
624 // functions. | |
625 if (isset($cache[$previous_hook]) && !$incomplete_previous_hook && isset($cache[$previous_hook]['incomplete preprocess functions'])) { | |
626 $incomplete_previous_hook = $cache[$previous_hook]; | |
627 unset($incomplete_previous_hook['incomplete preprocess functions']); | |
628 } | |
629 $previous_hook = substr($previous_hook, 0, $pos); | |
630 $this->mergePreprocessFunctions($hook, $previous_hook, $incomplete_previous_hook, $cache); | |
631 } | |
632 | |
633 // In addition to processing suggestions, include base hooks. | |
634 if (isset($cache[$hook]['base hook'])) { | |
635 // In order to retain the additions from above, pass in the current hook | |
636 // as the parent hook, otherwise it will be overwritten. | |
637 $this->mergePreprocessFunctions($hook, $cache[$hook]['base hook'], $cache[$hook], $cache); | |
638 } | |
639 } | |
640 | |
641 /** | |
642 * Merges the source hook's preprocess functions into the destination hook's. | |
643 * | |
644 * @param string $destination_hook_name | |
645 * The name of the hook to merge preprocess functions to. | |
646 * @param string $source_hook_name | |
647 * The name of the hook to merge preprocess functions from. | |
648 * @param array $parent_hook | |
649 * The parent hook if it exists. Either an incomplete hook from suggestions | |
650 * or a base hook. | |
651 * @param array $cache | |
652 * The theme registry, as documented in | |
653 * \Drupal\Core\Theme\Registry::processExtension(). | |
654 */ | |
655 protected function mergePreprocessFunctions($destination_hook_name, $source_hook_name, $parent_hook, array &$cache) { | |
656 // If base hook exists clone of it for the preprocess function | |
657 // without a template. | |
658 // @see https://www.drupal.org/node/2457295 | |
659 if (isset($cache[$source_hook_name]) && (!isset($cache[$source_hook_name]['incomplete preprocess functions']) || !isset($cache[$destination_hook_name]['incomplete preprocess functions']))) { | |
660 $cache[$destination_hook_name] = $parent_hook + $cache[$source_hook_name]; | |
661 if (isset($parent_hook['preprocess functions'])) { | |
662 $diff = array_diff($parent_hook['preprocess functions'], $cache[$source_hook_name]['preprocess functions']); | |
663 $cache[$destination_hook_name]['preprocess functions'] = array_merge($cache[$source_hook_name]['preprocess functions'], $diff); | |
664 } | |
665 // If a base hook isn't set, this is the actual base hook. | |
666 if (!isset($cache[$source_hook_name]['base hook'])) { | |
667 $cache[$destination_hook_name]['base hook'] = $source_hook_name; | |
668 } | |
669 } | |
670 } | |
671 | |
672 /** | |
673 * Completes the theme registry adding discovered functions and hooks. | |
674 * | |
675 * @param array $cache | |
676 * The theme registry as documented in | |
677 * \Drupal\Core\Theme\Registry::processExtension(). | |
678 * @param \Drupal\Core\Theme\ActiveTheme $theme | |
679 * Current active theme. | |
680 * | |
681 * @see ::processExtension() | |
682 */ | |
683 protected function postProcessExtension(array &$cache, ActiveTheme $theme) { | |
684 // Gather prefixes. This will be used to limit the found functions to the | |
685 // expected naming conventions. | |
686 $prefixes = array_keys((array) $this->moduleHandler->getModuleList()); | |
687 foreach (array_reverse($theme->getBaseThemes()) as $base) { | |
688 $prefixes[] = $base->getName(); | |
689 } | |
690 if ($theme->getEngine()) { | |
691 $prefixes[] = $theme->getEngine() . '_engine'; | |
692 } | |
693 $prefixes[] = $theme->getName(); | |
694 | |
695 $grouped_functions = $this->getPrefixGroupedUserFunctions($prefixes); | |
696 | |
697 // Collect all variable preprocess functions in the correct order. | |
698 $suggestion_level = []; | |
699 $matches = []; | |
700 // Look for functions named according to the pattern and add them if they | |
701 // have matching hooks in the registry. | |
702 foreach ($prefixes as $prefix) { | |
703 // Grep only the functions which are within the prefix group. | |
704 list($first_prefix,) = explode('_', $prefix, 2); | |
705 if (!isset($grouped_functions[$first_prefix])) { | |
706 continue; | |
707 } | |
708 // Add the function and the name of the associated theme hook to the list | |
709 // of preprocess functions grouped by suggestion specificity if a matching | |
710 // base hook is found. | |
711 foreach ($grouped_functions[$first_prefix] as $candidate) { | |
712 if (preg_match("/^{$prefix}_preprocess_(((?:[^_]++|_(?!_))+)__.*)/", $candidate, $matches)) { | |
713 if (isset($cache[$matches[2]])) { | |
714 $level = substr_count($matches[1], '__'); | |
715 $suggestion_level[$level][$candidate] = $matches[1]; | |
716 } | |
717 } | |
718 } | |
719 } | |
720 | |
721 // Add missing variable preprocessors. This is needed for modules that do | |
722 // not explicitly register the hook. For example, when a theme contains a | |
723 // variable preprocess function but it does not implement a template, it | |
724 // will go missing. This will add the expected function. It also allows | |
725 // modules or themes to have a variable process function based on a pattern | |
726 // even if the hook does not exist. | |
727 ksort($suggestion_level); | |
728 foreach ($suggestion_level as $level => $item) { | |
729 foreach ($item as $preprocessor => $hook) { | |
730 if (isset($cache[$hook]['preprocess functions']) && !in_array($hook, $cache[$hook]['preprocess functions'])) { | |
731 // Add missing preprocessor to existing hook. | |
732 $cache[$hook]['preprocess functions'][] = $preprocessor; | |
733 } | |
734 elseif (!isset($cache[$hook]) && strpos($hook, '__')) { | |
735 // Process non-existing hook and register it. | |
736 // Look for a previously defined hook that is either a less specific | |
737 // suggestion hook or the base hook. | |
738 $this->completeSuggestion($hook, $cache); | |
739 $cache[$hook]['preprocess functions'][] = $preprocessor; | |
740 } | |
741 } | |
742 } | |
743 // Inherit all base hook variable preprocess functions into suggestion | |
744 // hooks. This ensures that derivative hooks have a complete set of variable | |
745 // preprocess functions. | |
746 foreach ($cache as $hook => $info) { | |
747 // The 'base hook' is only applied to derivative hooks already registered | |
748 // from a pattern. This is typically set from | |
749 // drupal_find_theme_functions() and drupal_find_theme_templates(). | |
750 if (isset($info['incomplete preprocess functions'])) { | |
751 $this->completeSuggestion($hook, $cache); | |
752 unset($cache[$hook]['incomplete preprocess functions']); | |
753 } | |
754 | |
755 // Optimize the registry. | |
756 if (isset($cache[$hook]['preprocess functions']) && empty($cache[$hook]['preprocess functions'])) { | |
757 unset($cache[$hook]['preprocess functions']); | |
758 } | |
759 // Ensure uniqueness. | |
760 if (isset($cache[$hook]['preprocess functions'])) { | |
761 $cache[$hook]['preprocess functions'] = array_unique($cache[$hook]['preprocess functions']); | |
762 } | |
763 } | |
764 } | |
765 | |
766 /** | |
767 * Invalidates theme registry caches. | |
768 * | |
769 * To be called when the list of enabled extensions is changed. | |
770 */ | |
771 public function reset() { | |
772 // Reset the runtime registry. | |
773 foreach ($this->runtimeRegistry as $runtime_registry) { | |
774 $runtime_registry->clear(); | |
775 } | |
776 $this->runtimeRegistry = []; | |
777 | |
778 $this->registry = []; | |
779 Cache::invalidateTags(['theme_registry']); | |
780 return $this; | |
781 } | |
782 | |
783 /** | |
784 * {@inheritdoc} | |
785 */ | |
786 public function destruct() { | |
787 foreach ($this->runtimeRegistry as $runtime_registry) { | |
788 $runtime_registry->destruct(); | |
789 } | |
790 } | |
791 | |
792 /** | |
793 * Gets all user functions grouped by the word before the first underscore. | |
794 * | |
795 * @param $prefixes | |
796 * An array of function prefixes by which the list can be limited. | |
797 * @return array | |
798 * Functions grouped by the first prefix. | |
799 */ | |
800 public function getPrefixGroupedUserFunctions($prefixes = []) { | |
801 $functions = get_defined_functions(); | |
802 | |
803 // If a list of prefixes is supplied, trim down the list to those items | |
804 // only as efficiently as possible. | |
805 if ($prefixes) { | |
806 $theme_functions = preg_grep('/^(' . implode(')|(', $prefixes) . ')_/', $functions['user']); | |
807 } | |
808 else { | |
809 $theme_functions = $functions['user']; | |
810 } | |
811 | |
812 $grouped_functions = []; | |
813 // Splitting user defined functions into groups by the first prefix. | |
814 foreach ($theme_functions as $function) { | |
815 list($first_prefix,) = explode('_', $function, 2); | |
816 $grouped_functions[$first_prefix][] = $function; | |
817 } | |
818 | |
819 return $grouped_functions; | |
820 } | |
821 | |
822 /** | |
823 * Wraps drupal_get_path(). | |
824 * | |
825 * @param string $module | |
826 * The name of the item for which the path is requested. | |
827 * | |
828 * @return string | |
829 */ | |
830 protected function getPath($module) { | |
831 return drupal_get_path('module', $module); | |
832 } | |
833 | |
834 } |