annotate core/lib/Drupal/Core/Theme/Registry.php @ 0:4c8ae668cc8c

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