annotate core/lib/Drupal/Core/Theme/Registry.php @ 19:fa3358dc1485 tip

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