2025-07-12 16:45:24 +02:00
< ? php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer ;
use Composer\Autoload\ClassLoader ;
use Composer\Semver\VersionParser ;
/**
* This class is copied in every Composer installed project and available to all
*
* See also https://getcomposer.org/doc/07-runtime.md#installed-versions
*
* To require its presence, you can require `composer-runtime-api ^2.0`
*
* @final
*/
class InstalledVersions
{
2025-08-06 10:23:34 +02:00
/**
* @var string|null if set (by reflection by Composer), this should be set to the path where this class is being copied to
* @internal
*/
private static $selfDir = null ;
2025-07-12 16:45:24 +02:00
/**
* @var mixed[]|null
* @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
*/
private static $installed ;
2025-08-06 10:23:34 +02:00
/**
* @var bool
*/
private static $installedIsLocalDir ;
2025-07-12 16:45:24 +02:00
/**
* @var bool|null
*/
private static $canGetVendors ;
/**
* @var array[]
* @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[]}>}>
*/
private static $installedByVendor = array ();
/**
* Returns a list of all package names which are present, either by being installed, replaced or provided
*
* @return string[]
* @psalm-return list<string>
*/
public static function getInstalledPackages ()
{
$packages = array ();
foreach ( self :: getInstalled () as $installed ) {
$packages [] = array_keys ( $installed [ 'versions' ]);
}
if ( 1 === \count ( $packages )) {
return $packages [ 0 ];
}
return array_keys ( array_flip ( \call_user_func_array ( 'array_merge' , $packages )));
}
/**
* Returns a list of all package names with a specific type e.g. 'library'
*
* @param string $type
* @return string[]
* @psalm-return list<string>
*/
public static function getInstalledPackagesByType ( $type )
{
$packagesByType = array ();
foreach ( self :: getInstalled () as $installed ) {
foreach ( $installed [ 'versions' ] as $name => $package ) {
if ( isset ( $package [ 'type' ]) && $package [ 'type' ] === $type ) {
$packagesByType [] = $name ;
}
}
}
return $packagesByType ;
}
/**
* Checks whether the given package is installed
*
* This also returns true if the package name is provided or replaced by another package
*
* @param string $packageName
* @param bool $includeDevRequirements
* @return bool
*/
public static function isInstalled ( $packageName , $includeDevRequirements = true )
{
foreach ( self :: getInstalled () as $installed ) {
if ( isset ( $installed [ 'versions' ][ $packageName ])) {
return $includeDevRequirements || ! isset ( $installed [ 'versions' ][ $packageName ][ 'dev_requirement' ]) || $installed [ 'versions' ][ $packageName ][ 'dev_requirement' ] === false ;
}
}
return false ;
}
/**
* Checks whether the given package satisfies a version constraint
*
* e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call:
*
* Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3')
*
* @param VersionParser $parser Install composer/semver to have access to this class and functionality
* @param string $packageName
* @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
* @return bool
*/
public static function satisfies ( VersionParser $parser , $packageName , $constraint )
{
$constraint = $parser -> parseConstraints (( string ) $constraint );
$provided = $parser -> parseConstraints ( self :: getVersionRanges ( $packageName ));
return $provided -> matches ( $constraint );
}
/**
* Returns a version constraint representing all the range(s) which are installed for a given package
*
* It is easier to use this via isInstalled() with the $constraint argument if you need to check
* whether a given version of a package is installed, and not just whether it exists
*
* @param string $packageName
* @return string Version constraint usable with composer/semver
*/
public static function getVersionRanges ( $packageName )
{
foreach ( self :: getInstalled () as $installed ) {
if ( ! isset ( $installed [ 'versions' ][ $packageName ])) {
continue ;
}
$ranges = array ();
if ( isset ( $installed [ 'versions' ][ $packageName ][ 'pretty_version' ])) {
$ranges [] = $installed [ 'versions' ][ $packageName ][ 'pretty_version' ];
}
if ( array_key_exists ( 'aliases' , $installed [ 'versions' ][ $packageName ])) {
$ranges = array_merge ( $ranges , $installed [ 'versions' ][ $packageName ][ 'aliases' ]);
}
if ( array_key_exists ( 'replaced' , $installed [ 'versions' ][ $packageName ])) {
$ranges = array_merge ( $ranges , $installed [ 'versions' ][ $packageName ][ 'replaced' ]);
}
if ( array_key_exists ( 'provided' , $installed [ 'versions' ][ $packageName ])) {
$ranges = array_merge ( $ranges , $installed [ 'versions' ][ $packageName ][ 'provided' ]);
}
return implode ( ' || ' , $ranges );
}
throw new \OutOfBoundsException ( 'Package "' . $packageName . '" is not installed' );
}
/**
* @param string $packageName
* @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
*/
public static function getVersion ( $packageName )
{
foreach ( self :: getInstalled () as $installed ) {
if ( ! isset ( $installed [ 'versions' ][ $packageName ])) {
continue ;
}
if ( ! isset ( $installed [ 'versions' ][ $packageName ][ 'version' ])) {
return null ;
}
return $installed [ 'versions' ][ $packageName ][ 'version' ];
}
throw new \OutOfBoundsException ( 'Package "' . $packageName . '" is not installed' );
}
/**
* @param string $packageName
* @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
*/
public static function getPrettyVersion ( $packageName )
{
foreach ( self :: getInstalled () as $installed ) {
if ( ! isset ( $installed [ 'versions' ][ $packageName ])) {
continue ;
}
if ( ! isset ( $installed [ 'versions' ][ $packageName ][ 'pretty_version' ])) {
return null ;
}
return $installed [ 'versions' ][ $packageName ][ 'pretty_version' ];
}
throw new \OutOfBoundsException ( 'Package "' . $packageName . '" is not installed' );
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference
*/
public static function getReference ( $packageName )
{
foreach ( self :: getInstalled () as $installed ) {
if ( ! isset ( $installed [ 'versions' ][ $packageName ])) {
continue ;
}
if ( ! isset ( $installed [ 'versions' ][ $packageName ][ 'reference' ])) {
return null ;
}
return $installed [ 'versions' ][ $packageName ][ 'reference' ];
}
throw new \OutOfBoundsException ( 'Package "' . $packageName . '" is not installed' );
}
/**
* @param string $packageName
* @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.
*/
public static function getInstallPath ( $packageName )
{
foreach ( self :: getInstalled () as $installed ) {
if ( ! isset ( $installed [ 'versions' ][ $packageName ])) {
continue ;
}
return isset ( $installed [ 'versions' ][ $packageName ][ 'install_path' ]) ? $installed [ 'versions' ][ $packageName ][ 'install_path' ] : null ;
}
throw new \OutOfBoundsException ( 'Package "' . $packageName . '" is not installed' );
}
/**
* @return array
* @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}
*/
public static function getRootPackage ()
{
$installed = self :: getInstalled ();
return $installed [ 0 ][ 'root' ];
}
/**
* Returns the raw installed.php data for custom implementations
*
* @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.
* @return array[]
* @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[]}>}
*/
public static function getRawData ()
{
@ 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 );
if ( null === self :: $installed ) {
// only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if ( substr ( __DIR__ , - 8 , 1 ) !== 'C' ) {
self :: $installed = include __DIR__ . '/installed.php' ;
} else {
self :: $installed = array ();
}
}
return self :: $installed ;
}
/**
* Returns the raw data of all installed.php which are currently loaded for custom implementations
*
* @return array[]
* @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[]}>}>
*/
public static function getAllRawData ()
{
return self :: getInstalled ();
}
/**
* Lets you reload the static array from another file
*
* This is only useful for complex integrations in which a project needs to use
* this class but then also needs to execute another project's autoloader in process,
* and wants to ensure both projects have access to their version of installed.php.
*
* A typical case would be PHPUnit, where it would need to make sure it reads all
* the data it needs from this class, then call reload() with
* `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure
* the project in which it runs can then also use this class safely, without
* interference between PHPUnit's dependencies and the project's dependencies.
*
* @param array[] $data A vendor/composer/installed.php data set
* @return void
*
* @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
*/
public static function reload ( $data )
{
self :: $installed = $data ;
self :: $installedByVendor = array ();
2025-08-06 10:23:34 +02:00
// when using reload, we disable the duplicate protection to ensure that self::$installed data is
// always returned, but we cannot know whether it comes from the installed.php in __DIR__ or not,
// so we have to assume it does not, and that may result in duplicate data being returned when listing
// all installed packages for example
self :: $installedIsLocalDir = false ;
}
/**
* @return string
*/
private static function getSelfDir ()
{
if ( self :: $selfDir === null ) {
self :: $selfDir = strtr ( __DIR__ , '\\' , '/' );
}
return self :: $selfDir ;
2025-07-12 16:45:24 +02:00
}
/**
* @return array[]
* @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[]}>}>
*/
private static function getInstalled ()
{
if ( null === self :: $canGetVendors ) {
self :: $canGetVendors = method_exists ( 'Composer\Autoload\ClassLoader' , 'getRegisteredLoaders' );
}
$installed = array ();
2025-08-06 10:23:34 +02:00
$copiedLocalDir = false ;
2025-07-12 16:45:24 +02:00
if ( self :: $canGetVendors ) {
2025-08-06 10:23:34 +02:00
$selfDir = self :: getSelfDir ();
2025-07-12 16:45:24 +02:00
foreach ( ClassLoader :: getRegisteredLoaders () as $vendorDir => $loader ) {
2025-08-06 10:23:34 +02:00
$vendorDir = strtr ( $vendorDir , '\\' , '/' );
2025-07-12 16:45:24 +02:00
if ( isset ( self :: $installedByVendor [ $vendorDir ])) {
$installed [] = self :: $installedByVendor [ $vendorDir ];
} elseif ( is_file ( $vendorDir . '/composer/installed.php' )) {
/** @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 */
$required = require $vendorDir . '/composer/installed.php' ;
2025-08-06 10:23:34 +02:00
self :: $installedByVendor [ $vendorDir ] = $required ;
$installed [] = $required ;
if ( self :: $installed === null && $vendorDir . '/composer' === $selfDir ) {
self :: $installed = $required ;
self :: $installedIsLocalDir = true ;
2025-07-12 16:45:24 +02:00
}
}
2025-08-06 10:23:34 +02:00
if ( self :: $installedIsLocalDir && $vendorDir . '/composer' === $selfDir ) {
$copiedLocalDir = true ;
}
2025-07-12 16:45:24 +02:00
}
}
if ( null === self :: $installed ) {
// only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if ( substr ( __DIR__ , - 8 , 1 ) !== 'C' ) {
/** @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 */
$required = require __DIR__ . '/installed.php' ;
self :: $installed = $required ;
} else {
self :: $installed = array ();
}
}
2025-08-06 10:23:34 +02:00
if ( self :: $installed !== array () && ! $copiedLocalDir ) {
2025-07-12 16:45:24 +02:00
$installed [] = self :: $installed ;
}
return $installed ;
}
}