annotate core/lib/Drupal/Core/Extension/ModuleHandler.php @ 12:7a779792577d

Update Drupal core to v8.4.5 (via Composer)
author Chris Cannam
date Fri, 23 Feb 2018 15:52:07 +0000
parents 4c8ae668cc8c
children 1fec387a4317
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@0 417 public function alter($type, &$data, &$context1 = NULL, &$context2 = NULL) {
Chris@0 418 // Most of the time, $type is passed as a string, so for performance,
Chris@0 419 // normalize it to that. When passed as an array, usually the first item in
Chris@0 420 // the array is a generic type, and additional items in the array are more
Chris@0 421 // specific variants of it, as in the case of array('form', 'form_FORM_ID').
Chris@0 422 if (is_array($type)) {
Chris@0 423 $cid = implode(',', $type);
Chris@0 424 $extra_types = $type;
Chris@0 425 $type = array_shift($extra_types);
Chris@0 426 // Allow if statements in this function to use the faster isset() rather
Chris@0 427 // than !empty() both when $type is passed as a string, or as an array
Chris@0 428 // with one item.
Chris@0 429 if (empty($extra_types)) {
Chris@0 430 unset($extra_types);
Chris@0 431 }
Chris@0 432 }
Chris@0 433 else {
Chris@0 434 $cid = $type;
Chris@0 435 }
Chris@0 436
Chris@0 437 // Some alter hooks are invoked many times per page request, so store the
Chris@0 438 // list of functions to call, and on subsequent calls, iterate through them
Chris@0 439 // quickly.
Chris@0 440 if (!isset($this->alterFunctions[$cid])) {
Chris@0 441 $this->alterFunctions[$cid] = [];
Chris@0 442 $hook = $type . '_alter';
Chris@0 443 $modules = $this->getImplementations($hook);
Chris@0 444 if (!isset($extra_types)) {
Chris@0 445 // For the more common case of a single hook, we do not need to call
Chris@0 446 // function_exists(), since $this->getImplementations() returns only
Chris@0 447 // modules with implementations.
Chris@0 448 foreach ($modules as $module) {
Chris@0 449 $this->alterFunctions[$cid][] = $module . '_' . $hook;
Chris@0 450 }
Chris@0 451 }
Chris@0 452 else {
Chris@0 453 // For multiple hooks, we need $modules to contain every module that
Chris@0 454 // implements at least one of them.
Chris@0 455 $extra_modules = [];
Chris@0 456 foreach ($extra_types as $extra_type) {
Chris@0 457 $extra_modules = array_merge($extra_modules, $this->getImplementations($extra_type . '_alter'));
Chris@0 458 }
Chris@0 459 // If any modules implement one of the extra hooks that do not implement
Chris@0 460 // the primary hook, we need to add them to the $modules array in their
Chris@0 461 // appropriate order. $this->getImplementations() can only return
Chris@0 462 // ordered implementations of a single hook. To get the ordered
Chris@0 463 // implementations of multiple hooks, we mimic the
Chris@0 464 // $this->getImplementations() logic of first ordering by
Chris@0 465 // $this->getModuleList(), and then calling
Chris@0 466 // $this->alter('module_implements').
Chris@0 467 if (array_diff($extra_modules, $modules)) {
Chris@0 468 // Merge the arrays and order by getModuleList().
Chris@0 469 $modules = array_intersect(array_keys($this->moduleList), array_merge($modules, $extra_modules));
Chris@0 470 // Since $this->getImplementations() already took care of loading the
Chris@0 471 // necessary include files, we can safely pass FALSE for the array
Chris@0 472 // values.
Chris@0 473 $implementations = array_fill_keys($modules, FALSE);
Chris@0 474 // Let modules adjust the order solely based on the primary hook. This
Chris@0 475 // ensures the same module order regardless of whether this if block
Chris@0 476 // runs. Calling $this->alter() recursively in this way does not
Chris@0 477 // result in an infinite loop, because this call is for a single
Chris@0 478 // $type, so we won't end up in this code block again.
Chris@0 479 $this->alter('module_implements', $implementations, $hook);
Chris@0 480 $modules = array_keys($implementations);
Chris@0 481 }
Chris@0 482 foreach ($modules as $module) {
Chris@0 483 // Since $modules is a merged array, for any given module, we do not
Chris@0 484 // know whether it has any particular implementation, so we need a
Chris@0 485 // function_exists().
Chris@0 486 $function = $module . '_' . $hook;
Chris@0 487 if (function_exists($function)) {
Chris@0 488 $this->alterFunctions[$cid][] = $function;
Chris@0 489 }
Chris@0 490 foreach ($extra_types as $extra_type) {
Chris@0 491 $function = $module . '_' . $extra_type . '_alter';
Chris@0 492 if (function_exists($function)) {
Chris@0 493 $this->alterFunctions[$cid][] = $function;
Chris@0 494 }
Chris@0 495 }
Chris@0 496 }
Chris@0 497 }
Chris@0 498 }
Chris@0 499
Chris@0 500 foreach ($this->alterFunctions[$cid] as $function) {
Chris@0 501 $function($data, $context1, $context2);
Chris@0 502 }
Chris@0 503 }
Chris@0 504
Chris@0 505 /**
Chris@0 506 * Provides information about modules' implementations of a hook.
Chris@0 507 *
Chris@0 508 * @param string $hook
Chris@0 509 * The name of the hook (e.g. "help" or "menu").
Chris@0 510 *
Chris@0 511 * @return mixed[]
Chris@0 512 * An array whose keys are the names of the modules which are implementing
Chris@0 513 * this hook and whose values are either a string identifying a file in
Chris@0 514 * which the implementation is to be found, or FALSE, if the implementation
Chris@0 515 * is in the module file.
Chris@0 516 */
Chris@0 517 protected function getImplementationInfo($hook) {
Chris@0 518 if (!isset($this->implementations)) {
Chris@0 519 $this->implementations = [];
Chris@0 520 $this->verified = [];
Chris@0 521 if ($cache = $this->cacheBackend->get('module_implements')) {
Chris@0 522 $this->implementations = $cache->data;
Chris@0 523 }
Chris@0 524 }
Chris@0 525 if (!isset($this->implementations[$hook])) {
Chris@0 526 // The hook is not cached, so ensure that whether or not it has
Chris@0 527 // implementations, the cache is updated at the end of the request.
Chris@0 528 $this->cacheNeedsWriting = TRUE;
Chris@0 529 // Discover implementations.
Chris@0 530 $this->implementations[$hook] = $this->buildImplementationInfo($hook);
Chris@0 531 // Implementations are always "verified" as part of the discovery.
Chris@0 532 $this->verified[$hook] = TRUE;
Chris@0 533 }
Chris@0 534 elseif (!isset($this->verified[$hook])) {
Chris@0 535 if (!$this->verifyImplementations($this->implementations[$hook], $hook)) {
Chris@0 536 // One or more of the implementations did not exist and need to be
Chris@0 537 // removed in the cache.
Chris@0 538 $this->cacheNeedsWriting = TRUE;
Chris@0 539 }
Chris@0 540 $this->verified[$hook] = TRUE;
Chris@0 541 }
Chris@0 542 return $this->implementations[$hook];
Chris@0 543 }
Chris@0 544
Chris@0 545 /**
Chris@0 546 * Builds hook implementation information for a given hook name.
Chris@0 547 *
Chris@0 548 * @param string $hook
Chris@0 549 * The name of the hook (e.g. "help" or "menu").
Chris@0 550 *
Chris@0 551 * @return mixed[]
Chris@0 552 * An array whose keys are the names of the modules which are implementing
Chris@0 553 * this hook and whose values are either a string identifying a file in
Chris@0 554 * which the implementation is to be found, or FALSE, if the implementation
Chris@0 555 * is in the module file.
Chris@0 556 *
Chris@0 557 * @throws \RuntimeException
Chris@0 558 * Exception thrown when an invalid implementation is added by
Chris@0 559 * hook_module_implements_alter().
Chris@0 560 *
Chris@0 561 * @see \Drupal\Core\Extension\ModuleHandler::getImplementationInfo()
Chris@0 562 */
Chris@0 563 protected function buildImplementationInfo($hook) {
Chris@0 564 $implementations = [];
Chris@0 565 $hook_info = $this->getHookInfo();
Chris@0 566 foreach ($this->moduleList as $module => $extension) {
Chris@0 567 $include_file = isset($hook_info[$hook]['group']) && $this->loadInclude($module, 'inc', $module . '.' . $hook_info[$hook]['group']);
Chris@0 568 // Since $this->implementsHook() may needlessly try to load the include
Chris@0 569 // file again, function_exists() is used directly here.
Chris@0 570 if (function_exists($module . '_' . $hook)) {
Chris@0 571 $implementations[$module] = $include_file ? $hook_info[$hook]['group'] : FALSE;
Chris@0 572 }
Chris@0 573 }
Chris@0 574 // Allow modules to change the weight of specific implementations, but avoid
Chris@0 575 // an infinite loop.
Chris@0 576 if ($hook != 'module_implements_alter') {
Chris@0 577 // Remember the original implementations, before they are modified with
Chris@0 578 // hook_module_implements_alter().
Chris@0 579 $implementations_before = $implementations;
Chris@0 580 // Verify implementations that were added or modified.
Chris@0 581 $this->alter('module_implements', $implementations, $hook);
Chris@0 582 // Verify new or modified implementations.
Chris@0 583 foreach (array_diff_assoc($implementations, $implementations_before) as $module => $group) {
Chris@0 584 // If an implementation of hook_module_implements_alter() changed or
Chris@0 585 // added a group, the respective file needs to be included.
Chris@0 586 if ($group) {
Chris@0 587 $this->loadInclude($module, 'inc', "$module.$group");
Chris@0 588 }
Chris@0 589 // If a new implementation was added, verify that the function exists.
Chris@0 590 if (!function_exists($module . '_' . $hook)) {
Chris@0 591 throw new \RuntimeException("An invalid implementation {$module}_{$hook} was added by hook_module_implements_alter()");
Chris@0 592 }
Chris@0 593 }
Chris@0 594 }
Chris@0 595 return $implementations;
Chris@0 596 }
Chris@0 597
Chris@0 598 /**
Chris@0 599 * Verifies an array of implementations loaded from the cache, by including
Chris@0 600 * the lazy-loaded $module.$group.inc, and checking function_exists().
Chris@0 601 *
Chris@0 602 * @param string[] $implementations
Chris@0 603 * Implementation "group" by module name.
Chris@0 604 * @param string $hook
Chris@0 605 * The hook name.
Chris@0 606 *
Chris@0 607 * @return bool
Chris@0 608 * TRUE, if all implementations exist.
Chris@0 609 * FALSE, if one or more implementations don't exist and need to be removed
Chris@0 610 * from the cache.
Chris@0 611 */
Chris@0 612 protected function verifyImplementations(&$implementations, $hook) {
Chris@0 613 $all_valid = TRUE;
Chris@0 614 foreach ($implementations as $module => $group) {
Chris@0 615 // If this hook implementation is stored in a lazy-loaded file, include
Chris@0 616 // that file first.
Chris@0 617 if ($group) {
Chris@0 618 $this->loadInclude($module, 'inc', "$module.$group");
Chris@0 619 }
Chris@0 620 // It is possible that a module removed a hook implementation without
Chris@0 621 // the implementations cache being rebuilt yet, so we check whether the
Chris@0 622 // function exists on each request to avoid undefined function errors.
Chris@0 623 // Since ModuleHandler::implementsHook() may needlessly try to
Chris@0 624 // load the include file again, function_exists() is used directly here.
Chris@0 625 if (!function_exists($module . '_' . $hook)) {
Chris@0 626 // Clear out the stale implementation from the cache and force a cache
Chris@0 627 // refresh to forget about no longer existing hook implementations.
Chris@0 628 unset($implementations[$module]);
Chris@0 629 // One of the implementations did not exist and needs to be removed in
Chris@0 630 // the cache.
Chris@0 631 $all_valid = FALSE;
Chris@0 632 }
Chris@0 633 }
Chris@0 634 return $all_valid;
Chris@0 635 }
Chris@0 636
Chris@0 637 /**
Chris@0 638 * Parses a dependency for comparison by drupal_check_incompatibility().
Chris@0 639 *
Chris@0 640 * @param $dependency
Chris@0 641 * A dependency string, which specifies a module dependency, and optionally
Chris@0 642 * the project it comes from and versions that are supported. Supported
Chris@0 643 * formats include:
Chris@0 644 * - 'module'
Chris@0 645 * - 'project:module'
Chris@0 646 * - 'project:module (>=version, version)'
Chris@0 647 *
Chris@0 648 * @return
Chris@0 649 * An associative array with three keys:
Chris@0 650 * - 'name' includes the name of the thing to depend on (e.g. 'foo').
Chris@0 651 * - 'original_version' contains the original version string (which can be
Chris@0 652 * used in the UI for reporting incompatibilities).
Chris@0 653 * - 'versions' is a list of associative arrays, each containing the keys
Chris@0 654 * 'op' and 'version'. 'op' can be one of: '=', '==', '!=', '<>', '<',
Chris@0 655 * '<=', '>', or '>='. 'version' is one piece like '4.5-beta3'.
Chris@0 656 * Callers should pass this structure to drupal_check_incompatibility().
Chris@0 657 *
Chris@0 658 * @see drupal_check_incompatibility()
Chris@0 659 */
Chris@0 660 public static function parseDependency($dependency) {
Chris@0 661 $value = [];
Chris@0 662 // Split out the optional project name.
Chris@0 663 if (strpos($dependency, ':') !== FALSE) {
Chris@0 664 list($project_name, $dependency) = explode(':', $dependency);
Chris@0 665 $value['project'] = $project_name;
Chris@0 666 }
Chris@0 667 // We use named subpatterns and support every op that version_compare
Chris@0 668 // supports. Also, op is optional and defaults to equals.
Chris@0 669 $p_op = '(?<operation>!=|==|=|<|<=|>|>=|<>)?';
Chris@0 670 // Core version is always optional: 8.x-2.x and 2.x is treated the same.
Chris@0 671 $p_core = '(?:' . preg_quote(\Drupal::CORE_COMPATIBILITY) . '-)?';
Chris@0 672 $p_major = '(?<major>\d+)';
Chris@0 673 // By setting the minor version to x, branches can be matched.
Chris@0 674 $p_minor = '(?<minor>(?:\d+|x)(?:-[A-Za-z]+\d+)?)';
Chris@0 675 $parts = explode('(', $dependency, 2);
Chris@0 676 $value['name'] = trim($parts[0]);
Chris@0 677 if (isset($parts[1])) {
Chris@0 678 $value['original_version'] = ' (' . $parts[1];
Chris@0 679 foreach (explode(',', $parts[1]) as $version) {
Chris@0 680 if (preg_match("/^\s*$p_op\s*$p_core$p_major\.$p_minor/", $version, $matches)) {
Chris@0 681 $op = !empty($matches['operation']) ? $matches['operation'] : '=';
Chris@0 682 if ($matches['minor'] == 'x') {
Chris@0 683 // Drupal considers "2.x" to mean any version that begins with
Chris@0 684 // "2" (e.g. 2.0, 2.9 are all "2.x"). PHP's version_compare(),
Chris@0 685 // on the other hand, treats "x" as a string; so to
Chris@0 686 // version_compare(), "2.x" is considered less than 2.0. This
Chris@0 687 // means that >=2.x and <2.x are handled by version_compare()
Chris@0 688 // as we need, but > and <= are not.
Chris@0 689 if ($op == '>' || $op == '<=') {
Chris@0 690 $matches['major']++;
Chris@0 691 }
Chris@0 692 // Equivalence can be checked by adding two restrictions.
Chris@0 693 if ($op == '=' || $op == '==') {
Chris@0 694 $value['versions'][] = ['op' => '<', 'version' => ($matches['major'] + 1) . '.x'];
Chris@0 695 $op = '>=';
Chris@0 696 }
Chris@0 697 }
Chris@0 698 $value['versions'][] = ['op' => $op, 'version' => $matches['major'] . '.' . $matches['minor']];
Chris@0 699 }
Chris@0 700 }
Chris@0 701 }
Chris@0 702 return $value;
Chris@0 703 }
Chris@0 704
Chris@0 705 /**
Chris@0 706 * {@inheritdoc}
Chris@0 707 */
Chris@0 708 public function getModuleDirectories() {
Chris@0 709 $dirs = [];
Chris@0 710 foreach ($this->getModuleList() as $name => $module) {
Chris@0 711 $dirs[$name] = $this->root . '/' . $module->getPath();
Chris@0 712 }
Chris@0 713 return $dirs;
Chris@0 714 }
Chris@0 715
Chris@0 716 /**
Chris@0 717 * {@inheritdoc}
Chris@0 718 */
Chris@0 719 public function getName($module) {
Chris@0 720 $info = system_get_info('module', $module);
Chris@0 721 return isset($info['name']) ? $info['name'] : $module;
Chris@0 722 }
Chris@0 723
Chris@0 724 }