3 namespace Drupal\Core\Extension;
5 use Drupal\Component\Graph\Graph;
6 use Drupal\Component\Utility\NestedArray;
7 use Drupal\Core\Cache\CacheBackendInterface;
8 use Drupal\Core\Extension\Exception\UnknownExtensionException;
11 * Class that manages modules in a Drupal installation.
13 class ModuleHandler implements ModuleHandlerInterface {
16 * List of loaded files.
19 * An associative array whose keys are file paths of loaded files, relative
20 * to the application's root directory.
22 protected $loadedFiles;
25 * List of installed modules.
27 * @var \Drupal\Core\Extension\Extension[]
29 protected $moduleList;
32 * Boolean indicating whether modules have been loaded.
36 protected $loaded = FALSE;
39 * List of hook implementations keyed by hook name.
43 protected $implementations;
46 * List of hooks where the implementations have been "verified".
49 * Associative array where keys are hook names.
54 * Information returned by hook_hook_info() implementations.
61 * Cache backend for storing module hook implementation information.
63 * @var \Drupal\Core\Cache\CacheBackendInterface
65 protected $cacheBackend;
68 * Whether the cache needs to be written.
72 protected $cacheNeedsWriting = FALSE;
75 * List of alter hook implementations keyed by hook name(s).
79 protected $alterFunctions;
89 * A list of module include file keys.
93 protected $includeFileKeys = [];
96 * Constructs a ModuleHandler object.
100 * @param array $module_list
101 * An associative array whose keys are the names of installed modules and
102 * whose values are Extension class parameters. This is normally the
103 * %container.modules% parameter being set up by DrupalKernel.
104 * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
105 * Cache backend for storing module hook implementation information.
107 * @see \Drupal\Core\DrupalKernel
108 * @see \Drupal\Core\CoreServiceProvider
110 public function __construct($root, array $module_list, CacheBackendInterface $cache_backend) {
112 $this->moduleList = [];
113 foreach ($module_list as $name => $module) {
114 $this->moduleList[$name] = new Extension($this->root, $module['type'], $module['pathname'], $module['filename']);
116 $this->cacheBackend = $cache_backend;
122 public function load($name) {
123 if (isset($this->loadedFiles[$name])) {
127 if (isset($this->moduleList[$name])) {
128 $this->moduleList[$name]->load();
129 $this->loadedFiles[$name] = TRUE;
138 public function loadAll() {
139 if (!$this->loaded) {
140 foreach ($this->moduleList as $name => $module) {
143 $this->loaded = TRUE;
150 public function reload() {
151 $this->loaded = FALSE;
158 public function isLoaded() {
159 return $this->loaded;
165 public function getModuleList() {
166 return $this->moduleList;
172 public function getModule($name) {
173 if (isset($this->moduleList[$name])) {
174 return $this->moduleList[$name];
176 throw new UnknownExtensionException(sprintf('The module %s does not exist.', $name));
182 public function setModuleList(array $module_list = []) {
183 $this->moduleList = $module_list;
184 // Reset the implementations, so a new call triggers a reloading of the
186 $this->resetImplementations();
192 public function addModule($name, $path) {
193 $this->add('module', $name, $path);
199 public function addProfile($name, $path) {
200 $this->add('profile', $name, $path);
204 * Adds a module or profile to the list of currently active modules.
206 * @param string $type
207 * The extension type; either 'module' or 'profile'.
208 * @param string $name
209 * The module name; e.g., 'node'.
210 * @param string $path
211 * The module path; e.g., 'core/modules/node'.
213 protected function add($type, $name, $path) {
214 $pathname = "$path/$name.info.yml";
215 $filename = file_exists($this->root . "/$path/$name.$type") ? "$name.$type" : NULL;
216 $this->moduleList[$name] = new Extension($this->root, $type, $pathname, $filename);
217 $this->resetImplementations();
223 public function buildModuleDependencies(array $modules) {
224 foreach ($modules as $module) {
225 $graph[$module->getName()]['edges'] = [];
226 if (isset($module->info['dependencies']) && is_array($module->info['dependencies'])) {
227 foreach ($module->info['dependencies'] as $dependency) {
228 $dependency_data = static::parseDependency($dependency);
229 $graph[$module->getName()]['edges'][$dependency_data['name']] = $dependency_data;
233 $graph_object = new Graph($graph);
234 $graph = $graph_object->searchAndSort();
235 foreach ($graph as $module_name => $data) {
236 $modules[$module_name]->required_by = isset($data['reverse_paths']) ? $data['reverse_paths'] : [];
237 $modules[$module_name]->requires = isset($data['paths']) ? $data['paths'] : [];
238 $modules[$module_name]->sort = $data['weight'];
246 public function moduleExists($module) {
247 return isset($this->moduleList[$module]);
253 public function loadAllIncludes($type, $name = NULL) {
254 foreach ($this->moduleList as $module => $filename) {
255 $this->loadInclude($module, $type, $name);
262 public function loadInclude($module, $type, $name = NULL) {
263 if ($type == 'install') {
264 // Make sure the installation API is available
265 include_once $this->root . '/core/includes/install.inc';
268 $name = $name ?: $module;
269 $key = $type . ':' . $module . ':' . $name;
270 if (isset($this->includeFileKeys[$key])) {
271 return $this->includeFileKeys[$key];
273 if (isset($this->moduleList[$module])) {
274 $file = $this->root . '/' . $this->moduleList[$module]->getPath() . "/$name.$type";
275 if (is_file($file)) {
277 $this->includeFileKeys[$key] = $file;
281 $this->includeFileKeys[$key] = FALSE;
290 public function getHookInfo() {
291 if (!isset($this->hookInfo)) {
292 if ($cache = $this->cacheBackend->get('hook_info')) {
293 $this->hookInfo = $cache->data;
296 $this->buildHookInfo();
297 $this->cacheBackend->set('hook_info', $this->hookInfo);
300 return $this->hookInfo;
304 * Builds hook_hook_info() information.
306 * @see \Drupal\Core\Extension\ModuleHandler::getHookInfo()
308 protected function buildHookInfo() {
309 $this->hookInfo = [];
310 // Make sure that the modules are loaded before checking.
312 // $this->invokeAll() would cause an infinite recursion.
313 foreach ($this->moduleList as $module => $filename) {
314 $function = $module . '_hook_info';
315 if (function_exists($function)) {
316 $result = $function();
317 if (isset($result) && is_array($result)) {
318 $this->hookInfo = NestedArray::mergeDeep($this->hookInfo, $result);
327 public function getImplementations($hook) {
328 $implementations = $this->getImplementationInfo($hook);
329 return array_keys($implementations);
335 public function writeCache() {
336 if ($this->cacheNeedsWriting) {
337 $this->cacheBackend->set('module_implements', $this->implementations);
338 $this->cacheNeedsWriting = FALSE;
345 public function resetImplementations() {
346 $this->implementations = NULL;
347 $this->hookInfo = NULL;
348 $this->alterFunctions = NULL;
349 // We maintain a persistent cache of hook implementations in addition to the
350 // static cache to avoid looping through every module and every hook on each
351 // request. Benchmarks show that the benefit of this caching outweighs the
352 // additional database hit even when using the default database caching
353 // backend and only a small number of modules are enabled. The cost of the
354 // $this->cacheBackend->get() is more or less constant and reduced further
355 // when non-database caching backends are used, so there will be more
356 // significant gains when a large number of modules are installed or hooks
357 // invoked, since this can quickly lead to
358 // \Drupal::moduleHandler()->implementsHook() being called several thousand
359 // times per request.
360 $this->cacheBackend->set('module_implements', []);
361 $this->cacheBackend->delete('hook_info');
367 public function implementsHook($module, $hook) {
368 $function = $module . '_' . $hook;
369 if (function_exists($function)) {
372 // If the hook implementation does not exist, check whether it lives in an
373 // optional include file registered via hook_hook_info().
374 $hook_info = $this->getHookInfo();
375 if (isset($hook_info[$hook]['group'])) {
376 $this->loadInclude($module, 'inc', $module . '.' . $hook_info[$hook]['group']);
377 if (function_exists($function)) {
387 public function invoke($module, $hook, array $args = []) {
388 if (!$this->implementsHook($module, $hook)) {
391 $function = $module . '_' . $hook;
392 return call_user_func_array($function, $args);
398 public function invokeAll($hook, array $args = []) {
400 $implementations = $this->getImplementations($hook);
401 foreach ($implementations as $module) {
402 $function = $module . '_' . $hook;
403 $result = call_user_func_array($function, $args);
404 if (isset($result) && is_array($result)) {
405 $return = NestedArray::mergeDeep($return, $result);
407 elseif (isset($result)) {
418 public function invokeDeprecated($description, $module, $hook, array $args = []) {
419 $result = $this->invoke($module, $hook, $args);
420 $this->triggerDeprecationError($description, $hook);
427 public function invokeAllDeprecated($description, $hook, array $args = []) {
428 $result = $this->invokeAll($hook, $args);
429 $this->triggerDeprecationError($description, $hook);
434 * Triggers an E_USER_DEPRECATED error if any module implements the hook.
436 * @param string $description
437 * Helpful text describing what to do instead of implementing this hook.
438 * @param string $hook
439 * The name of the hook.
441 private function triggerDeprecationError($description, $hook) {
442 $modules = array_keys($this->getImplementationInfo($hook));
443 if (!empty($modules)) {
444 $message = 'The deprecated hook hook_' . $hook . '() is implemented in these functions: ';
445 $implementations = array_map(function ($module) use ($hook) {
446 return $module . '_' . $hook . '()';
448 @trigger_error($message . implode(', ', $implementations) . '. ' . $description, E_USER_DEPRECATED);
455 public function alter($type, &$data, &$context1 = NULL, &$context2 = NULL) {
456 // Most of the time, $type is passed as a string, so for performance,
457 // normalize it to that. When passed as an array, usually the first item in
458 // the array is a generic type, and additional items in the array are more
459 // specific variants of it, as in the case of array('form', 'form_FORM_ID').
460 if (is_array($type)) {
461 $cid = implode(',', $type);
462 $extra_types = $type;
463 $type = array_shift($extra_types);
464 // Allow if statements in this function to use the faster isset() rather
465 // than !empty() both when $type is passed as a string, or as an array
467 if (empty($extra_types)) {
475 // Some alter hooks are invoked many times per page request, so store the
476 // list of functions to call, and on subsequent calls, iterate through them
478 if (!isset($this->alterFunctions[$cid])) {
479 $this->alterFunctions[$cid] = [];
480 $hook = $type . '_alter';
481 $modules = $this->getImplementations($hook);
482 if (!isset($extra_types)) {
483 // For the more common case of a single hook, we do not need to call
484 // function_exists(), since $this->getImplementations() returns only
485 // modules with implementations.
486 foreach ($modules as $module) {
487 $this->alterFunctions[$cid][] = $module . '_' . $hook;
491 // For multiple hooks, we need $modules to contain every module that
492 // implements at least one of them.
494 foreach ($extra_types as $extra_type) {
495 $extra_modules = array_merge($extra_modules, $this->getImplementations($extra_type . '_alter'));
497 // If any modules implement one of the extra hooks that do not implement
498 // the primary hook, we need to add them to the $modules array in their
499 // appropriate order. $this->getImplementations() can only return
500 // ordered implementations of a single hook. To get the ordered
501 // implementations of multiple hooks, we mimic the
502 // $this->getImplementations() logic of first ordering by
503 // $this->getModuleList(), and then calling
504 // $this->alter('module_implements').
505 if (array_diff($extra_modules, $modules)) {
506 // Merge the arrays and order by getModuleList().
507 $modules = array_intersect(array_keys($this->moduleList), array_merge($modules, $extra_modules));
508 // Since $this->getImplementations() already took care of loading the
509 // necessary include files, we can safely pass FALSE for the array
511 $implementations = array_fill_keys($modules, FALSE);
512 // Let modules adjust the order solely based on the primary hook. This
513 // ensures the same module order regardless of whether this if block
514 // runs. Calling $this->alter() recursively in this way does not
515 // result in an infinite loop, because this call is for a single
516 // $type, so we won't end up in this code block again.
517 $this->alter('module_implements', $implementations, $hook);
518 $modules = array_keys($implementations);
520 foreach ($modules as $module) {
521 // Since $modules is a merged array, for any given module, we do not
522 // know whether it has any particular implementation, so we need a
523 // function_exists().
524 $function = $module . '_' . $hook;
525 if (function_exists($function)) {
526 $this->alterFunctions[$cid][] = $function;
528 foreach ($extra_types as $extra_type) {
529 $function = $module . '_' . $extra_type . '_alter';
530 if (function_exists($function)) {
531 $this->alterFunctions[$cid][] = $function;
538 foreach ($this->alterFunctions[$cid] as $function) {
539 $function($data, $context1, $context2);
546 public function alterDeprecated($description, $type, &$data, &$context1 = NULL, &$context2 = NULL) {
547 // Invoke the alter hook. This has the side effect of populating
548 // $this->alterFunctions.
549 $this->alter($type, $data, $context1, $context2);
550 // The $type parameter can be an array. alter() will deal with this
551 // internally, but we have to extract the proper $cid in order to discover
554 if (is_array($type)) {
555 $cid = implode(',', $type);
556 $extra_types = $type;
557 $type = array_shift($extra_types);
559 if (!empty($this->alterFunctions[$cid])) {
560 $message = 'The deprecated alter hook hook_' . $type . '_alter() is implemented in these functions: ' . implode(', ', $this->alterFunctions[$cid]) . '.';
561 @trigger_error($message . ' ' . $description, E_USER_DEPRECATED);
566 * Provides information about modules' implementations of a hook.
568 * @param string $hook
569 * The name of the hook (e.g. "help" or "menu").
572 * An array whose keys are the names of the modules which are implementing
573 * this hook and whose values are either a string identifying a file in
574 * which the implementation is to be found, or FALSE, if the implementation
575 * is in the module file.
577 protected function getImplementationInfo($hook) {
578 if (!isset($this->implementations)) {
579 $this->implementations = [];
580 $this->verified = [];
581 if ($cache = $this->cacheBackend->get('module_implements')) {
582 $this->implementations = $cache->data;
585 if (!isset($this->implementations[$hook])) {
586 // The hook is not cached, so ensure that whether or not it has
587 // implementations, the cache is updated at the end of the request.
588 $this->cacheNeedsWriting = TRUE;
589 // Discover implementations.
590 $this->implementations[$hook] = $this->buildImplementationInfo($hook);
591 // Implementations are always "verified" as part of the discovery.
592 $this->verified[$hook] = TRUE;
594 elseif (!isset($this->verified[$hook])) {
595 if (!$this->verifyImplementations($this->implementations[$hook], $hook)) {
596 // One or more of the implementations did not exist and need to be
597 // removed in the cache.
598 $this->cacheNeedsWriting = TRUE;
600 $this->verified[$hook] = TRUE;
602 return $this->implementations[$hook];
606 * Builds hook implementation information for a given hook name.
608 * @param string $hook
609 * The name of the hook (e.g. "help" or "menu").
612 * An array whose keys are the names of the modules which are implementing
613 * this hook and whose values are either a string identifying a file in
614 * which the implementation is to be found, or FALSE, if the implementation
615 * is in the module file.
617 * @throws \RuntimeException
618 * Exception thrown when an invalid implementation is added by
619 * hook_module_implements_alter().
621 * @see \Drupal\Core\Extension\ModuleHandler::getImplementationInfo()
623 protected function buildImplementationInfo($hook) {
624 $implementations = [];
625 $hook_info = $this->getHookInfo();
626 foreach ($this->moduleList as $module => $extension) {
627 $include_file = isset($hook_info[$hook]['group']) && $this->loadInclude($module, 'inc', $module . '.' . $hook_info[$hook]['group']);
628 // Since $this->implementsHook() may needlessly try to load the include
629 // file again, function_exists() is used directly here.
630 if (function_exists($module . '_' . $hook)) {
631 $implementations[$module] = $include_file ? $hook_info[$hook]['group'] : FALSE;
634 // Allow modules to change the weight of specific implementations, but avoid
636 if ($hook != 'module_implements_alter') {
637 // Remember the original implementations, before they are modified with
638 // hook_module_implements_alter().
639 $implementations_before = $implementations;
640 // Verify implementations that were added or modified.
641 $this->alter('module_implements', $implementations, $hook);
642 // Verify new or modified implementations.
643 foreach (array_diff_assoc($implementations, $implementations_before) as $module => $group) {
644 // If an implementation of hook_module_implements_alter() changed or
645 // added a group, the respective file needs to be included.
647 $this->loadInclude($module, 'inc', "$module.$group");
649 // If a new implementation was added, verify that the function exists.
650 if (!function_exists($module . '_' . $hook)) {
651 throw new \RuntimeException("An invalid implementation {$module}_{$hook} was added by hook_module_implements_alter()");
655 return $implementations;
659 * Verifies an array of implementations loaded from the cache, by including
660 * the lazy-loaded $module.$group.inc, and checking function_exists().
662 * @param string[] $implementations
663 * Implementation "group" by module name.
664 * @param string $hook
668 * TRUE, if all implementations exist.
669 * FALSE, if one or more implementations don't exist and need to be removed
672 protected function verifyImplementations(&$implementations, $hook) {
674 foreach ($implementations as $module => $group) {
675 // If this hook implementation is stored in a lazy-loaded file, include
678 $this->loadInclude($module, 'inc', "$module.$group");
680 // It is possible that a module removed a hook implementation without
681 // the implementations cache being rebuilt yet, so we check whether the
682 // function exists on each request to avoid undefined function errors.
683 // Since ModuleHandler::implementsHook() may needlessly try to
684 // load the include file again, function_exists() is used directly here.
685 if (!function_exists($module . '_' . $hook)) {
686 // Clear out the stale implementation from the cache and force a cache
687 // refresh to forget about no longer existing hook implementations.
688 unset($implementations[$module]);
689 // One of the implementations did not exist and needs to be removed in
698 * Parses a dependency for comparison by drupal_check_incompatibility().
701 * A dependency string, which specifies a module dependency, and optionally
702 * the project it comes from and versions that are supported. Supported
706 * - 'project:module (>=version, version)'
709 * An associative array with three keys:
710 * - 'name' includes the name of the thing to depend on (e.g. 'foo').
711 * - 'original_version' contains the original version string (which can be
712 * used in the UI for reporting incompatibilities).
713 * - 'versions' is a list of associative arrays, each containing the keys
714 * 'op' and 'version'. 'op' can be one of: '=', '==', '!=', '<>', '<',
715 * '<=', '>', or '>='. 'version' is one piece like '4.5-beta3'.
716 * Callers should pass this structure to drupal_check_incompatibility().
718 * @see drupal_check_incompatibility()
720 public static function parseDependency($dependency) {
722 // Split out the optional project name.
723 if (strpos($dependency, ':') !== FALSE) {
724 list($project_name, $dependency) = explode(':', $dependency);
725 $value['project'] = $project_name;
727 // We use named subpatterns and support every op that version_compare
728 // supports. Also, op is optional and defaults to equals.
729 $p_op = '(?<operation>!=|==|=|<|<=|>|>=|<>)?';
730 // Core version is always optional: 8.x-2.x and 2.x is treated the same.
731 $p_core = '(?:' . preg_quote(\Drupal::CORE_COMPATIBILITY) . '-)?';
732 $p_major = '(?<major>\d+)';
733 // By setting the minor version to x, branches can be matched.
734 $p_minor = '(?<minor>(?:\d+|x)(?:-[A-Za-z]+\d+)?)';
735 $parts = explode('(', $dependency, 2);
736 $value['name'] = trim($parts[0]);
737 if (isset($parts[1])) {
738 $value['original_version'] = ' (' . $parts[1];
739 foreach (explode(',', $parts[1]) as $version) {
740 if (preg_match("/^\s*$p_op\s*$p_core$p_major\.$p_minor/", $version, $matches)) {
741 $op = !empty($matches['operation']) ? $matches['operation'] : '=';
742 if ($matches['minor'] == 'x') {
743 // Drupal considers "2.x" to mean any version that begins with
744 // "2" (e.g. 2.0, 2.9 are all "2.x"). PHP's version_compare(),
745 // on the other hand, treats "x" as a string; so to
746 // version_compare(), "2.x" is considered less than 2.0. This
747 // means that >=2.x and <2.x are handled by version_compare()
748 // as we need, but > and <= are not.
749 if ($op == '>' || $op == '<=') {
752 // Equivalence can be checked by adding two restrictions.
753 if ($op == '=' || $op == '==') {
754 $value['versions'][] = ['op' => '<', 'version' => ($matches['major'] + 1) . '.x'];
758 $value['versions'][] = ['op' => $op, 'version' => $matches['major'] . '.' . $matches['minor']];
768 public function getModuleDirectories() {
770 foreach ($this->getModuleList() as $name => $module) {
771 $dirs[$name] = $this->root . '/' . $module->getPath();
779 public function getName($module) {
780 return \Drupal::service('extension.list.module')->getName($module);