Skip to main content
Drupal API
User account menu
  • Log in

Breadcrumb

  1. Drupal Core 11.1.x

Transaction.php

Same filename in this branch
  1. 11.1.x core/lib/Drupal/Core/Database/Transaction.php

Namespace

Composer\DependencyResolver

File

vendor/composer/composer/src/Composer/DependencyResolver/Transaction.php

View source
<?php

declare (strict_types=1);

/*
 * 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\DependencyResolver;

use Composer\Package\AliasPackage;
use Composer\Package\Link;
use Composer\Package\PackageInterface;
use Composer\Repository\PlatformRepository;
use Composer\DependencyResolver\Operation\OperationInterface;

/**
 * @author Nils Adermann <naderman@naderman.de>
 * @internal
 */
class Transaction {
    
    /**
     * @var OperationInterface[]
     */
    protected $operations;
    
    /**
     * Packages present at the beginning of the transaction
     * @var PackageInterface[]
     */
    protected $presentPackages;
    
    /**
     * Package set resulting from this transaction
     * @var array<string, PackageInterface>
     */
    protected $resultPackageMap;
    
    /**
     * @var array<string, PackageInterface[]>
     */
    protected $resultPackagesByName = [];
    
    /**
     * @param PackageInterface[] $presentPackages
     * @param PackageInterface[] $resultPackages
     */
    public function __construct(array $presentPackages, array $resultPackages) {
        $this->presentPackages = $presentPackages;
        $this->setResultPackageMaps($resultPackages);
        $this->operations = $this->calculateOperations();
    }
    
    /**
     * @return OperationInterface[]
     */
    public function getOperations() : array {
        return $this->operations;
    }
    
    /**
     * @param PackageInterface[] $resultPackages
     */
    private function setResultPackageMaps(array $resultPackages) : void {
        $packageSort = static function (PackageInterface $a, PackageInterface $b) : int {
            // sort alias packages by the same name behind their non alias version
            if ($a->getName() === $b->getName()) {
                if ($a instanceof AliasPackage !== $b instanceof AliasPackage) {
                    return $a instanceof AliasPackage ? -1 : 1;
                }
                // if names are the same, compare version, e.g. to sort aliases reliably, actual order does not matter
                return strcmp($b->getVersion(), $a->getVersion());
            }
            return strcmp($b->getName(), $a->getName());
        };
        $this->resultPackageMap = [];
        foreach ($resultPackages as $package) {
            $this->resultPackageMap[spl_object_hash($package)] = $package;
            foreach ($package->getNames() as $name) {
                $this->resultPackagesByName[$name][] = $package;
            }
        }
        uasort($this->resultPackageMap, $packageSort);
        foreach ($this->resultPackagesByName as $name => $packages) {
            uasort($this->resultPackagesByName[$name], $packageSort);
        }
    }
    
