61f0483363e74ee6c4d53938755a23d05fe33b16
[yaffs-website] / web / core / lib / Drupal / Core / Theme / ThemeManager.php
1 <?php
2
3 namespace Drupal\Core\Theme;
4
5 use Drupal\Component\Render\MarkupInterface;
6 use Drupal\Core\Render\Markup;
7 use Drupal\Core\Routing\RouteMatchInterface;
8 use Drupal\Core\Routing\StackedRouteMatchInterface;
9 use Drupal\Core\Extension\ModuleHandlerInterface;
10 use Drupal\Core\Template\Attribute;
11
12 /**
13  * Provides the default implementation of a theme manager.
14  */
15 class ThemeManager implements ThemeManagerInterface {
16
17   /**
18    * The theme negotiator.
19    *
20    * @var \Drupal\Core\Theme\ThemeNegotiatorInterface
21    */
22   protected $themeNegotiator;
23
24   /**
25    * The theme registry used to render an output.
26    *
27    * @var \Drupal\Core\Theme\Registry
28    */
29   protected $themeRegistry;
30
31   /**
32    * Contains the current active theme.
33    *
34    * @var \Drupal\Core\Theme\ActiveTheme
35    */
36   protected $activeTheme;
37
38   /**
39    * The theme initialization.
40    *
41    * @var \Drupal\Core\Theme\ThemeInitializationInterface
42    */
43   protected $themeInitialization;
44
45   /**
46    * The module handler.
47    *
48    * @var \Drupal\Core\Extension\ModuleHandlerInterface
49    */
50   protected $moduleHandler;
51
52   /**
53    * The app root.
54    *
55    * @var string
56    */
57   protected $root;
58
59   /**
60    * Constructs a new ThemeManager object.
61    *
62    * @param string $root
63    *   The app root.
64    * @param \Drupal\Core\Theme\ThemeNegotiatorInterface $theme_negotiator
65    *   The theme negotiator.
66    * @param \Drupal\Core\Theme\ThemeInitializationInterface $theme_initialization
67    *   The theme initialization.
68    * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
69    *   The module handler.
70    */
71   public function __construct($root, ThemeNegotiatorInterface $theme_negotiator, ThemeInitializationInterface $theme_initialization, ModuleHandlerInterface $module_handler) {
72     $this->root = $root;
73     $this->themeNegotiator = $theme_negotiator;
74     $this->themeInitialization = $theme_initialization;
75     $this->moduleHandler = $module_handler;
76   }
77
78   /**
79    * Sets the theme registry.
80    *
81    * @param \Drupal\Core\Theme\Registry $theme_registry
82    *   The theme registry.
83    *
84    * @return $this
85    */
86   public function setThemeRegistry(Registry $theme_registry) {
87     $this->themeRegistry = $theme_registry;
88     return $this;
89   }
90
91   /**
92    * {@inheritdoc}
93    */
94   public function getActiveTheme(RouteMatchInterface $route_match = NULL) {
95     if (!isset($this->activeTheme)) {
96       $this->initTheme($route_match);
97     }
98     return $this->activeTheme;
99   }
100
101   /**
102    * {@inheritdoc}
103    */
104   public function hasActiveTheme() {
105     return isset($this->activeTheme);
106   }
107
108   /**
109    * {@inheritdoc}
110    */
111   public function resetActiveTheme() {
112     $this->activeTheme = NULL;
113     return $this;
114   }
115
116   /**
117    * {@inheritdoc}
118    */
119   public function setActiveTheme(ActiveTheme $active_theme) {
120     $this->activeTheme = $active_theme;
121     if ($active_theme) {
122       $this->themeInitialization->loadActiveTheme($active_theme);
123     }
124     return $this;
125   }
126
127   /**
128    * {@inheritdoc}
129    */
130   public function render($hook, array $variables) {
131     static $default_attributes;
132
133     $active_theme = $this->getActiveTheme();
134
135     // If called before all modules are loaded, we do not necessarily have a
136     // full theme registry to work with, and therefore cannot process the theme
137     // request properly. See also \Drupal\Core\Theme\Registry::get().
138     if (!$this->moduleHandler->isLoaded() && !defined('MAINTENANCE_MODE')) {
139       throw new \Exception('The theme implementations may not be rendered until all modules are loaded.');
140     }
141
142     $theme_registry = $this->themeRegistry->getRuntime();
143
144     // If an array of hook candidates were passed, use the first one that has an
145     // implementation.
146     if (is_array($hook)) {
147       foreach ($hook as $candidate) {
148         if ($theme_registry->has($candidate)) {
149           break;
150         }
151       }
152       $hook = $candidate;
153     }
154     // Save the original theme hook, so it can be supplied to theme variable
155     // preprocess callbacks.
156     $original_hook = $hook;
157
158     // If there's no implementation, check for more generic fallbacks.
159     // If there's still no implementation, log an error and return an empty
160     // string.
161     if (!$theme_registry->has($hook)) {
162       // Iteratively strip everything after the last '__' delimiter, until an
163       // implementation is found.
164       while ($pos = strrpos($hook, '__')) {
165         $hook = substr($hook, 0, $pos);
166         if ($theme_registry->has($hook)) {
167           break;
168         }
169       }
170       if (!$theme_registry->has($hook)) {
171         // Only log a message when not trying theme suggestions ($hook being an
172         // array).
173         if (!isset($candidate)) {
174           \Drupal::logger('theme')->warning('Theme hook %hook not found.', ['%hook' => $hook]);
175         }
176         // There is no theme implementation for the hook passed. Return FALSE so
177         // the function calling
178         // \Drupal\Core\Theme\ThemeManagerInterface::render() can differentiate
179         // between a hook that exists and renders an empty string, and a hook
180         // that is not implemented.
181         return FALSE;
182       }
183     }
184
185     $info = $theme_registry->get($hook);
186
187     // If a renderable array is passed as $variables, then set $variables to
188     // the arguments expected by the theme function.
189     if (isset($variables['#theme']) || isset($variables['#theme_wrappers'])) {
190       $element = $variables;
191       $variables = [];
192       if (isset($info['variables'])) {
193         foreach (array_keys($info['variables']) as $name) {
194           if (isset($element["#$name"]) || array_key_exists("#$name", $element)) {
195             $variables[$name] = $element["#$name"];
196           }
197         }
198       }
199       else {
200         $variables[$info['render element']] = $element;
201         // Give a hint to render engines to prevent infinite recursion.
202         $variables[$info['render element']]['#render_children'] = TRUE;
203       }
204     }
205
206     // Merge in argument defaults.
207     if (!empty($info['variables'])) {
208       $variables += $info['variables'];
209     }
210     elseif (!empty($info['render element'])) {
211       $variables += [$info['render element'] => []];
212     }
213     // Supply original caller info.
214     $variables += [
215       'theme_hook_original' => $original_hook,
216     ];
217
218     // Set base hook for later use. For example if '#theme' => 'node__article'
219     // is called, we run hook_theme_suggestions_node_alter() rather than
220     // hook_theme_suggestions_node__article_alter(), and also pass in the base
221     // hook as the last parameter to the suggestions alter hooks.
222     if (isset($info['base hook'])) {
223       $base_theme_hook = $info['base hook'];
224     }
225     else {
226       $base_theme_hook = $hook;
227     }
228
229     // Invoke hook_theme_suggestions_HOOK().
230     $suggestions = $this->moduleHandler->invokeAll('theme_suggestions_' . $base_theme_hook, [$variables]);
231     // If the theme implementation was invoked with a direct theme suggestion
232     // like '#theme' => 'node__article', add it to the suggestions array before
233     // invoking suggestion alter hooks.
234     if (isset($info['base hook'])) {
235       $suggestions[] = $hook;
236     }
237
238     // Invoke hook_theme_suggestions_alter() and
239     // hook_theme_suggestions_HOOK_alter().
240     $hooks = [
241       'theme_suggestions',
242       'theme_suggestions_' . $base_theme_hook,
243     ];
244     $this->moduleHandler->alter($hooks, $suggestions, $variables, $base_theme_hook);
245     $this->alter($hooks, $suggestions, $variables, $base_theme_hook);
246
247     // Check if each suggestion exists in the theme registry, and if so,
248     // use it instead of the base hook. For example, a function may use
249     // '#theme' => 'node', but a module can add 'node__article' as a suggestion
250     // via hook_theme_suggestions_HOOK_alter(), enabling a theme to have
251     // an alternate template file for article nodes.
252     foreach (array_reverse($suggestions) as $suggestion) {
253       if ($theme_registry->has($suggestion)) {
254         $info = $theme_registry->get($suggestion);
255         break;
256       }
257     }
258
259     // Include a file if the theme function or variable preprocessor is held
260     // elsewhere.
261     if (!empty($info['includes'])) {
262       foreach ($info['includes'] as $include_file) {
263         include_once $this->root . '/' . $include_file;
264       }
265     }
266
267     // Invoke the variable preprocessors, if any.
268     if (isset($info['base hook'])) {
269       $base_hook = $info['base hook'];
270       $base_hook_info = $theme_registry->get($base_hook);
271       // Include files required by the base hook, since its variable
272       // preprocessors might reside there.
273       if (!empty($base_hook_info['includes'])) {
274         foreach ($base_hook_info['includes'] as $include_file) {
275           include_once $this->root . '/' . $include_file;
276         }
277       }
278       if (isset($base_hook_info['preprocess functions'])) {
279         // Set a variable for the 'theme_hook_suggestion'. This is used to
280         // maintain backwards compatibility with template engines.
281         $theme_hook_suggestion = $hook;
282       }
283     }
284     if (isset($info['preprocess functions'])) {
285       foreach ($info['preprocess functions'] as $preprocessor_function) {
286         if (function_exists($preprocessor_function)) {
287           $preprocessor_function($variables, $hook, $info);
288         }
289       }
290       // Allow theme preprocess functions to set $variables['#attached'] and
291       // $variables['#cache'] and use them like the corresponding element
292       // properties on render arrays. In Drupal 8, this is the (only) officially
293       // supported method of attaching bubbleable metadata from preprocess
294       // functions. Assets attached here should be associated with the template
295       // that we are preprocessing variables for.
296       $preprocess_bubbleable = [];
297       foreach (['#attached', '#cache'] as $key) {
298         if (isset($variables[$key])) {
299           $preprocess_bubbleable[$key] = $variables[$key];
300         }
301       }
302       // We do not allow preprocess functions to define cacheable elements.
303       unset($preprocess_bubbleable['#cache']['keys']);
304       if ($preprocess_bubbleable) {
305         // @todo Inject the Renderer in https://www.drupal.org/node/2529438.
306         \Drupal::service('renderer')->render($preprocess_bubbleable);
307       }
308     }
309
310     // Generate the output using either a function or a template.
311     $output = '';
312     if (isset($info['function'])) {
313       if (function_exists($info['function'])) {
314         // Theme functions do not render via the theme engine, so the output is
315         // not autoescaped. However, we can only presume that the theme function
316         // has been written correctly and that the markup is safe.
317         $output = Markup::create($info['function']($variables));
318       }
319     }
320     else {
321       $render_function = 'twig_render_template';
322       $extension = '.html.twig';
323
324       // The theme engine may use a different extension and a different
325       // renderer.
326       $theme_engine = $active_theme->getEngine();
327       if (isset($theme_engine)) {
328         if ($info['type'] != 'module') {
329           if (function_exists($theme_engine . '_render_template')) {
330             $render_function = $theme_engine . '_render_template';
331           }
332           $extension_function = $theme_engine . '_extension';
333           if (function_exists($extension_function)) {
334             $extension = $extension_function();
335           }
336         }
337       }
338
339       // In some cases, a template implementation may not have had
340       // template_preprocess() run (for example, if the default implementation
341       // is a function, but a template overrides that default implementation).
342       // In these cases, a template should still be able to expect to have
343       // access to the variables provided by template_preprocess(), so we add
344       // them here if they don't already exist. We don't want the overhead of
345       // running template_preprocess() twice, so we use the 'directory' variable
346       // to determine if it has already run, which while not completely
347       // intuitive, is reasonably safe, and allows us to save on the overhead of
348       // adding some new variable to track that.
349       if (!isset($variables['directory'])) {
350         $default_template_variables = [];
351         template_preprocess($default_template_variables, $hook, $info);
352         $variables += $default_template_variables;
353       }
354       if (!isset($default_attributes)) {
355         $default_attributes = new Attribute();
356       }
357       foreach (['attributes', 'title_attributes', 'content_attributes'] as $key) {
358         if (isset($variables[$key]) && !($variables[$key] instanceof Attribute)) {
359           if ($variables[$key]) {
360             $variables[$key] = new Attribute($variables[$key]);
361           }
362           else {
363             // Create empty attributes.
364             $variables[$key] = clone $default_attributes;
365           }
366         }
367       }
368
369       // Render the output using the template file.
370       $template_file = $info['template'] . $extension;
371       if (isset($info['path'])) {
372         $template_file = $info['path'] . '/' . $template_file;
373       }
374       // Add the theme suggestions to the variables array just before rendering
375       // the template for backwards compatibility with template engines.
376       $variables['theme_hook_suggestions'] = $suggestions;
377       // For backwards compatibility, pass 'theme_hook_suggestion' on to the
378       // template engine. This is only set when calling a direct suggestion like
379       // '#theme' => 'menu__shortcut_default' when the template exists in the
380       // current theme.
381       if (isset($theme_hook_suggestion)) {
382         $variables['theme_hook_suggestion'] = $theme_hook_suggestion;
383       }
384       $output = $render_function($template_file, $variables);
385     }
386
387     return ($output instanceof MarkupInterface) ? $output : (string) $output;
388   }
389
390   /**
391    * Initializes the active theme for a given route match.
392    *
393    * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
394    *   The current route match.
395    */
396   protected function initTheme(RouteMatchInterface $route_match = NULL) {
397     // Determine the active theme for the theme negotiator service. This includes
398     // the default theme as well as really specific ones like the ajax base theme.
399     if (!$route_match) {
400       $route_match = \Drupal::routeMatch();
401     }
402     if ($route_match instanceof StackedRouteMatchInterface) {
403       $route_match = $route_match->getMasterRouteMatch();
404     }
405     $theme = $this->themeNegotiator->determineActiveTheme($route_match);
406     $this->activeTheme = $this->themeInitialization->initTheme($theme);
407   }
408
409   /**
410    * {@inheritdoc}
411    *
412    * @todo Should we cache some of these information?
413    */
414   public function alterForTheme(ActiveTheme $theme, $type, &$data, &$context1 = NULL, &$context2 = NULL) {
415     // Most of the time, $type is passed as a string, so for performance,
416     // normalize it to that. When passed as an array, usually the first item in
417     // the array is a generic type, and additional items in the array are more
418     // specific variants of it, as in the case of array('form', 'form_FORM_ID').
419     if (is_array($type)) {
420       $extra_types = $type;
421       $type = array_shift($extra_types);
422       // Allow if statements in this function to use the faster isset() rather
423       // than !empty() both when $type is passed as a string, or as an array with
424       // one item.
425       if (empty($extra_types)) {
426         unset($extra_types);
427       }
428     }
429
430     $theme_keys = [];
431     foreach ($theme->getBaseThemes() as $base) {
432       $theme_keys[] = $base->getName();
433     }
434
435     $theme_keys[] = $theme->getName();
436     $functions = [];
437     foreach ($theme_keys as $theme_key) {
438       $function = $theme_key . '_' . $type . '_alter';
439       if (function_exists($function)) {
440         $functions[] = $function;
441       }
442       if (isset($extra_types)) {
443         foreach ($extra_types as $extra_type) {
444           $function = $theme_key . '_' . $extra_type . '_alter';
445           if (function_exists($function)) {
446             $functions[] = $function;
447           }
448         }
449       }
450     }
451
452     foreach ($functions as $function) {
453       $function($data, $context1, $context2);
454     }
455   }
456
457   /**
458    * {@inheritdoc}
459    */
460   public function alter($type, &$data, &$context1 = NULL, &$context2 = NULL) {
461     $theme = $this->getActiveTheme();
462     $this->alterForTheme($theme, $type, $data, $context1, $context2);
463   }
464
465 }