class InstallationManager
Package operation manager.
@author Konstantin Kudryashov <ever.zet@gmail.com> @author Jordi Boggiano <j.boggiano@seld.be> @author Nils Adermann <naderman@naderman.de>
Hierarchy
- class \Composer\Installer\InstallationManager
Expanded class hierarchy of InstallationManager
7 files declare their use of InstallationManager
- AutoloadGenerator.php in vendor/
composer/ composer/ src/ Composer/ Autoload/ AutoloadGenerator.php - FilesystemRepository.php in vendor/
composer/ composer/ src/ Composer/ Repository/ FilesystemRepository.php - Installer.php in vendor/
composer/ composer/ src/ Composer/ Installer.php - Locker.php in vendor/
composer/ composer/ src/ Composer/ Package/ Locker.php - PartialComposer.php in vendor/
composer/ composer/ src/ Composer/ PartialComposer.php
File
-
vendor/
composer/ composer/ src/ Composer/ Installer/ InstallationManager.php, line 40
Namespace
Composer\InstallerView source
class InstallationManager {
/** @var list<InstallerInterface> */
private $installers = [];
/** @var array<string, InstallerInterface> */
private $cache = [];
/** @var array<string, array<PackageInterface>> */
private $notifiablePackages = [];
/** @var Loop */
private $loop;
/** @var IOInterface */
private $io;
/** @var ?EventDispatcher */
private $eventDispatcher;
/** @var bool */
private $outputProgress;
public function __construct(Loop $loop, IOInterface $io, ?EventDispatcher $eventDispatcher = null) {
$this->loop = $loop;
$this->io = $io;
$this->eventDispatcher = $eventDispatcher;
}
public function reset() : void {
$this->notifiablePackages = [];
FileDownloader::$downloadMetadata = [];
}
/**
* Adds installer
*
* @param InstallerInterface $installer installer instance
*/
public function addInstaller(InstallerInterface $installer) : void {
array_unshift($this->installers, $installer);
$this->cache = [];
}
/**
* Removes installer
*
* @param InstallerInterface $installer installer instance
*/
public function removeInstaller(InstallerInterface $installer) : void {
if (false !== ($key = array_search($installer, $this->installers, true))) {
array_splice($this->installers, $key, 1);
$this->cache = [];
}
}
/**
* Disables plugins.
*
* We prevent any plugins from being instantiated by
* disabling the PluginManager. This ensures that no third-party
* code is ever executed.
*/
public function disablePlugins() : void {
foreach ($this->installers as $i => $installer) {
if (!$installer instanceof PluginInstaller) {
continue;
}
$installer->disablePlugins();
}
}
/**
* Returns installer for a specific package type.
*
* @param string $type package type
*
* @throws \InvalidArgumentException if installer for provided type is not registered
*/
public function getInstaller(string $type) : InstallerInterface {
$type = strtolower($type);
if (isset($this->cache[$type])) {
return $this->cache[$type];
}
foreach ($this->installers as $installer) {
if ($installer->supports($type)) {
return $this->cache[$type] = $installer;
}
}
throw new \InvalidArgumentException('Unknown installer type: ' . $type);
}
/**
* Checks whether provided package is installed in one of the registered installers.
*
* @param InstalledRepositoryInterface $repo repository in which to check
* @param PackageInterface $package package instance
*/
public function isPackageInstalled(InstalledRepositoryInterface $repo, PackageInterface $package) : bool {
if ($package instanceof AliasPackage) {
return $repo->hasPackage($package) && $this->isPackageInstalled($repo, $package->getAliasOf());
}
return $this->getInstaller($package->getType())
->isInstalled($repo, $package);
}
/**
* Install binary for the given package.
* If the installer associated to this package doesn't handle that function, it'll do nothing.
*
* @param PackageInterface $package Package instance
*/
public function ensureBinariesPresence(PackageInterface $package) : void {
try {
$installer = $this->getInstaller($package->getType());
} catch (\InvalidArgumentException $e) {
// no installer found for the current package type (@see `getInstaller()`)
return;
}
// if the given installer support installing binaries
if ($installer instanceof BinaryPresenceInterface) {
$installer->ensureBinariesPresence($package);
}
}
/**
* Executes solver operation.
*
* @param InstalledRepositoryInterface $repo repository in which to add/remove/update packages
* @param OperationInterface[] $operations operations to execute
* @param bool $devMode whether the install is being run in dev mode
* @param bool $runScripts whether to dispatch script events
* @param bool $downloadOnly whether to only download packages
*/
public function execute(InstalledRepositoryInterface $repo, array $operations, bool $devMode = true, bool $runScripts = true, bool $downloadOnly = false) : void {
/** @var array<callable(): ?PromiseInterface<void|null>> $cleanupPromises */
$cleanupPromises = [];
$signalHandler = SignalHandler::create([
SignalHandler::SIGINT,
SignalHandler::SIGTERM,
SignalHandler::SIGHUP,
], function (string $signal, SignalHandler $handler) use (&$cleanupPromises) {
$this->io
->writeError('Received ' . $signal . ', aborting', true, IOInterface::DEBUG);
$this->runCleanup($cleanupPromises);
$handler->exitWithLastSignal();
});
try {
// execute operations in batches to make sure download-modifying-plugins are installed
// before the other packages get downloaded
$batches = [];
$batch = [];
foreach ($operations as $index => $operation) {
if ($operation instanceof UpdateOperation || $operation instanceof InstallOperation) {
$package = $operation instanceof UpdateOperation ? $operation->getTargetPackage() : $operation->getPackage();
if ($package->getType() === 'composer-plugin') {
$extra = $package->getExtra();
if (isset($extra['plugin-modifies-downloads']) && $extra['plugin-modifies-downloads'] === true) {
if (count($batch) > 0) {
$batches[] = $batch;
}
$batches[] = [
$index => $operation,
];
$batch = [];
continue;
}
}
}
$batch[$index] = $operation;
}
if (count($batch) > 0) {
$batches[] = $batch;
}
foreach ($batches as $batchToExecute) {
$this->downloadAndExecuteBatch($repo, $batchToExecute, $cleanupPromises, $devMode, $runScripts, $downloadOnly, $operations);
}
} catch (\Exception $e) {
$this->runCleanup($cleanupPromises);
throw $e;
} finally {
$signalHandler->unregister();
}
if ($downloadOnly) {
return;
}
// do a last write so that we write the repository even if nothing changed
// as that can trigger an update of some files like InstalledVersions.php if
// running a new composer version
$repo->write($devMode, $this);
}
/**
* @param OperationInterface[] $operations List of operations to execute in this batch
* @param OperationInterface[] $allOperations Complete list of operations to be executed in the install job, used for event listeners
* @phpstan-param array<callable(): ?PromiseInterface<void|null>> $cleanupPromises
*/
private function downloadAndExecuteBatch(InstalledRepositoryInterface $repo, array $operations, array &$cleanupPromises, bool $devMode, bool $runScripts, bool $downloadOnly, array $allOperations) : void {
$promises = [];
foreach ($operations as $index => $operation) {
$opType = $operation->getOperationType();
// ignoring alias ops as they don't need to execute anything at this stage
if (!in_array($opType, [
'update',
'install',
'uninstall',
], true)) {
continue;
}
if ($opType === 'update') {
/** @var UpdateOperation $operation */
$package = $operation->getTargetPackage();
$initialPackage = $operation->getInitialPackage();
}
else {
/** @var InstallOperation|MarkAliasInstalledOperation|MarkAliasUninstalledOperation|UninstallOperation $operation */
$package = $operation->getPackage();
$initialPackage = null;
}
$installer = $this->getInstaller($package->getType());
$cleanupPromises[$index] = static function () use ($opType, $installer, $package, $initialPackage) : ?PromiseInterface {
// avoid calling cleanup if the download was not even initialized for a package
// as without installation source configured nothing will work
if (null === $package->getInstallationSource()) {
return \React\Promise\resolve(null);
}
return $installer->cleanup($opType, $package, $initialPackage);
};
if ($opType !== 'uninstall') {
$promise = $installer->download($package, $initialPackage);
if (null !== $promise) {
$promises[] = $promise;
}
}
}
// execute all downloads first
if (count($promises) > 0) {
$this->waitOnPromises($promises);
}
if ($downloadOnly) {
$this->runCleanup($cleanupPromises);
return;
}
// execute operations in batches to make sure every plugin is installed in the
// right order and activated before the packages depending on it are installed
$batches = [];
$batch = [];
foreach ($operations as $index => $operation) {
if ($operation instanceof InstallOperation || $operation instanceof UpdateOperation) {
$package = $operation instanceof UpdateOperation ? $operation->getTargetPackage() : $operation->getPackage();
if ($package->getType() === 'composer-plugin' || $package->getType() === 'composer-installer') {
if (count($batch) > 0) {
$batches[] = $batch;
}
$batches[] = [
$index => $operation,
];
$batch = [];
continue;
}
}
$batch[$index] = $operation;
}
if (count($batch) > 0) {
$batches[] = $batch;
}
foreach ($batches as $batchToExecute) {
$this->executeBatch($repo, $batchToExecute, $cleanupPromises, $devMode, $runScripts, $allOperations);
}
}
/**
* @param OperationInterface[] $operations List of operations to execute in this batch
* @param OperationInterface[] $allOperations Complete list of operations to be executed in the install job, used for event listeners
* @phpstan-param array<callable(): ?PromiseInterface<void|null>> $cleanupPromises
*/
private function executeBatch(InstalledRepositoryInterface $repo, array $operations, array $cleanupPromises, bool $devMode, bool $runScripts, array $allOperations) : void {
$promises = [];
$postExecCallbacks = [];
foreach ($operations as $index => $operation) {
$opType = $operation->getOperationType();
// ignoring alias ops as they don't need to execute anything
if (!in_array($opType, [
'update',
'install',
'uninstall',
], true)) {
// output alias ops in debug verbosity as they have no output otherwise
if ($this->io
->isDebug()) {
$this->io
->writeError(' - ' . $operation->show(false));
}
$this->{$opType}($repo, $operation);
continue;
}
if ($opType === 'update') {
/** @var UpdateOperation $operation */
$package = $operation->getTargetPackage();
$initialPackage = $operation->getInitialPackage();
}
else {
/** @var InstallOperation|MarkAliasInstalledOperation|MarkAliasUninstalledOperation|UninstallOperation $operation */
$package = $operation->getPackage();
$initialPackage = null;
}
$installer = $this->getInstaller($package->getType());
$eventName = [
'install' => PackageEvents::PRE_PACKAGE_INSTALL,
'update' => PackageEvents::PRE_PACKAGE_UPDATE,
'uninstall' => PackageEvents::PRE_PACKAGE_UNINSTALL,
][$opType];
if ($runScripts && $this->eventDispatcher !== null) {
$this->eventDispatcher
->dispatchPackageEvent($eventName, $devMode, $repo, $allOperations, $operation);
}
$dispatcher = $this->eventDispatcher;
$io = $this->io;
$promise = $installer->prepare($opType, $package, $initialPackage);
if (!$promise instanceof PromiseInterface) {
$promise = \React\Promise\resolve(null);
}
$promise = $promise->then(function () use ($opType, $repo, $operation) {
return $this->{$opType}($repo, $operation);
})
->then($cleanupPromises[$index])
->then(function () use ($devMode, $repo) : void {
$repo->write($devMode, $this);
}, static function ($e) use ($opType, $package, $io) : void {
$io->writeError(' <error>' . ucfirst($opType) . ' of ' . $package->getPrettyName() . ' failed</error>');
throw $e;
});
$eventName = [
'install' => PackageEvents::POST_PACKAGE_INSTALL,
'update' => PackageEvents::POST_PACKAGE_UPDATE,
'uninstall' => PackageEvents::POST_PACKAGE_UNINSTALL,
][$opType];
if ($runScripts && $dispatcher !== null) {
$postExecCallbacks[] = static function () use ($dispatcher, $eventName, $devMode, $repo, $allOperations, $operation) : void {
$dispatcher->dispatchPackageEvent($eventName, $devMode, $repo, $allOperations, $operation);
};
}
$promises[] = $promise;
}
// execute all prepare => installs/updates/removes => cleanup steps
if (count($promises) > 0) {
$this->waitOnPromises($promises);
}
Platform::workaroundFilesystemIssues();
foreach ($postExecCallbacks as $cb) {
$cb();
}
}
/**
* @param array<PromiseInterface<void|null>> $promises
*/
private function waitOnPromises(array $promises) : void {
$progress = null;
if ($this->outputProgress && $this->io instanceof ConsoleIO && !(bool) Platform::getEnv('CI') && !$this->io
->isDebug() && count($promises) > 1) {
$progress = $this->io
->getProgressBar();
}
$this->loop
->wait($promises, $progress);
if ($progress !== null) {
$progress->clear();
// ProgressBar in non-decorated output does not output a final line-break and clear() does nothing
if (!$this->io
->isDecorated()) {
$this->io
->writeError('');
}
}
}
/**
* Executes download operation.
*
* @phpstan-return PromiseInterface<void|null>|null
*/
public function download(PackageInterface $package) : ?PromiseInterface {
$installer = $this->getInstaller($package->getType());
$promise = $installer->cleanup("install", $package);
return $promise;
}
/**
* Executes install operation.
*
* @param InstalledRepositoryInterface $repo repository in which to check
* @param InstallOperation $operation operation instance
* @phpstan-return PromiseInterface<void|null>|null
*/
public function install(InstalledRepositoryInterface $repo, InstallOperation $operation) : ?PromiseInterface {
$package = $operation->getPackage();
$installer = $this->getInstaller($package->getType());
$promise = $installer->install($repo, $package);
$this->markForNotification($package);
return $promise;
}
/**
* Executes update operation.
*
* @param InstalledRepositoryInterface $repo repository in which to check
* @param UpdateOperation $operation operation instance
* @phpstan-return PromiseInterface<void|null>|null
*/
public function update(InstalledRepositoryInterface $repo, UpdateOperation $operation) : ?PromiseInterface {
$initial = $operation->getInitialPackage();
$target = $operation->getTargetPackage();
$initialType = $initial->getType();
$targetType = $target->getType();
if ($initialType === $targetType) {
$installer = $this->getInstaller($initialType);
$promise = $installer->update($repo, $initial, $target);
$this->markForNotification($target);
}
else {
$promise = $this->getInstaller($initialType)
->uninstall($repo, $initial);
if (!$promise instanceof PromiseInterface) {
$promise = \React\Promise\resolve(null);
}
$installer = $this->getInstaller($targetType);
$promise = $promise->then(static function () use ($installer, $repo, $target) : PromiseInterface {
$promise = $installer->install($repo, $target);
if ($promise instanceof PromiseInterface) {
return $promise;
}
return \React\Promise\resolve(null);
});
}
return $promise;
}
/**
* Uninstalls package.
*
* @param InstalledRepositoryInterface $repo repository in which to check
* @param UninstallOperation $operation operation instance
* @phpstan-return PromiseInterface<void|null>|null
*/
public function uninstall(InstalledRepositoryInterface $repo, UninstallOperation $operation) : ?PromiseInterface {
$package = $operation->getPackage();
$installer = $this->getInstaller($package->getType());
return $installer->uninstall($repo, $package);
}
/**
* Executes markAliasInstalled operation.
*
* @param InstalledRepositoryInterface $repo repository in which to check
* @param MarkAliasInstalledOperation $operation operation instance
*/
public function markAliasInstalled(InstalledRepositoryInterface $repo, MarkAliasInstalledOperation $operation) : void {
$package = $operation->getPackage();
if (!$repo->hasPackage($package)) {
$repo->addPackage(clone $package);
}
}
/**
* Executes markAlias operation.
*
* @param InstalledRepositoryInterface $repo repository in which to check
* @param MarkAliasUninstalledOperation $operation operation instance
*/
public function markAliasUninstalled(InstalledRepositoryInterface $repo, MarkAliasUninstalledOperation $operation) : void {
$package = $operation->getPackage();
$repo->removePackage($package);
}
/**
* Returns the installation path of a package
*
* @return string|null absolute path to install to, which does not end with a slash, or null if the package does not have anything installed on disk
*/
public function getInstallPath(PackageInterface $package) : ?string {
$installer = $this->getInstaller($package->getType());
return $installer->getInstallPath($package);
}
public function setOutputProgress(bool $outputProgress) : void {
$this->outputProgress = $outputProgress;
}
public function notifyInstalls(IOInterface $io) : void {
$promises = [];
try {
foreach ($this->notifiablePackages as $repoUrl => $packages) {
// non-batch API, deprecated
if (str_contains($repoUrl, '%package%')) {
foreach ($packages as $package) {
$url = str_replace('%package%', $package->getPrettyName(), $repoUrl);
$params = [
'version' => $package->getPrettyVersion(),
'version_normalized' => $package->getVersion(),
];
$opts = [
'retry-auth-failure' => false,
'http' => [
'method' => 'POST',
'header' => [
'Content-type: application/x-www-form-urlencoded',
],
'content' => http_build_query($params, '', '&'),
'timeout' => 3,
],
];
$promises[] = $this->loop
->getHttpDownloader()
->add($url, $opts);
}
continue;
}
$postData = [
'downloads' => [],
];
foreach ($packages as $package) {
$packageNotification = [
'name' => $package->getPrettyName(),
'version' => $package->getVersion(),
];
if (strpos($repoUrl, 'packagist.org/') !== false) {
if (isset(FileDownloader::$downloadMetadata[$package->getName()])) {
$packageNotification['downloaded'] = FileDownloader::$downloadMetadata[$package->getName()];
}
else {
$packageNotification['downloaded'] = false;
}
}
$postData['downloads'][] = $packageNotification;
}
$opts = [
'retry-auth-failure' => false,
'http' => [
'method' => 'POST',
'header' => [
'Content-Type: application/json',
],
'content' => json_encode($postData),
'timeout' => 6,
],
];
$promises[] = $this->loop
->getHttpDownloader()
->add($repoUrl, $opts);
}
$this->loop
->wait($promises);
} catch (\Exception $e) {
}
$this->reset();
}
private function markForNotification(PackageInterface $package) : void {
if ($package->getNotificationUrl() !== null) {
$this->notifiablePackages[$package->getNotificationUrl()][$package->getName()] = $package;
}
}
/**
* @return void
* @phpstan-param array<callable(): ?PromiseInterface<void|null>> $cleanupPromises
*/
private function runCleanup(array $cleanupPromises) : void {
$promises = [];
$this->loop
->abortJobs();
foreach ($cleanupPromises as $cleanup) {
$promises[] = new \React\Promise\Promise(static function ($resolve) use ($cleanup) : void {
$promise = $cleanup();
if (!$promise instanceof PromiseInterface) {
$resolve(null);
}
else {
$promise->then(static function () use ($resolve) : void {
$resolve(null);
});
}
});
}
if (count($promises) > 0) {
$this->loop
->wait($promises);
}
}
}
Members
Title Sort descending | Modifiers | Object type | Summary |
---|---|---|---|
InstallationManager::$cache | private | property | @var array<string, InstallerInterface> |
InstallationManager::$eventDispatcher | private | property | @var ?EventDispatcher |
InstallationManager::$installers | private | property | @var list<InstallerInterface> |
InstallationManager::$io | private | property | @var IOInterface |
InstallationManager::$loop | private | property | @var Loop |
InstallationManager::$notifiablePackages | private | property | @var array<string, array<PackageInterface>> |
InstallationManager::$outputProgress | private | property | @var bool |
InstallationManager::addInstaller | public | function | Adds installer |
InstallationManager::disablePlugins | public | function | Disables plugins. |
InstallationManager::download | public | function | Executes download operation. |
InstallationManager::downloadAndExecuteBatch | private | function | @phpstan-param array<callable(): ?PromiseInterface<void|null>> $cleanupPromises |
InstallationManager::ensureBinariesPresence | public | function | Install binary for the given package. If the installer associated to this package doesn't handle that function, it'll do nothing. |
InstallationManager::execute | public | function | Executes solver operation. |
InstallationManager::executeBatch | private | function | @phpstan-param array<callable(): ?PromiseInterface<void|null>> $cleanupPromises |
InstallationManager::getInstaller | public | function | Returns installer for a specific package type. |
InstallationManager::getInstallPath | public | function | Returns the installation path of a package |
InstallationManager::install | public | function | Executes install operation. |
InstallationManager::isPackageInstalled | public | function | Checks whether provided package is installed in one of the registered installers. |
InstallationManager::markAliasInstalled | public | function | Executes markAliasInstalled operation. |
InstallationManager::markAliasUninstalled | public | function | Executes markAlias operation. |
InstallationManager::markForNotification | private | function | |
InstallationManager::notifyInstalls | public | function | |
InstallationManager::removeInstaller | public | function | Removes installer |
InstallationManager::reset | public | function | |
InstallationManager::runCleanup | private | function | @phpstan-param array<callable(): ?PromiseInterface<void|null>> $cleanupPromises |
InstallationManager::setOutputProgress | public | function | |
InstallationManager::uninstall | public | function | Uninstalls package. |
InstallationManager::update | public | function | Executes update operation. |
InstallationManager::waitOnPromises | private | function | |
InstallationManager::__construct | public | function |