Mercurial > hg > isophonics-drupal-site
comparison core/lib/Drupal/Core/Theme/Registry.php @ 0:4c8ae668cc8c
Initial import (non-working)
author | Chris Cannam |
---|---|
date | Wed, 29 Nov 2017 16:09:58 +0000 |
parents | |
children | 1fec387a4317 |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:4c8ae668cc8c |
---|---|
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 } |