ThemeManager.php 16 KB

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