Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.
   1  <?php
   2  
   3  /*
   4   * This file is part of Composer.
   5   *
   6   * (c) Nils Adermann <naderman@naderman.de>
   7   *     Jordi Boggiano <j.boggiano@seld.be>
   8   *
   9   * For the full copyright and license information, please view the LICENSE
  10   * file that was distributed with this source code.
  11   */
  12  
  13  namespace Composer;
  14  
  15  use Composer\Autoload\ClassLoader;
  16  use Composer\Semver\VersionParser;
  17  
  18  /**
  19   * This class is copied in every Composer installed project and available to all
  20   *
  21   * See also https://getcomposer.org/doc/07-runtime.md#installed-versions
  22   *
  23   * To require its presence, you can require `composer-runtime-api ^2.0`
  24   *
  25   * @final
  26   */
  27  class InstalledVersions
  28  {
  29      /**
  30       * @var mixed[]|null
  31       * @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
  32       */
  33      private static $installed;
  34  
  35      /**
  36       * @var bool|null
  37       */
  38      private static $canGetVendors;
  39  
  40      /**
  41       * @var array[]
  42       * @psalm-var array<string, array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
  43       */
  44      private static $installedByVendor = array();
  45  
  46      /**
  47       * Returns a list of all package names which are present, either by being installed, replaced or provided
  48       *
  49       * @return string[]
  50       * @psalm-return list<string>
  51       */
  52      public static function getInstalledPackages()
  53      {
  54          $packages = array();
  55          foreach (self::getInstalled() as $installed) {
  56              $packages[] = array_keys($installed['versions']);
  57          }
  58  
  59          if (1 === \count($packages)) {
  60              return $packages[0];
  61          }
  62  
  63          return array_keys(array_flip(\call_user_func_array('array_merge', $packages)));
  64      }
  65  
  66      /**
  67       * Returns a list of all package names with a specific type e.g. 'library'
  68       *
  69       * @param  string   $type
  70       * @return string[]
  71       * @psalm-return list<string>
  72       */
  73      public static function getInstalledPackagesByType($type)
  74      {
  75          $packagesByType = array();
  76  
  77          foreach (self::getInstalled() as $installed) {
  78              foreach ($installed['versions'] as $name => $package) {
  79                  if (isset($package['type']) && $package['type'] === $type) {
  80                      $packagesByType[] = $name;
  81                  }
  82              }
  83          }
  84  
  85          return $packagesByType;
  86      }
  87  
  88      /**
  89       * Checks whether the given package is installed
  90       *
  91       * This also returns true if the package name is provided or replaced by another package
  92       *
  93       * @param  string $packageName
  94       * @param  bool   $includeDevRequirements
  95       * @return bool
  96       */
  97      public static function isInstalled($packageName, $includeDevRequirements = true)
  98      {
  99          foreach (self::getInstalled() as $installed) {
 100              if (isset($installed['versions'][$packageName])) {
 101                  return $includeDevRequirements || empty($installed['versions'][$packageName]['dev_requirement']);
 102              }
 103          }
 104  
 105          return false;
 106      }
 107  
 108      /**
 109       * Checks whether the given package satisfies a version constraint
 110       *
 111       * e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call:
 112       *
 113       *   Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3')
 114       *
 115       * @param  VersionParser $parser      Install composer/semver to have access to this class and functionality
 116       * @param  string        $packageName
 117       * @param  string|null   $constraint  A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package
 118       * @return bool
 119       */
 120      public static function satisfies(VersionParser $parser, $packageName, $constraint)
 121      {
 122          $constraint = $parser->parseConstraints($constraint);
 123          $provided = $parser->parseConstraints(self::getVersionRanges($packageName));
 124  
 125          return $provided->matches($constraint);
 126      }
 127  
 128      /**
 129       * Returns a version constraint representing all the range(s) which are installed for a given package
 130       *
 131       * It is easier to use this via isInstalled() with the $constraint argument if you need to check
 132       * whether a given version of a package is installed, and not just whether it exists
 133       *
 134       * @param  string $packageName
 135       * @return string Version constraint usable with composer/semver
 136       */
 137      public static function getVersionRanges($packageName)
 138      {
 139          foreach (self::getInstalled() as $installed) {
 140              if (!isset($installed['versions'][$packageName])) {
 141                  continue;
 142              }
 143  
 144              $ranges = array();
 145              if (isset($installed['versions'][$packageName]['pretty_version'])) {
 146                  $ranges[] = $installed['versions'][$packageName]['pretty_version'];
 147              }
 148              if (array_key_exists('aliases', $installed['versions'][$packageName])) {
 149                  $ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']);
 150              }
 151              if (array_key_exists('replaced', $installed['versions'][$packageName])) {
 152                  $ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']);
 153              }
 154              if (array_key_exists('provided', $installed['versions'][$packageName])) {
 155                  $ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']);
 156              }
 157  
 158              return implode(' || ', $ranges);
 159          }
 160  
 161          throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
 162      }
 163  
 164      /**
 165       * @param  string      $packageName
 166       * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
 167       */
 168      public static function getVersion($packageName)
 169      {
 170          foreach (self::getInstalled() as $installed) {
 171              if (!isset($installed['versions'][$packageName])) {
 172                  continue;
 173              }
 174  
 175              if (!isset($installed['versions'][$packageName]['version'])) {
 176                  return null;
 177              }
 178  
 179              return $installed['versions'][$packageName]['version'];
 180          }
 181  
 182          throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
 183      }
 184  
 185      /**
 186       * @param  string      $packageName
 187       * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
 188       */
 189      public static function getPrettyVersion($packageName)
 190      {
 191          foreach (self::getInstalled() as $installed) {
 192              if (!isset($installed['versions'][$packageName])) {
 193                  continue;
 194              }
 195  
 196              if (!isset($installed['versions'][$packageName]['pretty_version'])) {
 197                  return null;
 198              }
 199  
 200              return $installed['versions'][$packageName]['pretty_version'];
 201          }
 202  
 203          throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
 204      }
 205  
 206      /**
 207       * @param  string      $packageName
 208       * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference
 209       */
 210      public static function getReference($packageName)
 211      {
 212          foreach (self::getInstalled() as $installed) {
 213              if (!isset($installed['versions'][$packageName])) {
 214                  continue;
 215              }
 216  
 217              if (!isset($installed['versions'][$packageName]['reference'])) {
 218                  return null;
 219              }
 220  
 221              return $installed['versions'][$packageName]['reference'];
 222          }
 223  
 224          throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
 225      }
 226  
 227      /**
 228       * @param  string      $packageName
 229       * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path.
 230       */
 231      public static function getInstallPath($packageName)
 232      {
 233          foreach (self::getInstalled() as $installed) {
 234              if (!isset($installed['versions'][$packageName])) {
 235                  continue;
 236              }
 237  
 238              return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null;
 239          }
 240  
 241          throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
 242      }
 243  
 244      /**
 245       * @return array
 246       * @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}
 247       */
 248      public static function getRootPackage()
 249      {
 250          $installed = self::getInstalled();
 251  
 252          return $installed[0]['root'];
 253      }
 254  
 255      /**
 256       * Returns the raw installed.php data for custom implementations
 257       *
 258       * @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect.
 259       * @return array[]
 260       * @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}
 261       */
 262      public static function getRawData()
 263      {
 264          @trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED);
 265  
 266          if (null === self::$installed) {
 267              // only require the installed.php file if this file is loaded from its dumped location,
 268              // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
 269              if (substr(__DIR__, -8, 1) !== 'C') {
 270                  self::$installed = include  __DIR__ . '/installed.php';
 271              } else {
 272                  self::$installed = array();
 273              }
 274          }
 275  
 276          return self::$installed;
 277      }
 278  
 279      /**
 280       * Returns the raw data of all installed.php which are currently loaded for custom implementations
 281       *
 282       * @return array[]
 283       * @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
 284       */
 285      public static function getAllRawData()
 286      {
 287          return self::getInstalled();
 288      }
 289  
 290      /**
 291       * Lets you reload the static array from another file
 292       *
 293       * This is only useful for complex integrations in which a project needs to use
 294       * this class but then also needs to execute another project's autoloader in process,
 295       * and wants to ensure both projects have access to their version of installed.php.
 296       *
 297       * A typical case would be PHPUnit, where it would need to make sure it reads all
 298       * the data it needs from this class, then call reload() with
 299       * `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure
 300       * the project in which it runs can then also use this class safely, without
 301       * interference between PHPUnit's dependencies and the project's dependencies.
 302       *
 303       * @param  array[] $data A vendor/composer/installed.php data set
 304       * @return void
 305       *
 306       * @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $data
 307       */
 308      public static function reload($data)
 309      {
 310          self::$installed = $data;
 311          self::$installedByVendor = array();
 312      }
 313  
 314      /**
 315       * @return array[]
 316       * @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
 317       */
 318      private static function getInstalled()
 319      {
 320          if (null === self::$canGetVendors) {
 321              self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders');
 322          }
 323  
 324          $installed = array();
 325  
 326          if (self::$canGetVendors) {
 327              foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
 328                  if (isset(self::$installedByVendor[$vendorDir])) {
 329                      $installed[] = self::$installedByVendor[$vendorDir];
 330                  } elseif (is_file($vendorDir.'/composer/installed.php')) {
 331                      $installed[] = self::$installedByVendor[$vendorDir] = require $vendorDir.'/composer/installed.php';
 332                      if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) {
 333                          self::$installed = $installed[count($installed) - 1];
 334                      }
 335                  }
 336              }
 337          }
 338  
 339          if (null === self::$installed) {
 340              // only require the installed.php file if this file is loaded from its dumped location,
 341              // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
 342              if (substr(__DIR__, -8, 1) !== 'C') {
 343                  self::$installed = require  __DIR__ . '/installed.php';
 344              } else {
 345                  self::$installed = array();
 346              }
 347          }
 348          $installed[] = self::$installed;
 349  
 350          return $installed;
 351      }
 352  }