annotate core/lib/Drupal/Core/Extension/ModuleHandler.php @ 14:1fec387a4317

Update Drupal core to 8.5.2 via Composer
author Chris Cannam
date Mon, 23 Apr 2018 09:46:53 +0100
parents 4c8ae668cc8c
children 129ea1e6d783
rev   line source
Chris@0 1 <?php
Chris@0 2
Chris@0 3 namespace Drupal\Core\Extension;
Chris@0 4
Chris@0 5 use Drupal\Component\Graph\Graph;
Chris@0 6 use Drupal\Component\Utility\NestedArray;
Chris@0 7 use Drupal\Core\Cache\CacheBackendInterface;
Chris@0 8
Chris@0 9 /**
Chris@0 10 * Class that manages modules in a Drupal installation.
Chris@0 11 */
Chris@0 12 class ModuleHandler implements ModuleHandlerInterface {
Chris@0 13
Chris@0 14 /**
Chris@0 15 * List of loaded files.
Chris@0 16 *
Chris@0 17 * @var array
Chris@0 18 * An associative array whose keys are file paths of loaded files, relative
Chris@0 19 * to the application's root directory.
Chris@0 20 */
Chris@0 21 protected $loadedFiles;
Chris@0 22
Chris@0 23 /**
Chris@0 24 * List of installed modules.
Chris@0 25 *
Chris@0 26 * @var \Drupal\Core\Extension\Extension[]
Chris@0 27 */
Chris@0 28 protected $moduleList;
Chris@0 29
Chris@0 30 /**
Chris@0 31 * Boolean indicating whether modules have been loaded.
Chris@0 32 *
Chris@0 33 * @var bool
Chris@0 34 */
Chris@0 35 protected $loaded = FALSE;
Chris@0 36
Chris@0 37 /**
Chris@0 38 * List of hook implementations keyed by hook name.
Chris@0 39 *
Chris@0 40 * @var array
Chris@0 41 */
Chris@0 42 protected $implementations;
Chris@0 43
Chris@0 44 /**
Chris@0 45 * List of hooks where the implementations have been "verified".
Chris@0 46 *
Chris@0 47 * @var true[]
Chris@0 48 * Associative array where keys are hook names.
Chris@0 49 */
Chris@0 50 protected $verified;
Chris@0 51
Chris@0 52 /**
Chris@0 53 * Information returned by hook_hook_info() implementations.
Chris@0 54 *
Chris@0 55 * @var array
Chris@0 56 */
Chris@0 57 protected $hookInfo;
Chris@0 58
Chris@0 59 /**
Chris@0 60 * Cache backend for storing module hook implementation information.
Chris@0 61 *
Chris@0 62 * @var \Drupal\Core\Cache\CacheBackendInterface
Chris@0 63 */
Chris@0 64 protected $cacheBackend;
Chris@0 65
Chris@0 66 /**
Chris@0 67 * Whether the cache needs to be written.
Chris@0 68 *
Chris@0 69 * @var bool
Chris@0 70 */
Chris@0 71 protected $cacheNeedsWriting = FALSE;
Chris@0 72
Chris@0 73 /**
Chris@0 74 * List of alter hook implementations keyed by hook name(s).
Chris@0 75 *
Chris@0 76 * @var array
Chris@0 77 */
Chris@0 78 protected $alterFunctions;
Chris@0 79
Chris@0 80 /**
Chris@0 81 * The app root.
Chris@0 82 *
Chris@0 83 * @var string
Chris@0 84 */
Chris@0 85 protected $root;
Chris@0 86
Chris@0 87 /**
Chris@0 88 * A list of module include file keys.
Chris@0 89 *
Chris@0 90 * @var array
Chris@0 91 */
Chris@0 92 protected $includeFileKeys = [];
Chris@0 93
Chris@0 94 /**
Chris@0 95 * Constructs a ModuleHandler object.
Chris@0 96 *
Chris@0 97 * @param string $root
Chris@0 98 * The app root.
Chris@0 99 * @param array $module_list
Chris@0 100 * An associative array whose keys are the names of installed modules and
Chris@0 101 * whose values are Extension class parameters. This is normally the
Chris@0 102 * %container.modules% parameter being set up by DrupalKernel.
Chris@0 103 * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
Chris@0 104 * Cache backend for storing module hook implementation information.
Chris@0 105 *
Chris@0 106 * @see \Drupal\Core\DrupalKernel
Chris@0 107 * @see \Drupal\Core\CoreServiceProvider
Chris@0 108 */
Chris@0 109 public function __construct($root, array $module_list, CacheBackendInterface $cache_backend) {
Chris@0 110 $this->root = $root;
Chris@0 111 $this->moduleList = [];
Chris@0 112 foreach ($module_list as $name => $module) {
Chris@0 113 $this->moduleList[$name] = new Extension($this->root, $module['type'], $module['pathname'], $module['filename']);
Chris@0 114 }
Chris@0 115 $this->cacheBackend = $cache_backend;
Chris@0 116 }
Chris@0 117
Chris@0 118 /**
Chris@0 119 * {@inheritdoc}
Chris@0 120 */
Chris@0 121 public function load($name) {
Chris@0 122 if (isset($this->loadedFiles[$name])) {
Chris@0 123 return TRUE;
Chris@0 124 }
Chris@0 125
Chris@0 126 if (isset($this->moduleList[$name])) {
Chris@0 127 $this->moduleList[$name]->load();
Chris@0 128 $this->loadedFiles[$name] = TRUE;
Chris@0 129 return TRUE;
Chris@0 130 }
Chris@0 131 return FALSE;
Chris@0 132 }
Chris@0 133
Chris@0 134 /**
Chris@0 135 * {@inheritdoc}
Chris@0 136 */
Chris@0 137 public function loadAll() {
Chris@0 138 if (!$this->loaded) {
Chris@0 139 foreach ($this->moduleList as $name => $module) {
Chris@0 140 $this->load($name);
Chris@0 141 }
Chris@0 142 $this->loaded = TRUE;
Chris@0 143 }
Chris@0 144 }
Chris@0 145
Chris@0 146 /**
Chris@0 147 * {@inheritdoc}
Chris@0 148 */
Chris@0 149 public function reload() {
Chris@0 150 $this->loaded = FALSE;
Chris@0 151 $this->loadAll();
Chris@0 152 }
Chris@0 153
Chris@0 154 /**
Chris@0 155 * {@inheritdoc}
Chris@0 156 */
Chris@0 157 public function isLoaded() {
Chris@0 158 return $this->loaded;
Chris@0 159 }
Chris@0 160
Chris@0 161 /**
Chris@0 162 * {@inheritdoc}
Chris@0 163 */
Chris@0 164 public function getModuleList() {
Chris@0 165 return $this->moduleList;
Chris@0 166 }
Chris@0 167
Chris@0 168 /**
Chris@0 169 * {@inheritdoc}
Chris@0 170 */
Chris@0 171 public function getModule($name) {
Chris@0 172 if (isset($this->moduleList[$name])) {
Chris@0 173 return $this->moduleList[$name];
Chris@0 174 }
Chris@0 175 throw new \InvalidArgumentException(sprintf('The module %s does not exist.', $name));
Chris@0 176 }
Chris@0 177
Chris@0 178 /**
Chris@0 179 * {@inheritdoc}
Chris@0 180 */
Chris@0 181 public function setModuleList(array $module_list = []) {
Chris@0 182 $this->moduleList = $module_list;
Chris@0 183 // Reset the implementations, so a new call triggers a reloading of the
Chris@0 184 // available hooks.
Chris@0 185 $this->resetImplementations();
Chris@0 186 }
Chris@0 187
Chris@0 188 /**
Chris@0 189 * {@inheritdoc}
Chris@0 190 */
Chris@0 191 public function addModule($name, $path) {
Chris@0 192 $this->add('module', $name, $path);
Chris@0 193 }
Chris@0 194
Chris@0 195 /**
Chris@0 196 * {@inheritdoc}
Chris@0 197 */
Chris@0 198 public function addProfile($name, $path) {
Chris@0 199 $this->add('profile', $name, $path);
Chris@0 200 }
Chris@0 201
Chris@0 202 /**
Chris@0 203 * Adds a module or profile to the list of currently active modules.
Chris@0 204 *
Chris@0 205 * @param string $type
Chris@0 206 * The extension type; either 'module' or 'profile'.
Chris@0 207 * @param string $name
Chris@0 208 * The module name; e.g., 'node'.
Chris@0 209 * @param string $path
Chris@0 210 * The module path; e.g., 'core/modules/node'.
Chris@0 211 */
Chris@0 212 protected function add($type, $name, $path) {
Chris@0 213 $pathname = "$path/$name.info.yml";
Chris@0 214 $filename = file_exists($this->root . "/$path/$name.$type") ? "$name.$type" : NULL;
Chris@0 215 $this->moduleList[$name] = new Extension($this->root, $type, $pathname, $filename);
Chris@0 216 $this->resetImplementations();
Chris@0 217 }
Chris@0 218
Chris@0 219 /**
Chris@0 220 * {@inheritdoc}
Chris@0 221 */
Chris@0 222 public function buildModuleDependencies(array $modules) {
Chris@0 223 foreach ($modules as $module) {
Chris@0 224 $graph[$module->getName()]['edges'] = [];
Chris@0 225 if (isset($module->info['dependencies']) && is_array($module->info['dependencies'])) {
Chris@0 226 foreach ($module->info['dependencies'] as $dependency) {
Chris@0 227 $dependency_data = static::parseDependency($dependency);
Chris@0 228 $graph[$module->getName()]['edges'][$dependency_data['name']] = $dependency_data;
Chris@0 229 }
Chris@0 230 }
Chris@0 231 }
Chris@0 232 $graph_object = new Graph($graph);
Chris@0 233 $graph = $graph_object->searchAndSort();
Chris@0 234 foreach ($graph as $module_name => $data) {
Chris@0 235 $modules[$module_name]->required_by = isset($data['reverse_paths']) ? $data['reverse_paths'] : [];
Chris@0 236 $modules[$module_name]->requires = isset($data['paths']) ? $data['paths'] : [];
Chris@0 237 $modules[$module_name]->sort = $data['weight'];
Chris@0 238 }
Chris@0 239 return $modules;
Chris@0 240 }
Chris@0 241
Chris@0 242 /**
Chris@0 243 * {@inheritdoc}
Chris@0 244 */
Chris@0 245 public function moduleExists($module) {
Chris@0 246 return isset($this->moduleList[$module]);
Chris@0 247 }
Chris@0 248
Chris@0 249 /**
Chris@0 250 * {@inheritdoc}
Chris@0 251 */
Chris@0 252 public function loadAllIncludes($type, $name = NULL) {
Chris@0 253 foreach ($this->moduleList as $module => $filename) {
Chris@0 254 $this->loadInclude($module, $type, $name);
Chris@0 255 }
Chris@0 256 }
Chris@0 257
Chris@0 258 /**
Chris@0 259 * {@inheritdoc}
Chris@0 260 */
Chris@0 261 public function loadInclude($module, $type, $name = NULL) {
Chris@0 262 if ($type == 'install') {
Chris@0 263 // Make sure the installation API is available
Chris@0 264 include_once $this->root . '/core/includes/install.inc';
Chris@0 265 }
Chris@0 266
Chris@0 267 $name = $name ?: $module;
Chris@0 268 $key = $type . ':' . $module . ':' . $name;
Chris@0 269 if (isset($this->includeFileKeys[$key])) {
Chris@0 270 return $this->includeFileKeys[$key];
Chris@0 271 }
Chris@0 272 if (isset($this->moduleList[$module])) {
Chris@0 273 $file = $this->root . '/' . $this->moduleList[$module]->getPath() . "/$name.$type";
Chris@0 274 if (is_file($file)) {
Chris@0 275 require_once $file;
Chris@0 276 $this->includeFileKeys[$key] = $file;
Chris@0 277 return $file;
Chris@0 278 }
Chris@0 279 else {
Chris@0 280 $this->includeFileKeys[$key] = FALSE;
Chris@0 281 }
Chris@0 282 }
Chris@0 283 return FALSE;
Chris@0 284 }
Chris@0 285
Chris@0 286 /**
Chris@0 287 * {@inheritdoc}
Chris@0 288 */
Chris@0 289 public function getHookInfo() {
Chris@0 290 if (!isset($this->hookInfo)) {
Chris@0 291 if ($cache = $this->cacheBackend->get('hook_info')) {
Chris@0 292 $this->hookInfo = $cache->data;
Chris@0 293 }
Chris@0 294 else {
Chris@0 295 $this->buildHookInfo();
Chris@0 296 $this->cacheBackend->set('hook_info', $this->hookInfo);
Chris@0 297 }
Chris@0 298 }
Chris@0 299 return $this->hookInfo;
Chris@0 300 }
Chris@0 301
Chris@0 302 /**
Chris@0 303 * Builds hook_hook_info() information.
Chris@0 304 *
Chris@0 305 * @see \Drupal\Core\Extension\ModuleHandler::getHookInfo()
Chris@0 306 */
Chris@0 307 protected function buildHookInfo() {
Chris@0 308 $this->hookInfo = [];
Chris@0 309 // Make sure that the modules are loaded before checking.
Chris@0 310 $this->reload();
Chris@0 311 // $this->invokeAll() would cause an infinite recursion.
Chris@0 312 foreach ($this->moduleList as $module => $filename) {
Chris@0 313 $function = $module . '_hook_info';
Chris@0 314 if (function_exists($function)) {
Chris@0 315 $result = $function();
Chris@0 316 if (isset($result) && is_array($result)) {
Chris@0 317 $this->hookInfo = NestedArray::mergeDeep($this->hookInfo, $result);
Chris@0 318 }
Chris@0 319 }
Chris@0 320 }
Chris@0 321 }
Chris@0 322
Chris@0 323 /**
Chris@0 324 * {@inheritdoc}
Chris@0 325 */
Chris@0 326 public function getImplementations($hook) {
Chris@0 327 $implementations = $this->getImplementationInfo($hook);
Chris@0 328 return array_keys($implementations);
Chris@0 329 }
Chris@0 330
Chris@0 331 /**
Chris@0 332 * {@inheritdoc}
Chris@0 333 */
Chris@0 334 public function writeCache() {
Chris@0 335 if ($this->cacheNeedsWriting) {
Chris@0 336 $this->cacheBackend->set('module_implements', $this->implementations);
Chris@0 337 $this->cacheNeedsWriting = FALSE;
Chris@0 338 }
Chris@0 339 }
Chris@0 340
Chris@0 341 /**
Chris@0 342 * {@inheritdoc}
Chris@0 343 */
Chris@0 344 public function resetImplementations() {
Chris@0 345 $this->implementations = NULL;
Chris@0 346 $this->hookInfo = NULL;
Chris@0 347 $this->alterFunctions = NULL;
Chris@0 348 // We maintain a persistent cache of hook implementations in addition to the
Chris@0 349 // static cache to avoid looping through every module and every hook on each
Chris@0 350 // request. Benchmarks show that the benefit of this caching outweighs the
Chris@0 351 // additional database hit even when using the default database caching
Chris@0 352 // backend and only a small number of modules are enabled. The cost of the
Chris@0 353 // $this->cacheBackend->get() is more or less constant and reduced further
Chris@0 354 // when non-database caching backends are used, so there will be more
Chris@0 355 // significant gains when a large number of modules are installed or hooks
Chris@0 356 // invoked, since this can quickly lead to
Chris@0 357 // \Drupal::moduleHandler()->implementsHook() being called several thousand
Chris@0 358 // times per request.
Chris@0 359 $this->cacheBackend->set('module_implements', []);
Chris@0 360 $this->cacheBackend->delete('hook_info');
Chris@0 361 }
Chris@0 362
Chris@0 363 /**
Chris@0 364 * {@inheritdoc}
Chris@0 365 */
Chris@0 366 public function implementsHook($module, $hook) {
Chris@0 367 $function = $module . '_' . $hook;
Chris@0 368 if (function_exists($function)) {
Chris@0 369 return TRUE;
Chris@0 370 }
Chris@0 371 // If the hook implementation does not exist, check whether it lives in an
Chris@0 372 // optional include file registered via hook_hook_info().
Chris@0 373 $hook_info = $this->getHookInfo();
Chris@0 374 if (isset($hook_info[$hook]['group'])) {
Chris@0 375 $this->loadInclude($module, 'inc', $module . '.' . $hook_info[$hook]['group']);
Chris@0 376 if (function_exists($function)) {
Chris@0 377 return TRUE;
Chris@0 378 }
Chris@0 379 }
Chris@0 380 return FALSE;
Chris@0 381 }
Chris@0 382
Chris@0 383 /**
Chris@0 384 * {@inheritdoc}
Chris@0 385 */
Chris@0 386 public function invoke($module, $hook, array $args = []) {
Chris@0 387 if (!$this->implementsHook($module, $hook)) {
Chris@0 388 return;
Chris@0 389 }
Chris@0 390 $function = $module . '_' . $hook;
Chris@0 391 return call_user_func_array($function, $args);
Chris@0 392 }
Chris@0 393
Chris@0 394 /**
Chris@0 395 * {@inheritdoc}
Chris@0 396 */
Chris@0 397 public function invokeAll($hook, array $args = []) {
Chris@0 398 $return = [];
Chris@0 399 $implementations = $this->getImplementations($hook);
Chris@0 400 foreach ($implementations as $module) {
Chris@0 401 $function = $module . '_' . $hook;
Chris@0 402 $result = call_user_func_array($function, $args);
Chris@0 403 if (isset($result) && is_array($result)) {
Chris@0 404 $return = NestedArray::mergeDeep($return, $result);
Chris@0 405 }
Chris@0 406 elseif (isset($result)) {
Chris@0 407 $return[] = $result;
Chris@0 408 }
Chris@0 409 }
Chris@0 410
Chris@0 411 return $return;
Chris@0 412 }
Chris@0 413
Chris@0 414 /**
Chris@0 415 * {@inheritdoc}
Chris@0 416 */
Chris@14 417 public function invokeDeprecated($description, $module, $hook, array $args = []) {
Chris@14 418 $result = $this->invoke($module, $hook, $args);
Chris@14 419 $this->triggerDeprecationError($description, $hook);
Chris@14 420 return $result;
Chris@14 421 }
Chris@14 422
Chris@14 423 /**
Chris@14 424 * {@inheritdoc}
Chris@14 425 */
Chris@14 426 public function invokeAllDeprecated($description, $hook, array $args = []) {
Chris@14 427 $result = $this->invokeAll($hook, $args);
Chris@14 428 $this->triggerDeprecationError($description, $hook);
Chris@14 429 return $result;
Chris@14 430 }
Chris@14 431
Chris@14 432
Chris@14 433 /**
Chris@14 434 * Triggers an E_USER_DEPRECATED error if any module implements the hook.
Chris@14 435 *
Chris@14 436 * @param string $description
Chris@14 437 * Helpful text describing what to do instead of implementing this hook.
Chris@14 438 * @param string $hook
Chris@14 439 * The name of the hook.
Chris@14 440 */
Chris@14 441 private function triggerDeprecationError($description, $hook) {
Chris@14 442 $modules = array_keys($this->getImplementationInfo($hook));
Chris@14 443 if (!empty($modules)) {
Chris@14 444 $message = 'The deprecated hook hook_' . $hook . '() is implemented in these functions: ';
Chris@14 445 $implementations = array_map(function ($module) use ($hook) {
Chris@14 446 return $module . '_' . $hook . '()';
Chris@14 447 }, $modules);
Chris@14 448 @trigger_error($message . implode(', ', $implementations) . '. ' . $description, E_USER_DEPRECATED);
Chris@14 449 }
Chris@14 450 }
Chris@14 451
Chris@14 452 /**
Chris@14 453 * {@inheritdoc}
Chris@14 454 */
Chris@0 455 public function alter($type, &$data, &$context1 = NULL, &$context2 = NULL) {
Chris@0 456 // Most of the time, $type is passed as a string, so for performance,
Chris@0 457 // normalize it to that. When passed as an array, usually the first item in
Chris@0 458 // the array is a generic type, and additional items in the array are more
Chris@0 459 // specific variants of it, as in the case of array('form', 'form_FORM_ID').
Chris@0 460 if (is_array($type)) {
Chris@0 461 $cid = implode(',', $type);
Chris@0 462 $extra_types = $type;
Chris@0 463 $type = array_shift($extra_types);
Chris@0 464 // Allow if statements in this function to use the faster isset() rather
Chris@0 465 // than !empty() both when $type is passed as a string, or as an array
Chris@0 466 // with one item.
Chris@0 467 if (empty($extra_types)) {
Chris@0 468 unset($extra_types);
Chris@0 469 }
Chris@0 470 }
Chris@0 471 else {
Chris@0 472 $cid = $type;
Chris@0 473 }
Chris@0 474
Chris@0 475 // Some alter hooks are invoked many times per page request, so store the
Chris@0 476 // list of functions to call, and on subsequent calls, iterate through them
Chris@0 477 // quickly.
Chris@0 478 if (!isset($this->alterFunctions[$cid])) {
Chris@0 479 $this->alterFunctions[$cid] = [];
Chris@0 480 $hook = $type . '_alter';
Chris@0 481 $modules = $this->getImplementations($hook);
Chris@0 482 if (!isset($extra_types)) {
Chris@0 483 // For the more common case of a single hook, we do not need to call
Chris@0 484 // function_exists(), since $this->getImplementations() returns only
Chris@0 485 // modules with implementations.
Chris@0 486 foreach ($modules as $module) {
Chris@0 487 $this->alterFunctions[$cid][] = $module . '_' . $hook;
Chris@0 488 }
Chris@0 489 }
Chris@0 490 else {
Chris@0 491 // For multiple hooks, we need $modules to contain every module that
Chris@0 492 // implements at least one of them.
Chris@0 493 $extra_modules = [];
Chris@0 494 foreach ($extra_types as $extra_type) {
Chris@0 495 $extra_modules = array_merge($extra_modules, $this->getImplementations($extra_type . '_alter'));
Chris@0 496 }
Chris@0 497 // If any modules implement one of the extra hooks that do not implement
Chris@0 498 // the primary hook, we need to add them to the $modules array in their
Chris@0 499 // appropriate order. $this->getImplementations() can only return
Chris@0 500 // ordered implementations of a single hook. To get the ordered
Chris@0 501 // implementations of multiple hooks, we mimic the
Chris@0 502 // $this->getImplementations() logic of first ordering by
Chris@0 503 // $this->getModuleList(), and then calling
Chris@0 504 // $this->alter('module_implements').
Chris@0 505 if (array_diff($extra_modules, $modules)) {
Chris@0 506 // Merge the arrays and order by getModuleList().
Chris@0 507 $modules = array_intersect(array_keys($this->moduleList), array_merge($modules, $extra_modules));
Chris@0 508 // Since $this->getImplementations() already took care of loading the
Chris@0 509 // necessary include files, we can safely pass FALSE for the array
Chris@0 510 // values.
Chris@0 511 $implementations = array_fill_keys($modules, FALSE);
Chris@0 512 // Let modules adjust the order solely based on the primary hook. This
Chris@0 513 // ensures the same module order regardless of whether this if block
Chris@0 514 // runs. Calling $this->alter() recursively in this way does not
Chris@0 515 // result in an infinite loop, because this call is for a single
Chris@0 516 // $type, so we won't end up in this code block again.
Chris@0 517 $this->alter('module_implements', $implementations, $hook);
Chris@0 518 $modules = array_keys($implementations);
Chris@0 519 }
Chris@0 520 foreach ($modules as $module) {
Chris@0 521 // Since $modules is a merged array, for any given module, we do not
Chris@0 522 // know whether it has any particular implementation, so we need a
Chris@0 523 // function_exists().
Chris@0 524 $function = $module . '_' . $hook;
Chris@0 525 if (function_exists($function)) {
Chris@0 526 $this->alterFunctions[$cid][] = $function;
Chris@0 527 }
Chris@0 528 foreach ($extra_types as $extra_type) {
Chris@0 529 $function = $module . '_' . $extra_type . '_alter';
Chris@0 530 if (function_exists($function)) {
Chris@0 531 $this->alterFunctions[$cid][] = $function;
Chris@0 532 }
Chris@0 533 }
Chris@0 534 }
Chris@0 535 }
Chris@0 536 }
Chris@0 537
Chris@0 538 foreach ($this->alterFunctions[$cid] as $function) {
Chris@0 539 $function($data, $context1, $context2);
Chris@0 540 }
Chris@0 541 }
Chris@0 542
Chris@0 543 /**
Chris@14 544 * {@inheritdoc}
Chris@14 545 */
Chris@14 546 public function alterDeprecated($description, $type, &$data, &$context1 = NULL, &$context2 = NULL) {
Chris@14 547 // Invoke the alter hook. This has the side effect of populating
Chris@14 548 // $this->alterFunctions.
Chris@14 549 $this->alter($type, $data, $context1, $context2);
Chris@14 550 // The $type parameter can be an array. alter() will deal with this
Chris@14 551 // internally, but we have to extract the proper $cid in order to discover
Chris@14 552 // implementations.
Chris@14 553 $cid = $type;
Chris@14 554 if (is_array($type)) {
Chris@14 555 $cid = implode(',', $type);
Chris@14 556 $extra_types = $type;
Chris@14 557 $type = array_shift($extra_types);
Chris@14 558 }
Chris@14 559 if (!empty($this->alterFunctions[$cid])) {
Chris@14 560 $message = 'The deprecated alter hook hook_' . $type . '_alter() is implemented in these functions: ' . implode(', ', $this->alterFunctions[$cid]) . '.';
Chris@14 561 @trigger_error($message . ' ' . $description, E_USER_DEPRECATED);
Chris@14 562 }
Chris@14 563 }
Chris@14 564
Chris@14 565 /**
Chris@0 566 * Provides information about modules' implementations of a hook.
Chris@0 567 *
Chris@0 568 * @param string $hook
Chris@0 569 * The name of the hook (e.g. "help" or "menu").
Chris@0 570 *
Chris@0 571 * @return mixed[]
Chris@0 572 * An array whose keys are the names of the modules which are implementing
Chris@0 573 * this hook and whose values are either a string identifying a file in
Chris@0 574 * which the implementation is to be found, or FALSE, if the implementation
Chris@0 575 * is in the module file.
Chris@0 576 */
Chris@0 577 protected function getImplementationInfo($hook) {
Chris@0 578 if (!isset($this->implementations)) {
Chris@0 579 $this->implementations = [];
Chris@0 580 $this->verified = [];
Chris@0 581 if ($cache = $this->cacheBackend->get('module_implements')) {
Chris@0 582 $this->implementations = $cache->data;
Chris@0 583 }
Chris@0 584 }
Chris@0 585 if (!isset($this->implementations[$hook])) {
Chris@0 586 // The hook is not cached, so ensure that whether or not it has
Chris@0 587 // implementations, the cache is updated at the end of the request.
Chris@0 588 $this->cacheNeedsWriting = TRUE;
Chris@0 589 // Discover implementations.
Chris@0 590 $this->implementations[$hook] = $this->buildImplementationInfo($hook);
Chris@0 591 // Implementations are always "verified" as part of the discovery.
Chris@0 592 $this->verified[$hook] = TRUE;
Chris@0 593 }
Chris@0 594 elseif (!isset($this->verified[$hook])) {
Chris@0 595 if (!$this->verifyImplementations($this->implementations[$hook], $hook)) {
Chris@0 596 // One or more of the implementations did not exist and need to be
Chris@0 597 // removed in the cache.
Chris@0 598 $this->cacheNeedsWriting = TRUE;
Chris@0 599 }
Chris@0 600 $this->verified[$hook] = TRUE;
Chris@0 601 }
Chris@0 602 return $this->implementations[$hook];
Chris@0 603 }
Chris@0 604
Chris@0 605 /**
Chris@0 606 * Builds hook implementation information for a given hook name.
Chris@0 607 *
Chris@0 608 * @param string $hook
Chris@0 609 * The name of the hook (e.g. "help" or "menu").
Chris@0 610 *
Chris@0 611 * @return mixed[]
Chris@0 612 * An array whose keys are the names of the modules which are implementing
Chris@0 613 * this hook and whose values are either a string identifying a file in
Chris@0 614 * which the implementation is to be found, or FALSE, if the implementation
Chris@0 615 * is in the module file.
Chris@0 616 *
Chris@0 617 * @throws \RuntimeException
Chris@0 618 * Exception thrown when an invalid implementation is added by
Chris@0 619 * hook_module_implements_alter().
Chris@0 620 *
Chris@0 621 * @see \Drupal\Core\Extension\ModuleHandler::getImplementationInfo()
Chris@0 622 */
Chris@0 623 protected function buildImplementationInfo($hook) {
Chris@0 624 $implementations = [];
Chris@0 625 $hook_info = $this->getHookInfo();
Chris@0 626 foreach ($this->moduleList as $module => $extension) {
Chris@0 627 $include_file = isset($hook_info[$hook]['group']) && $this->loadInclude($module, 'inc', $module . '.' . $hook_info[$hook]['group']);
Chris@0 628 // Since $this->implementsHook() may needlessly try to load the include
Chris@0 629 // file again, function_exists() is used directly here.
Chris@0 630 if (function_exists($module . '_' . $hook)) {
Chris@0 631 $implementations[$module] = $include_file ? $hook_info[$hook]['group'] : FALSE;
Chris@0 632 }
Chris@0 633 }
Chris@0 634 // Allow modules to change the weight of specific implementations, but avoid
Chris@0 635 // an infinite loop.
Chris@0 636 if ($hook != 'module_implements_alter') {
Chris@0 637 // Remember the original implementations, before they are modified with
Chris@0 638 // hook_module_implements_alter().
Chris@0 639 $implementations_before = $implementations;
Chris@0 640 // Verify implementations that were added or modified.
Chris@0 641 $this->alter('module_implements', $implementations, $hook);
Chris@0 642 // Verify new or modified implementations.
Chris@0 643 foreach (array_diff_assoc($implementations, $implementations_before) as $module => $group) {
Chris@0 644 // If an implementation of hook_module_implements_alter() changed or
Chris@0 645 // added a group, the respective file needs to be included.
Chris@0 646 if ($group) {
Chris@0 647 $this->loadInclude($module, 'inc', "$module.$group");
Chris@0 648 }
Chris@0 649 // If a new implementation was added, verify that the function exists.
Chris@0 650 if (!function_exists($module . '_' . $hook)) {
Chris@0 651 throw new \RuntimeException("An invalid implementation {$module}_{$hook} was added by hook_module_implements_alter()");
Chris@0 652 }
Chris@0 653 }
Chris@0 654 }
Chris@0 655 return $implementations;
Chris@0 656 }
Chris@0 657
Chris@0 658 /**
Chris@0 659 * Verifies an array of implementations loaded from the cache, by including
Chris@0 660 * the lazy-loaded $module.$group.inc, and checking function_exists().
Chris@0 661 *
Chris@0 662 * @param string[] $implementations
Chris@0 663 * Implementation "group" by module name.
Chris@0 664 * @param string $hook
Chris@0 665 * The hook name.
Chris@0 666 *
Chris@0 667 * @return bool
Chris@0 668 * TRUE, if all implementations exist.
Chris@0 669 * FALSE, if one or more implementations don't exist and need to be removed
Chris@0 670 * from the cache.
Chris@0 671 */
Chris@0 672 protected function verifyImplementations(&$implementations, $hook) {
Chris@0 673 $all_valid = TRUE;
Chris@0 674 foreach ($implementations as $module => $group) {
Chris@0 675 // If this hook implementation is stored in a lazy-loaded file, include
Chris@0 676 // that file first.
Chris@0 677 if ($group) {
Chris@0 678 $this->loadInclude($module, 'inc', "$module.$group");
Chris@0 679 }
Chris@0 680 // It is possible that a module removed a hook implementation without
Chris@0 681 // the implementations cache being rebuilt yet, so we check whether the
Chris@0 682 // function exists on each request to avoid undefined function errors.
Chris@0 683 // Since ModuleHandler::implementsHook() may needlessly try to
Chris@0 684 // load the include file again, function_exists() is used directly here.
Chris@0 685 if (!function_exists($module . '_' . $hook)) {
Chris@0 686 // Clear out the stale implementation from the cache and force a cache
Chris@0 687 // refresh to forget about no longer existing hook implementations.
Chris@0 688 unset($implementations[$module]);
Chris@0 689 // One of the implementations did not exist and needs to be removed in
Chris@0 690 // the cache.
Chris@0 691 $all_valid = FALSE;
Chris@0 692 }
Chris@0 693 }
Chris@0 694 return $all_valid;
Chris@0 695 }
Chris@0 696
Chris@0 697 /**
Chris@0 698 * Parses a dependency for comparison by drupal_check_incompatibility().
Chris@0 699 *
Chris@0 700 * @param $dependency
Chris@0 701 * A dependency string, which specifies a module dependency, and optionally
Chris@0 702 * the project it comes from and versions that are supported. Supported
Chris@0 703 * formats include:
Chris@0 704 * - 'module'
Chris@0 705 * - 'project:module'
Chris@0 706 * - 'project:module (>=version, version)'
Chris@0 707 *
Chris@0 708 * @return
Chris@0 709 * An associative array with three keys:
Chris@0 710 * - 'name' includes the name of the thing to depend on (e.g. 'foo').
Chris@0 711 * - 'original_version' contains the original version string (which can be
Chris@0 712 * used in the UI for reporting incompatibilities).
Chris@0 713 * - 'versions' is a list of associative arrays, each containing the keys
Chris@0 714 * 'op' and 'version'. 'op' can be one of: '=', '==', '!=', '<>', '<',
Chris@0 715 * '<=', '>', or '>='. 'version' is one piece like '4.5-beta3'.
Chris@0 716 * Callers should pass this structure to drupal_check_incompatibility().
Chris@0 717 *
Chris@0 718 * @see drupal_check_incompatibility()
Chris@0 719 */
Chris@0 720 public static function parseDependency($dependency) {
Chris@0 721 $value = [];
Chris@0 722 // Split out the optional project name.
Chris@0 723 if (strpos($dependency, ':') !== FALSE) {
Chris@0 724 list($project_name, $dependency) = explode(':', $dependency);
Chris@0 725 $value['project'] = $project_name;
Chris@0 726 }
Chris@0 727 // We use named subpatterns and support every op that version_compare
Chris@0 728 // supports. Also, op is optional and defaults to equals.
Chris@0 729 $p_op = '(?<operation>!=|==|=|<|<=|>|>=|<>)?';
Chris@0 730 // Core version is always optional: 8.x-2.x and 2.x is treated the same.
Chris@0 731 $p_core = '(?:' . preg_quote(\Drupal::CORE_COMPATIBILITY) . '-)?';
Chris@0 732 $p_major = '(?<major>\d+)';
Chris@0 733 // By setting the minor version to x, branches can be matched.
Chris@0 734 $p_minor = '(?<minor>(?:\d+|x)(?:-[A-Za-z]+\d+)?)';
Chris@0 735 $parts = explode('(', $dependency, 2);
Chris@0 736 $value['name'] = trim($parts[0]);
Chris@0 737 if (isset($parts[1])) {
Chris@0 738 $value['original_version'] = ' (' . $parts[1];
Chris@0 739 foreach (explode(',', $parts[1]) as $version) {
Chris@0 740 if (preg_match("/^\s*$p_op\s*$p_core$p_major\.$p_minor/", $version, $matches)) {
Chris@0 741 $op = !empty($matches['operation']) ? $matches['operation'] : '=';
Chris@0 742 if ($matches['minor'] == 'x') {
Chris@0 743 // Drupal considers "2.x" to mean any version that begins with
Chris@0 744 // "2" (e.g. 2.0, 2.9 are all "2.x"). PHP's version_compare(),
Chris@0 745 // on the other hand, treats "x" as a string; so to
Chris@0 746 // version_compare(), "2.x" is considered less than 2.0. This
Chris@0 747 // means that >=2.x and <2.x are handled by version_compare()
Chris@0 748 // as we need, but > and <= are not.
Chris@0 749 if ($op == '>' || $op == '<=') {
Chris@0 750 $matches['major']++;
Chris@0 751 }
Chris@0 752 // Equivalence can be checked by adding two restrictions.
Chris@0 753 if ($op == '=' || $op == '==') {
Chris@0 754 $value['versions'][] = ['op' => '<', 'version' => ($matches['major'] + 1) . '.x'];
Chris@0 755 $op = '>=';
Chris@0 756 }
Chris@0 757 }
Chris@0 758 $value['versions'][] = ['op' => $op, 'version' => $matches['major'] . '.' . $matches['minor']];
Chris@0 759 }
Chris@0 760 }
Chris@0 761 }
Chris@0 762 return $value;
Chris@0 763 }
Chris@0 764
Chris@0 765 /**
Chris@0 766 * {@inheritdoc}
Chris@0 767 */
Chris@0 768 public function getModuleDirectories() {
Chris@0 769 $dirs = [];
Chris@0 770 foreach ($this->getModuleList() as $name => $module) {
Chris@0 771 $dirs[$name] = $this->root . '/' . $module->getPath();
Chris@0 772 }
Chris@0 773 return $dirs;
Chris@0 774 }
Chris@0 775
Chris@0 776 /**
Chris@0 777 * {@inheritdoc}
Chris@0 778 */
Chris@0 779 public function getName($module) {
Chris@0 780 $info = system_get_info('module', $module);
Chris@0 781 return isset($info['name']) ? $info['name'] : $module;
Chris@0 782 }
Chris@0 783
Chris@0 784 }