    /**
     * @return OperationInterface[]
     */
    protected function calculateOperations() : array {
        $operations = [];
        $presentPackageMap = [];
        $removeMap = [];
        $presentAliasMap = [];
        $removeAliasMap = [];
        foreach ($this->presentPackages as $package) {
            if ($package instanceof AliasPackage) {
                $presentAliasMap[$package->getName() . '::' . $package->getVersion()] = $package;
                $removeAliasMap[$package->getName() . '::' . $package->getVersion()] = $package;
            }
            else {
                $presentPackageMap[$package->getName()] = $package;
                $removeMap[$package->getName()] = $package;
            }
        }
        $stack = $this->getRootPackages();
        $visited = [];
        $processed = [];
        while (\count($stack) > 0) {
            $package = array_pop($stack);
            if (isset($processed[spl_object_hash($package)])) {
                continue;
            }
            if (!isset($visited[spl_object_hash($package)])) {
                $visited[spl_object_hash($package)] = true;
                $stack[] = $package;
                if ($package instanceof AliasPackage) {
                    $stack[] = $package->getAliasOf();
                }
                else {
                    foreach ($package->getRequires() as $link) {
                        $possibleRequires = $this->getProvidersInResult($link);
                        foreach ($possibleRequires as $require) {
                            $stack[] = $require;
                        }
                    }
                }
            }
            elseif (!isset($processed[spl_object_hash($package)])) {
                $processed[spl_object_hash($package)] = true;
                if ($package instanceof AliasPackage) {
                    $aliasKey = $package->getName() . '::' . $package->getVersion();
                    if (isset($presentAliasMap[$aliasKey])) {
                        unset($removeAliasMap[$aliasKey]);
                    }
                    else {
                        $operations[] = new Operation\MarkAliasInstalledOperation($package);
                    }
                }
                else {
                    if (isset($presentPackageMap[$package->getName()])) {
                        $source = $presentPackageMap[$package->getName()];
                        // do we need to update?
                        // TODO different for lock?
                        if ($package->getVersion() !== $presentPackageMap[$package->getName()]
                            ->getVersion() || $package->getDistReference() !== $presentPackageMap[$package->getName()]
                            ->getDistReference() || $package->getSourceReference() !== $presentPackageMap[$package->getName()]
                            ->getSourceReference()) {
                            $operations[] = new Operation\UpdateOperation($source, $package);
                        }
                        unset($removeMap[$package->getName()]);
                    }
                    else {
                        $operations[] = new Operation\InstallOperation($package);
                        unset($removeMap[$package->getName()]);
                    }
                }
            }
        }
        foreach ($removeMap as $name => $package) {
            array_unshift($operations, new Operation\UninstallOperation($package));
        }
        foreach ($removeAliasMap as $nameVersion => $package) {
            $operations[] = new Operation\MarkAliasUninstalledOperation($package);
        }
        $operations = $this->movePluginsToFront($operations);
        // TODO fix this:
        // we have to do this again here even though the above stack code did it because moving plugins moves them before uninstalls
        $operations = $this->moveUninstallsToFront($operations);
        // TODO skip updates which don't update? is this needed? we shouldn't schedule this update in the first place?
        
        /*
                if ('update' === $opType) {
                    $targetPackage = $operation->getTargetPackage();
                    if ($targetPackage->isDev()) {
                        $initialPackage = $operation->getInitialPackage();
                        if ($targetPackage->getVersion() === $initialPackage->getVersion()
                            && (!$targetPackage->getSourceReference() || $targetPackage->getSourceReference() === $initialPackage->getSourceReference())
                            && (!$targetPackage->getDistReference() || $targetPackage->getDistReference() === $initialPackage->getDistReference())
                        ) {
                            $this->io->writeError('  - Skipping update of ' . $targetPackage->getPrettyName() . ' to the same reference-locked version', true, IOInterface::DEBUG);
                            $this->io->writeError('', true, IOInterface::DEBUG);
        
                            continue;
                        }
                    }
                }*/
        return $this->operations = $operations;
    }
    
    /**
     * Determine which packages in the result are not required by any other packages in it.
     *
     * These serve as a starting point to enumerate packages in a topological order despite potential cycles.
     * If there are packages with a cycle on the top level the package with the lowest name gets picked
     *
     * @return array<string, PackageInterface>
     */
    protected function getRootPackages() : array {
        $roots = $this->resultPackageMap;
        foreach ($this->resultPackageMap as $packageHash => $package) {
            if (!isset($roots[$packageHash])) {
                continue;
            }
            foreach ($package->getRequires() as $link) {
                $possibleRequires = $this->getProvidersInResult($link);
                foreach ($possibleRequires as $require) {
                    if ($require !== $package) {
                        unset($roots[spl_object_hash($require)]);
                    }
                }
            }
        }
        return $roots;
    }
    
    /**
     * @return PackageInterface[]
     */
    protected function getProvidersInResult(Link $link) : array {
        if (!isset($this->resultPackagesByName[$link->getTarget()])) {
            return [];
        }
        return $this->resultPackagesByName[$link->getTarget()];
    }
    
