Updated Drupal to 8.6. This goes with the following updates because it's possible...
[yaffs-website] / web / core / lib / Drupal / Core / Extension / ModuleHandler.php
1 <?php
2
3 namespace Drupal\Core\Extension;
4
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;
9
10 /**
11  * Class that manages modules in a Drupal installation.
12  */
13 class ModuleHandler implements ModuleHandlerInterface {
14
15   /**
16    * List of loaded files.
17    *
18    * @var array
19    *   An associative array whose keys are file paths of loaded files, relative
20    *   to the application's root directory.
21    */
22   protected $loadedFiles;
23
24   /**
25    * List of installed modules.
26    *
27    * @var \Drupal\Core\Extension\Extension[]
28    */
29   protected $moduleList;
30
31   /**
32    * Boolean indicating whether modules have been loaded.
33    *
34    * @var bool
35    */
36   protected $loaded = FALSE;
37
38   /**
39    * List of hook implementations keyed by hook name.
40    *
41    * @var array
42    */
43   protected $implementations;
44
45   /**
46    * List of hooks where the implementations have been "verified".
47    *
48    * @var true[]
49    *   Associative array where keys are hook names.
50    */
51   protected $verified;
52
53   /**
54    * Information returned by hook_hook_info() implementations.
55    *
56    * @var array
57    */
58   protected $hookInfo;
59
60   /**
61    * Cache backend for storing module hook implementation information.
62    *
63    * @var \Drupal\Core\Cache\CacheBackendInterface
64    */
65   protected $cacheBackend;
66
67   /**
68    * Whether the cache needs to be written.
69    *
70    * @var bool
71    */
72   protected $cacheNeedsWriting = FALSE;
73
74   /**
75    * List of alter hook implementations keyed by hook name(s).
76    *
77    * @var array
78    */
79   protected $alterFunctions;
80
81   /**
82    * The app root.
83    *
84    * @var string
85    */
86   protected $root;
87
88   /**
89    * A list of module include file keys.
90    *
91    * @var array
92    */
93   protected $includeFileKeys = [];
94
95   /**
96    * Constructs a ModuleHandler object.
97    *
98    * @param string $root
99    *   The app root.
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.
106    *
107    * @see \Drupal\Core\DrupalKernel
108    * @see \Drupal\Core\CoreServiceProvider
109    */
110   public function __construct($root, array $module_list, CacheBackendInterface $cache_backend) {
111     $this->root = $root;
112     $this->moduleList = [];
113     foreach ($module_list as $name => $module) {
114       $this->moduleList[$name] = new Extension($this->root, $module['type'], $module['pathname'], $module['filename']);
115     }
116     $this->cacheBackend = $cache_backend;
117   }
118
119   /**
120    * {@inheritdoc}
121    */
122   public function load($name) {
123     if (isset($this->loadedFiles[$name])) {
124       return TRUE;
125     }
126
127     if (isset($this->moduleList[$name])) {
128       $this->moduleList[$name]->load();
129       $this->loadedFiles[$name] = TRUE;
130       return TRUE;
131     }
132     return FALSE;
133   }
134
135   /**
136    * {@inheritdoc}
137    */
138   public function loadAll() {
139     if (!$this->loaded) {
140       foreach ($this->moduleList as $name => $module) {
141         $this->load($name);
142       }
143       $this->loaded = TRUE;
144     }
145   }
146
147   /**
148    * {@inheritdoc}
149    */
150   public function reload() {
151     $this->loaded = FALSE;
152     $this->loadAll();
153   }
154
155   /**
156    * {@inheritdoc}
157    */
158   public function isLoaded() {
159     return $this->loaded;
160   }
161
162   /**
163    * {@inheritdoc}
164    */
165   public function getModuleList() {
166     return $this->moduleList;
167   }
168
169   /**
170    * {@inheritdoc}
171    */
172   public function getModule($name) {
173     if (isset($this->moduleList[$name])) {
174       return $this->moduleList[$name];
175     }
176     throw new UnknownExtensionException(sprintf('The module %s does not exist.', $name));
177   }
178
179   /**
180    * {@inheritdoc}
181    */
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
185     // available hooks.
186     $this->resetImplementations();
187   }
188
189   /**
190    * {@inheritdoc}
191    */
192   public function addModule($name, $path) {
193     $this->add('module', $name, $path);
194   }
195
196   /**
197    * {@inheritdoc}
198    */
199   public function addProfile($name, $path) {
200     $this->add('profile', $name, $path);
201   }
202
203   /**
204    * Adds a module or profile to the list of currently active modules.
205    *
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'.
212    */
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();
218   }
219
220   /**
221    * {@inheritdoc}
222    */
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;
230         }
231       }
232     }
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'];
239     }
240     return $modules;
241   }
242
243   /**
244    * {@inheritdoc}
245    */
246   public function moduleExists($module) {
247     return isset($this->moduleList[$module]);
248   }
249
250   /**
251    * {@inheritdoc}
252    */
253   public function loadAllIncludes($type, $name = NULL) {
254     foreach ($this->moduleList as $module => $filename) {
255       $this->loadInclude($module, $type, $name);
256     }
257   }
258
259   /**
260    * {@inheritdoc}
261    */
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';
266     }
267
268     $name = $name ?: $module;
269     $key = $type . ':' . $module . ':' . $name;
270     if (isset($this->includeFileKeys[$key])) {
271       return $this->includeFileKeys[$key];
272     }
273     if (isset($this->moduleList[$module])) {
274       $file = $this->root . '/' . $this->moduleList[$module]->getPath() . "/$name.$type";
275       if (is_file($file)) {
276         require_once $file;
277         $this->includeFileKeys[$key] = $file;
278         return $file;
279       }
280       else {
281         $this->includeFileKeys[$key] = FALSE;
282       }
283     }
284     return FALSE;
285   }
286
287   /**
288    * {@inheritdoc}
289    */
290   public function getHookInfo() {
291     if (!isset($this->hookInfo)) {
292       if ($cache = $this->cacheBackend->get('hook_info')) {
293         $this->hookInfo = $cache->data;
294       }
295       else {
296         $this->buildHookInfo();
297         $this->cacheBackend->set('hook_info', $this->hookInfo);
298       }
299     }
300     return $this->hookInfo;
301   }
302
303   /**
304    * Builds hook_hook_info() information.
305    *
306    * @see \Drupal\Core\Extension\ModuleHandler::getHookInfo()
307    */
308   protected function buildHookInfo() {
309     $this->hookInfo = [];
310     // Make sure that the modules are loaded before checking.
311     $this->reload();
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);
319         }
320       }
321     }
322   }
323
324   /**
325    * {@inheritdoc}
326    */
327   public function getImplementations($hook) {
328     $implementations = $this->getImplementationInfo($hook);
329     return array_keys($implementations);
330   }
331
332   /**
333    * {@inheritdoc}
334    */
335   public function writeCache() {
336     if ($this->cacheNeedsWriting) {
337       $this->cacheBackend->set('module_implements', $this->implementations);
338       $this->cacheNeedsWriting = FALSE;
339     }
340   }
341
342   /**
343    * {@inheritdoc}
344    */
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');
362   }
363
364   /**
365    * {@inheritdoc}
366    */
367   public function implementsHook($module, $hook) {
368     $function = $module . '_' . $hook;
369     if (function_exists($function)) {
370       return TRUE;
371     }
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)) {
378         return TRUE;
379       }
380     }
381     return FALSE;
382   }
383
384   /**
385    * {@inheritdoc}
386    */
387   public function invoke($module, $hook, array $args = []) {
388     if (!$this->implementsHook($module, $hook)) {
389       return;
390     }
391     $function = $module . '_' . $hook;
392     return call_user_func_array($function, $args);
393   }
394
395   /**
396    * {@inheritdoc}
397    */
398   public function invokeAll($hook, array $args = []) {
399     $return = [];
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);
406       }
407       elseif (isset($result)) {
408         $return[] = $result;
409       }
410     }
411
412     return $return;
413   }
414
415   /**
416    * {@inheritdoc}
417    */
418   public function invokeDeprecated($description, $module, $hook, array $args = []) {
419     $result = $this->invoke($module, $hook, $args);
420     $this->triggerDeprecationError($description, $hook);
421     return $result;
422   }
423
424   /**
425    * {@inheritdoc}
426    */
427   public function invokeAllDeprecated($description, $hook, array $args = []) {
428     $result = $this->invokeAll($hook, $args);
429     $this->triggerDeprecationError($description, $hook);
430     return $result;
431   }
432
433   /**
434    * Triggers an E_USER_DEPRECATED error if any module implements the hook.
435    *
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.
440    */
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 . '()';
447       }, $modules);
448       @trigger_error($message . implode(', ', $implementations) . '. ' . $description, E_USER_DEPRECATED);
449     }
450   }
451
452   /**
453    * {@inheritdoc}
454    */
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
466       // with one item.
467       if (empty($extra_types)) {
468         unset($extra_types);
469       }
470     }
471     else {
472       $cid = $type;
473     }
474
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
477     // quickly.
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;
488         }
489       }
490       else {
491         // For multiple hooks, we need $modules to contain every module that
492         // implements at least one of them.
493         $extra_modules = [];
494         foreach ($extra_types as $extra_type) {
495           $extra_modules = array_merge($extra_modules, $this->getImplementations($extra_type . '_alter'));
496         }
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
510           // values.
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);
519         }
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;
527           }
528           foreach ($extra_types as $extra_type) {
529             $function = $module . '_' . $extra_type . '_alter';
530             if (function_exists($function)) {
531               $this->alterFunctions[$cid][] = $function;
532             }
533           }
534         }
535       }
536     }
537
538     foreach ($this->alterFunctions[$cid] as $function) {
539       $function($data, $context1, $context2);
540     }
541   }
542
543   /**
544    * {@inheritdoc}
545    */
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
552     // implementations.
553     $cid = $type;
554     if (is_array($type)) {
555       $cid = implode(',', $type);
556       $extra_types = $type;
557       $type = array_shift($extra_types);
558     }
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);
562     }
563   }
564
565   /**
566    * Provides information about modules' implementations of a hook.
567    *
568    * @param string $hook
569    *   The name of the hook (e.g. "help" or "menu").
570    *
571    * @return mixed[]
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.
576    */
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;
583       }
584     }
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;
593     }
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;
599       }
600       $this->verified[$hook] = TRUE;
601     }
602     return $this->implementations[$hook];
603   }
604
605   /**
606    * Builds hook implementation information for a given hook name.
607    *
608    * @param string $hook
609    *   The name of the hook (e.g. "help" or "menu").
610    *
611    * @return mixed[]
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.
616    *
617    * @throws \RuntimeException
618    *   Exception thrown when an invalid implementation is added by
619    *   hook_module_implements_alter().
620    *
621    * @see \Drupal\Core\Extension\ModuleHandler::getImplementationInfo()
622    */
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;
632       }
633     }
634     // Allow modules to change the weight of specific implementations, but avoid
635     // an infinite loop.
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.
646         if ($group) {
647           $this->loadInclude($module, 'inc', "$module.$group");
648         }
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()");
652         }
653       }
654     }
655     return $implementations;
656   }
657
658   /**
659    * Verifies an array of implementations loaded from the cache, by including
660    * the lazy-loaded $module.$group.inc, and checking function_exists().
661    *
662    * @param string[] $implementations
663    *   Implementation "group" by module name.
664    * @param string $hook
665    *   The hook name.
666    *
667    * @return bool
668    *   TRUE, if all implementations exist.
669    *   FALSE, if one or more implementations don't exist and need to be removed
670    *     from the cache.
671    */
672   protected function verifyImplementations(&$implementations, $hook) {
673     $all_valid = TRUE;
674     foreach ($implementations as $module => $group) {
675       // If this hook implementation is stored in a lazy-loaded file, include
676       // that file first.
677       if ($group) {
678         $this->loadInclude($module, 'inc', "$module.$group");
679       }
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
690         // the cache.
691         $all_valid = FALSE;
692       }
693     }
694     return $all_valid;
695   }
696
697   /**
698    * Parses a dependency for comparison by drupal_check_incompatibility().
699    *
700    * @param $dependency
701    *   A dependency string, which specifies a module dependency, and optionally
702    *   the project it comes from and versions that are supported. Supported
703    *   formats include:
704    *   - 'module'
705    *   - 'project:module'
706    *   - 'project:module (>=version, version)'
707    *
708    * @return
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().
717    *
718    * @see drupal_check_incompatibility()
719    */
720   public static function parseDependency($dependency) {
721     $value = [];
722     // Split out the optional project name.
723     if (strpos($dependency, ':') !== FALSE) {
724       list($project_name, $dependency) = explode(':', $dependency);
725       $value['project'] = $project_name;
726     }
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 == '<=') {
750               $matches['major']++;
751             }
752             // Equivalence can be checked by adding two restrictions.
753             if ($op == '=' || $op == '==') {
754               $value['versions'][] = ['op' => '<', 'version' => ($matches['major'] + 1) . '.x'];
755               $op = '>=';
756             }
757           }
758           $value['versions'][] = ['op' => $op, 'version' => $matches['major'] . '.' . $matches['minor']];
759         }
760       }
761     }
762     return $value;
763   }
764
765   /**
766    * {@inheritdoc}
767    */
768   public function getModuleDirectories() {
769     $dirs = [];
770     foreach ($this->getModuleList() as $name => $module) {
771       $dirs[$name] = $this->root . '/' . $module->getPath();
772     }
773     return $dirs;
774   }
775
776   /**
777    * {@inheritdoc}
778    */
779   public function getName($module) {
780     return \Drupal::service('extension.list.module')->getName($module);
781   }
782
783 }