Registry.php 33 KB

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