1. 8.3.x core/lib/Drupal/Core/Theme/Registry.php
  2. 8.0.x core/lib/Drupal/Core/Theme/Registry.php
  3. 8.1.x core/lib/Drupal/Core/Theme/Registry.php
  4. 8.2.x core/lib/Drupal/Core/Theme/Registry.php
  5. 8.4.x core/lib/Drupal/Core/Theme/Registry.php

Namespace

Drupal\Core\Theme

File

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

Classes

Namesort descending Description
Registry Defines the theme registry service.