Registry.php 34 KB

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