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