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 }