    /**
     * Workaround: if your packages depend on plugins, we must be sure
     * that those are installed / updated first; else it would lead to packages
     * being installed multiple times in different folders, when running Composer
     * twice.
     *
     * While this does not fix the root-causes of https://github.com/composer/composer/issues/1147,
     * it at least fixes the symptoms and makes usage of composer possible (again)
     * in such scenarios.
     *
     * @param  OperationInterface[] $operations
     * @return OperationInterface[] reordered operation list
     */
    private function movePluginsToFront(array $operations) : array {
        $dlModifyingPluginsNoDeps = [];
        $dlModifyingPluginsWithDeps = [];
        $dlModifyingPluginRequires = [];
        $pluginsNoDeps = [];
        $pluginsWithDeps = [];
        $pluginRequires = [];
        foreach (array_reverse($operations, true) as $idx => $op) {
            if ($op instanceof Operation\InstallOperation) {
                $package = $op->getPackage();
            }
            elseif ($op instanceof Operation\UpdateOperation) {
                $package = $op->getTargetPackage();
            }
            else {
                continue;
            }
            $extra = $package->getExtra();
            $isDownloadsModifyingPlugin = $package->getType() === 'composer-plugin' && isset($extra['plugin-modifies-downloads']) && $extra['plugin-modifies-downloads'] === true;
            // is this a downloads modifying plugin or a dependency of one?
            if ($isDownloadsModifyingPlugin || \count(array_intersect($package->getNames(), $dlModifyingPluginRequires)) > 0) {
                // get the package's requires, but filter out any platform requirements
                $requires = array_filter(array_keys($package->getRequires()), static function ($req) : bool {
                    return !PlatformRepository::isPlatformPackage($req);
                });
                // is this a plugin with no meaningful dependencies?
                if ($isDownloadsModifyingPlugin && 0 === \count($requires)) {
                    // plugins with no dependencies go to the very front
                    array_unshift($dlModifyingPluginsNoDeps, $op);
                }
                else {
                    // capture the requirements for this package so those packages will be moved up as well
                    $dlModifyingPluginRequires = array_merge($dlModifyingPluginRequires, $requires);
                    // move the operation to the front
                    array_unshift($dlModifyingPluginsWithDeps, $op);
                }
                unset($operations[$idx]);
                continue;
            }
            // is this package a plugin?
            $isPlugin = $package->getType() === 'composer-plugin' || $package->getType() === 'composer-installer';
            // is this a plugin or a dependency of a plugin?
            if ($isPlugin || \count(array_intersect($package->getNames(), $pluginRequires)) > 0) {
                // get the package's requires, but filter out any platform requirements
                $requires = array_filter(array_keys($package->getRequires()), static function ($req) : bool {
                    return !PlatformRepository::isPlatformPackage($req);
                });
                // is this a plugin with no meaningful dependencies?
                if ($isPlugin && 0 === \count($requires)) {
                    // plugins with no dependencies go to the very front
                    array_unshift($pluginsNoDeps, $op);
                }
                else {
                    // capture the requirements for this package so those packages will be moved up as well
                    $pluginRequires = array_merge($pluginRequires, $requires);
                    // move the operation to the front
                    array_unshift($pluginsWithDeps, $op);
                }
                unset($operations[$idx]);
            }
        }
        return array_merge($dlModifyingPluginsNoDeps, $dlModifyingPluginsWithDeps, $pluginsNoDeps, $pluginsWithDeps, $operations);
    }
    
    /**
     * Removals of packages should be executed before installations in
     * case two packages resolve to the same path (due to custom installers)
     *
     * @param  OperationInterface[] $operations
     * @return OperationInterface[] reordered operation list
     */
    private function moveUninstallsToFront(array $operations) : array {
        $uninstOps = [];
        foreach ($operations as $idx => $op) {
            if ($op instanceof Operation\UninstallOperation || $op instanceof Operation\MarkAliasUninstalledOperation) {
                $uninstOps[] = $op;
                unset($operations[$idx]);
            }
        }
        return array_merge($uninstOps, $operations);
    }

}

Classes

Title Deprecated Summary
Transaction @author Nils Adermann <naderman@naderman.de> @internal

API Navigation

  • Drupal Core 11.1.x
  • Topics
  • Classes
  • Functions
  • Constants
  • Globals
  • Files
  • Namespaces
  • Deprecated
  • Services
RSS feed
Powered by Drupal