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