Updated to Drupal 8.5. Core Media not yet in use.
[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
9 /**
10  * Class that manages modules in a Drupal installation.
11  */
12 class ModuleHandler implements ModuleHandlerInterface {
13
14   /**
15    * List of loaded files.
16    *
17    * @var array
18    *   An associative array whose keys are file paths of loaded files, relative
19    *   to the application's root directory.
20    */
21   protected $loadedFiles;
22
23   /**
24    * List of installed modules.
25    *
26    * @var \Drupal\Core\Extension\Extension[]
27    */
28   protected $moduleList;
29
30   /**
31    * Boolean indicating whether modules have been loaded.
32    *
33    * @var bool
34    */
35   protected $loaded = FALSE;
36
37   /**
38    * List of hook implementations keyed by hook name.
39    *
40    * @var array
41    */
42   protected $implementations;
43
44   /**
45    * List of hooks where the implementations have been "verified".
46    *
47    * @var true[]
48    *   Associative array where keys are hook names.
49    */
50   protected $verified;
51
52   /**
53    * Information returned by hook_hook_info() implementations.
54    *
55    * @var array
56    */
57   protected $hookInfo;
58
59   /**
60    * Cache backend for storing module hook implementation information.
61    *
62    * @var \Drupal\Core\Cache\CacheBackendInterface
63    */
64   protected $cacheBackend;
65
66   /**
67    * Whether the cache needs to be written.
68    *
69    * @var bool
70    */
71   protected $cacheNeedsWriting = FALSE;
72
73   /**
74    * List of alter hook implementations keyed by hook name(s).
75    *
76    * @var array
77    */
78   protected $alterFunctions;
79
80   /**
81    * The app root.
82    *
83    * @var string
84    */
85   protected $root;
86
87   /**
88    * A list of module include file keys.
89    *
90    * @var array
91    */
92   protected $includeFileKeys = [];
93
94   /**
95    * Constructs a ModuleHandler object.
96    *
97    * @param string $root
98    *   The app root.
99    * @param array $module_list
100    *   An associative array whose keys are the names of installed modules and
101    *   whose values are Extension class parameters. This is normally the
102    *   %container.modules% parameter being set up by DrupalKernel.
103    * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
104    *   Cache backend for storing module hook implementation information.
105    *
106    * @see \Drupal\Core\DrupalKernel
107    * @see \Drupal\Core\CoreServiceProvider
108    */
109   public function __construct($root, array $module_list, CacheBackendInterface $cache_backend) {
110     $this->root = $root;
111     $this->moduleList = [];
112     foreach ($module_list as $name => $module) {
113       $this->moduleList[$name] = new Extension($this->root, $module['type'], $module['pathname'], $module['filename']);
114     }
115     $this->cacheBackend = $cache_backend;
116   }
117
118   /**
119    * {@inheritdoc}
120    */
121   public function load($name) {
122     if (isset($this->loadedFiles[$name])) {
123       return TRUE;
124     }
125
126     if (isset($this->moduleList[$name])) {
127       $this->moduleList[$name]->load();
128       $this->loadedFiles[$name] = TRUE;
129       return TRUE;
130     }
131     return FALSE;
132   }
133
134   /**
135    * {@inheritdoc}
136    */
137   public function loadAll() {
138     if (!$this->loaded) {
139       foreach ($this->moduleList as $name => $module) {
140         $this->load($name);
141       }
142       $this->loaded = TRUE;
143     }
144   }
145
146   /**
147    * {@inheritdoc}
148    */
149   public function reload() {
150     $this->loaded = FALSE;
151     $this->loadAll();
152   }
153
154   /**
155    * {@inheritdoc}
156    */
157   public function isLoaded() {
158     return $this->loaded;
159   }
160
161   /**
162    * {@inheritdoc}
163    */
164   public function getModuleList() {
165     return $this->moduleList;
166   }
167
168   /**
169    * {@inheritdoc}
170    */
171   public function getModule($name) {
172     if (isset($this->moduleList[$name])) {
173       return $this->moduleList[$name];
174     }
175     throw new \InvalidArgumentException(sprintf('The module %s does not exist.', $name));
176   }
177
178   /**
179    * {@inheritdoc}
180    */
181   public function setModuleList(array $module_list = []) {
182     $this->moduleList = $module_list;
183     // Reset the implementations, so a new call triggers a reloading of the
184     // available hooks.
185     $this->resetImplementations();
186   }
187
188   /**
189    * {@inheritdoc}
190    */
191   public function addModule($name, $path) {
192     $this->add('module', $name, $path);
193   }
194
195   /**
196    * {@inheritdoc}
197    */
198   public function addProfile($name, $path) {
199     $this->add('profile', $name, $path);
200   }
201
202   /**
203    * Adds a module or profile to the list of currently active modules.
204    *
205    * @param string $type
206    *   The extension type; either 'module' or 'profile'.
207    * @param string $name
208    *   The module name; e.g., 'node'.
209    * @param string $path
210    *   The module path; e.g., 'core/modules/node'.
211    */
212   protected function add($type, $name, $path) {
213     $pathname = "$path/$name.info.yml";
214     $filename = file_exists($this->root . "/$path/$name.$type") ? "$name.$type" : NULL;
215     $this->moduleList[$name] = new Extension($this->root, $type, $pathname, $filename);
216     $this->resetImplementations();
217   }
218
219   /**
220    * {@inheritdoc}
221    */
222   public function buildModuleDependencies(array $modules) {
223     foreach ($modules as $module) {
224       $graph[$module->getName()]['edges'] = [];
225       if (isset($module->info['dependencies']) && is_array($module->info['dependencies'])) {
226         foreach ($module->info['dependencies'] as $dependency) {
227           $dependency_data = static::parseDependency($dependency);
228           $graph[$module->getName()]['edges'][$dependency_data['name']] = $dependency_data;
229         }
230       }
231     }
232     $graph_object = new Graph($graph);
233     $graph = $graph_object->searchAndSort();
234     foreach ($graph as $module_name => $data) {
235       $modules[$module_name]->required_by = isset($data['reverse_paths']) ? $data['reverse_paths'] : [];
236       $modules[$module_name]->requires = isset($data['paths']) ? $data['paths'] : [];
237       $modules[$module_name]->sort = $data['weight'];
238     }
239     return $modules;
240   }
241
242   /**
243    * {@inheritdoc}
244    */
245   public function moduleExists($module) {
246     return isset($this->moduleList[$module]);
247   }
248
249   /**
250    * {@inheritdoc}
251    */
252   public function loadAllIncludes($type, $name = NULL) {
253     foreach ($this->moduleList as $module => $filename) {
254       $this->loadInclude($module, $type, $name);
255     }
256   }
257
258   /**
259    * {@inheritdoc}
260    */
261   public function loadInclude($module, $type, $name = NULL) {
262     if ($type == 'install') {
263       // Make sure the installation API is available
264       include_once $this->root . '/core/includes/install.inc';
265     }
266
267     $name = $name ?: $module;
268     $key = $type . ':' . $module . ':' . $name;
269     if (isset($this->includeFileKeys[$key])) {
270       return $this->includeFileKeys[$key];
271     }
272     if (isset($this->moduleList[$module])) {
273       $file = $this->root . '/' . $this->moduleList[$module]->getPath() . "/$name.$type";
274       if (is_file($file)) {
275         require_once $file;
276         $this->includeFileKeys[$key] = $file;
277         return $file;
278       }
279       else {
280         $this->includeFileKeys[$key] = FALSE;
281       }
282     }
283     return FALSE;
284   }
285
286   /**
287    * {@inheritdoc}
288    */
289   public function getHookInfo() {
290     if (!isset($this->hookInfo)) {
291       if ($cache = $this->cacheBackend->get('hook_info')) {
292         $this->hookInfo = $cache->data;
293       }
294       else {
295         $this->buildHookInfo();
296         $this->cacheBackend->set('hook_info', $this->hookInfo);
297       }
298     }
299     return $this->hookInfo;
300   }
301
302   /**
303    * Builds hook_hook_info() information.
304    *
305    * @see \Drupal\Core\Extension\ModuleHandler::getHookInfo()
306    */
307   protected function buildHookInfo() {
308     $this->hookInfo = [];
309     // Make sure that the modules are loaded before checking.
310     $this->reload();
311     // $this->invokeAll() would cause an infinite recursion.
312     foreach ($this->moduleList as $module => $filename) {
313       $function = $module . '_hook_info';
314       if (function_exists($function)) {
315         $result = $function();
316         if (isset($result) && is_array($result)) {
317           $this->hookInfo = NestedArray::mergeDeep($this->hookInfo, $result);
318         }
319       }
320     }
321   }
322
323   /**
324    * {@inheritdoc}
325    */
326   public function getImplementations($hook) {
327     $implementations = $this->getImplementationInfo($hook);
328     return array_keys($implementations);
329   }
330
331   /**
332    * {@inheritdoc}
333    */
334   public function writeCache() {
335     if ($this->cacheNeedsWriting) {
336       $this->cacheBackend->set('module_implements', $this->implementations);
337       $this->cacheNeedsWriting = FALSE;
338     }
339   }
340
341   /**
342    * {@inheritdoc}
343    */
344   public function resetImplementations() {
345     $this->implementations = NULL;
346     $this->hookInfo = NULL;
347     $this->alterFunctions = NULL;
348     // We maintain a persistent cache of hook implementations in addition to the
349     // static cache to avoid looping through every module and every hook on each
350     // request. Benchmarks show that the benefit of this caching outweighs the
351     // additional database hit even when using the default database caching
352     // backend and only a small number of modules are enabled. The cost of the
353     // $this->cacheBackend->get() is more or less constant and reduced further
354     // when non-database caching backends are used, so there will be more
355     // significant gains when a large number of modules are installed or hooks
356     // invoked, since this can quickly lead to
357     // \Drupal::moduleHandler()->implementsHook() being called several thousand
358     // times per request.
359     $this->cacheBackend->set('module_implements', []);
360     $this->cacheBackend->delete('hook_info');
361   }
362
363   /**
364    * {@inheritdoc}
365    */
366   public function implementsHook($module, $hook) {
367     $function = $module . '_' . $hook;
368     if (function_exists($function)) {
369       return TRUE;
370     }
371     // If the hook implementation does not exist, check whether it lives in an
372     // optional include file registered via hook_hook_info().
373     $hook_info = $this->getHookInfo();
374     if (isset($hook_info[$hook]['group'])) {
375       $this->loadInclude($module, 'inc', $module . '.' . $hook_info[$hook]['group']);
376       if (function_exists($function)) {
377         return TRUE;
378       }
379     }
380     return FALSE;
381   }
382
383   /**
384    * {@inheritdoc}
385    */
386   public function invoke($module, $hook, array $args = []) {
387     if (!$this->implementsHook($module, $hook)) {
388       return;
389     }
390     $function = $module . '_' . $hook;
391     return call_user_func_array($function, $args);
392   }
393
394   /**
395    * {@inheritdoc}
396    */
397   public function invokeAll($hook, array $args = []) {
398     $return = [];
399     $implementations = $this->getImplementations($hook);
400     foreach ($implementations as $module) {
401       $function = $module . '_' . $hook;
402       $result = call_user_func_array($function, $args);
403       if (isset($result) && is_array($result)) {
404         $return = NestedArray::mergeDeep($return, $result);
405       }
406       elseif (isset($result)) {
407         $return[] = $result;
408       }
409     }
410
411     return $return;
412   }
413
414   /**
415    * {@inheritdoc}
416    */
417   public function invokeDeprecated($description, $module, $hook, array $args = []) {
418     $result = $this->invoke($module, $hook, $args);
419     $this->triggerDeprecationError($description, $hook);
420     return $result;
421   }
422
423   /**
424    * {@inheritdoc}
425    */
426   public function invokeAllDeprecated($description, $hook, array $args = []) {
427     $result = $this->invokeAll($hook, $args);
428     $this->triggerDeprecationError($description, $hook);
429     return $result;
430   }
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     $info = system_get_info('module', $module);
781     return isset($info['name']) ? $info['name'] : $module;
782   }
783
784 }