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