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