From e8c694877047f331e042c17a4ac3b63b1a25962d Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Tue, 4 Dec 2018 11:20:35 +0100 Subject: [PATCH] Deduplicate link instances between versions of a given package --- src/Composer/Package/Loader/ArrayLoader.php | 155 ++++++++++++---- .../Repository/ComposerRepository.php | 170 ++++++++++-------- .../Repository/ComposerRepositoryTest.php | 17 +- 3 files changed, 230 insertions(+), 112 deletions(-) diff --git a/src/Composer/Package/Loader/ArrayLoader.php b/src/Composer/Package/Loader/ArrayLoader.php index 49ba45aa8..ac91b18fe 100644 --- a/src/Composer/Package/Loader/ArrayLoader.php +++ b/src/Composer/Package/Loader/ArrayLoader.php @@ -38,6 +38,70 @@ class ArrayLoader implements LoaderInterface } public function load(array $config, $class = 'Composer\Package\CompletePackage') + { + $package = $this->createObject($config, $class); + + foreach (Package\BasePackage::$supportedLinkTypes as $type => $opts) { + if (isset($config[$type])) { + $method = 'set'.ucfirst($opts['method']); + $package->{$method}( + $this->parseLinks( + $package->getName(), + $package->getPrettyVersion(), + $opts['description'], + $config[$type] + ) + ); + } + } + + $package = $this->configureObject($package, $config); + + return $package; + } + + public function loadPackages(array $versions, $class) + { + static $uniqKeys = array('version', 'version_normalized', 'source', 'dist', 'time'); + + $packages = array(); + $linkCache = array(); + + foreach ($versions as $version) { + if (isset($version['versions'])) { + $baseVersion = $version; + foreach ($uniqKeys as $key) { + unset($baseVersion[$key.'s']); + } + + foreach ($version['versions'] as $index => $dummy) { + $unpackedVersion = $baseVersion; + foreach ($uniqKeys as $key) { + $unpackedVersion[$key] = $version[$key.'s'][$index]; + } + + $package = $this->createObject($unpackedVersion, $class); + + $this->configureCachedLinks($linkCache, $package, $unpackedVersion); + $package = $this->configureObject($package, $unpackedVersion); + + $packages[] = $package; + } + } else { + $package = $this->createObject($version, $class); + + $this->configureCachedLinks($linkCache, $package, $version); + $package = $this->configureObject($package, $version); + + $packages[] = $package; + } + + } + + return $packages; + } + + private function createObject(array $config, $class) { if (!isset($config['name'])) { throw new \UnexpectedValueException('Unknown package has no name defined ('.json_encode($config).').'); @@ -52,7 +116,12 @@ class ArrayLoader implements LoaderInterface } else { $version = $this->versionParser->normalize($config['version']); } - $package = new $class($config['name'], $version, $config['version']); + + return new $class($config['name'], $version, $config['version']); + } + + private function configureObject($package, array $config) + { $package->setType(isset($config['type']) ? strtolower($config['type']) : 'library'); if (isset($config['target-dir'])) { @@ -109,20 +178,6 @@ class ArrayLoader implements LoaderInterface } } - foreach (Package\BasePackage::$supportedLinkTypes as $type => $opts) { - if (isset($config[$type])) { - $method = 'set'.ucfirst($opts['method']); - $package->{$method}( - $this->parseLinks( - $package->getName(), - $package->getPrettyVersion(), - $opts['description'], - $config[$type] - ) - ); - } - } - if (isset($config['suggest']) && is_array($config['suggest'])) { foreach ($config['suggest'] as $target => $reason) { if ('self.version' === trim($reason)) { @@ -202,21 +257,50 @@ class ArrayLoader implements LoaderInterface } } - if ($aliasNormalized = $this->getBranchAlias($config)) { - if ($package instanceof RootPackageInterface) { - $package = new RootAliasPackage($package, $aliasNormalized, preg_replace('{(\.9{7})+}', '.x', $aliasNormalized)); - } else { - $package = new AliasPackage($package, $aliasNormalized, preg_replace('{(\.9{7})+}', '.x', $aliasNormalized)); - } - } - if ($this->loadOptions && isset($config['transport-options'])) { $package->setTransportOptions($config['transport-options']); } + if ($aliasNormalized = $this->getBranchAlias($config)) { + if ($package instanceof RootPackageInterface) { + return new RootAliasPackage($package, $aliasNormalized, preg_replace('{(\.9{7})+}', '.x', $aliasNormalized)); + } + + return new AliasPackage($package, $aliasNormalized, preg_replace('{(\.9{7})+}', '.x', $aliasNormalized)); + } + return $package; } + private function configureCachedLinks(&$linkCache, $package, array $config) + { + $name = $package->getName(); + $prettyVersion = $package->getPrettyVersion(); + + foreach (Package\BasePackage::$supportedLinkTypes as $type => $opts) { + if (isset($config[$type])) { + $method = 'set'.ucfirst($opts['method']); + + $links = array(); + foreach ($config[$type] as $prettyTarget => $constraint) { + $target = strtolower($prettyTarget); + if ($constraint === 'self.version') { + $links[$target] = $this->createLink($name, $prettyVersion, $opts['description'], $target, $constraint); + } else { + if (!isset($linkCache[$name][$type][$target][$constraint])) { + $linkCache[$name][$type][$target][$constraint] = array($target, $this->createLink($name, $prettyVersion, $opts['description'], $target, $constraint)); + } + + list($target, $link) = $linkCache[$name][$type][$target][$constraint]; + $links[$target] = $link; + } + } + + $package->{$method}($links); + } + } + } + /** * @param string $source source package name * @param string $sourceVersion source package version (pretty version ideally) @@ -228,21 +312,26 @@ class ArrayLoader implements LoaderInterface { $res = array(); foreach ($links as $target => $constraint) { - if (!is_string($constraint)) { - throw new \UnexpectedValueException('Link constraint in '.$source.' '.$description.' > '.$target.' should be a string, got '.gettype($constraint) . ' (' . var_export($constraint, true) . ')'); - } - if ('self.version' === $constraint) { - $parsedConstraint = $this->versionParser->parseConstraints($sourceVersion); - } else { - $parsedConstraint = $this->versionParser->parseConstraints($constraint); - } - - $res[strtolower($target)] = new Link($source, $target, $parsedConstraint, $description, $constraint); + $res[strtolower($target)] = $this->createLink($source, $sourceVersion, $description, $target, $constraint); } return $res; } + private function createLink($source, $sourceVersion, $description, $target, $prettyConstraint) + { + if (!is_string($prettyConstraint)) { + throw new \UnexpectedValueException('Link constraint in '.$source.' '.$description.' > '.$target.' should be a string, got '.gettype($prettyConstraint) . ' (' . var_export($prettyConstraint, true) . ')'); + } + if ('self.version' === $prettyConstraint) { + $parsedConstraint = $this->versionParser->parseConstraints($sourceVersion); + } else { + $parsedConstraint = $this->versionParser->parseConstraints($prettyConstraint); + } + + return new Link($source, $target, $parsedConstraint, $description, $prettyConstraint); + } + /** * Retrieves a branch alias (dev-master => 1.0.x-dev for example) if it exists * diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index a491bec72..06c984bd5 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -61,7 +61,11 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito private $rootData; private $hasPartialPackages; private $partialPackagesByName; - private $versionParser; + /** + * TODO v3 should make this private once we can drop PHP 5.3 support + * @private + */ + public $versionParser; public function __construct(array $repoConfig, IOInterface $io, Config $config, HttpDownloader $httpDownloader, EventDispatcher $eventDispatcher = null) { @@ -414,6 +418,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $this->providers[$name] = array(); foreach ($packages['packages'] as $versions) { + $versionsToLoad = array(); foreach ($versions as $version) { if (!$loadingPartialPackage && $this->hasPartialPackages && isset($this->partialPackagesByName[$version['name']])) { continue; @@ -440,40 +445,44 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito continue; } - // load acceptable packages in the providers - $package = $this->createPackage($version, 'Composer\Package\CompletePackage'); - $package->setRepository($this); + $versionsToLoad[] = $version; + } + } - if ($package instanceof AliasPackage) { - $aliased = $package->getAliasOf(); - $aliased->setRepository($this); + // load acceptable packages in the providers + $loadedPackages = $this->createPackages($versionsToLoad, 'Composer\Package\CompletePackage'); + foreach ($loadedPackages as $package) { + $package->setRepository($this); - $this->providers[$name][$version['uid']] = $aliased; - $this->providers[$name][$version['uid'].'-alias'] = $package; + if ($package instanceof AliasPackage) { + $aliased = $package->getAliasOf(); + $aliased->setRepository($this); - // override provider with its alias so it can be expanded in the if block above - $this->providersByUid[$version['uid']] = $package; - } else { - $this->providers[$name][$version['uid']] = $package; - $this->providersByUid[$version['uid']] = $package; - } + $this->providers[$name][$version['uid']] = $aliased; + $this->providers[$name][$version['uid'].'-alias'] = $package; - // handle root package aliases - unset($rootAliasData); + // override provider with its alias so it can be expanded in the if block above + $this->providersByUid[$version['uid']] = $package; + } else { + $this->providers[$name][$version['uid']] = $package; + $this->providersByUid[$version['uid']] = $package; + } - if (isset($this->rootAliases[$package->getName()][$package->getVersion()])) { - $rootAliasData = $this->rootAliases[$package->getName()][$package->getVersion()]; - } elseif ($package instanceof AliasPackage && isset($this->rootAliases[$package->getName()][$package->getAliasOf()->getVersion()])) { - $rootAliasData = $this->rootAliases[$package->getName()][$package->getAliasOf()->getVersion()]; - } + // handle root package aliases + unset($rootAliasData); - if (isset($rootAliasData)) { - $alias = $this->createAliasPackage($package, $rootAliasData['alias_normalized'], $rootAliasData['alias']); - $alias->setRepository($this); + if (isset($this->rootAliases[$package->getName()][$package->getVersion()])) { + $rootAliasData = $this->rootAliases[$package->getName()][$package->getVersion()]; + } elseif ($package instanceof AliasPackage && isset($this->rootAliases[$package->getName()][$package->getAliasOf()->getVersion()])) { + $rootAliasData = $this->rootAliases[$package->getName()][$package->getAliasOf()->getVersion()]; + } - $this->providers[$name][$version['uid'].'-root'] = $alias; - $this->providersByUid[$version['uid'].'-root'] = $alias; - } + if (isset($rootAliasData)) { + $alias = $this->createAliasPackage($package, $rootAliasData['alias_normalized'], $rootAliasData['alias']); + $alias->setRepository($this); + + $this->providers[$name][$version['uid'].'-root'] = $alias; + $this->providersByUid[$version['uid'].'-root'] = $alias; } } } @@ -501,8 +510,8 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $repoData = $this->loadDataFromServer(); - foreach ($repoData as $package) { - $this->addPackage($this->createPackage($package, 'Composer\Package\CompletePackage')); + foreach ($this->createPackages($repoData, 'Composer\Package\CompletePackage') as $package) { + $this->addPackage($package); } } @@ -545,6 +554,8 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $this->asyncFetchFile($url, $cacheKey, $lastModified) ->then(function ($response) use (&$packages, $contents, $name, $constraint, $repo, $isPackageAcceptableCallable) { + static $uniqKeys = array('version', 'version_normalized', 'source', 'dist', 'time'); + if (true === $response) { $response = $contents; } @@ -553,24 +564,37 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito return; } - $uniqKeys = array('version', 'version_normalized', 'source', 'dist', 'time'); + $versionsToLoad = array(); foreach ($response['packages'][$name] as $version) { - if (isset($version['versions'])) { - $baseVersion = $version; - foreach ($uniqKeys as $key) { - unset($baseVersion[$key.'s']); - } - - foreach ($version['versions'] as $index => $dummy) { - $unpackedVersion = $baseVersion; - foreach ($uniqKeys as $key) { - $unpackedVersion[$key] = $version[$key.'s'][$index]; + if (isset($version['version_normalizeds'])) { + foreach ($version['version_normalizeds'] as $index => $normalizedVersion) { + if (!$repo->isVersionAcceptable($isPackageAcceptableCallable, $constraint, $name, $normalizedVersion)) { + foreach ($uniqKeys as $key) { + unset($version[$key.'s'][$index]); + } } - - $repo->createPackageIfAcceptable($packages, $isPackageAcceptableCallable, $unpackedVersion, $constraint); + } + if (count($version['version_normalizeds'])) { + $versionsToLoad[] = $version; } } else { - $repo->createPackageIfAcceptable($packages, $isPackageAcceptableCallable, $version, $constraint); + if (!isset($version['version_normalized'])) { + $version['version_normalized'] = $repo->versionParser->normalize($version['version']); + } + + if ($repo->isVersionAcceptable($isPackageAcceptableCallable, $constraint, $name, $version['version_normalized'])) { + $versionsToLoad[] = $version; + } + } + } + + $loadedPackages = $this->createPackages($versionsToLoad, 'Composer\Package\CompletePackage'); + foreach ($loadedPackages as $package) { + $package->setRepository($this); + + $packages[spl_object_hash($package)] = $package; + if ($package instanceof AliasPackage && !isset($packages[spl_object_hash($package->getAliasOf())])) { + $packages[spl_object_hash($package->getAliasOf())] = $package->getAliasOf(); } } }, function ($e) { @@ -592,27 +616,17 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito * * @private */ - public function createPackageIfAcceptable(&$packages, $isPackageAcceptableCallable, $version, $constraint) + public function isVersionAcceptable($isPackageAcceptableCallable, $constraint, $name, $versionNormalized) { - if (!call_user_func($isPackageAcceptableCallable, strtolower($version['name']), VersionParser::parseStability($version['version']))) { - return; + if (!call_user_func($isPackageAcceptableCallable, strtolower($name), VersionParser::parseStability($versionNormalized))) { + return false; } - if (isset($version['version_normalized']) && $constraint && !$constraint->matches(new Constraint('==', $version['version_normalized']))) { - return; + if ($constraint && !$constraint->matches(new Constraint('==', $versionNormalized))) { + return false; } - // load acceptable packages in the providers - $package = $this->createPackage($version, 'Composer\Package\CompletePackage'); - $package->setRepository($this); - - // if there was no version_normalized, then we need to check now for the constraint - if (!$constraint || isset($version['version_normalized']) || $constraint->matches(new Constraint('==', $package->getVersion()))) { - $packages[spl_object_hash($package)] = $package; - if ($package instanceof AliasPackage && !isset($packages[spl_object_hash($package->getAliasOf())])) { - $packages[spl_object_hash($package->getAliasOf())] = $package->getAliasOf(); - } - } + return true; } protected function loadRootServerFile() @@ -775,23 +789,37 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito return $packages; } - protected function createPackage(array $data, $class = 'Composer\Package\CompletePackage') + /** + * TODO v3 should make this private once we can drop PHP 5.3 support + * + * @private + */ + public function createPackages(array $packages, $class = 'Composer\Package\CompletePackage') { + if (!$packages) { + return; + } + try { - if (!isset($data['notification-url'])) { - $data['notification-url'] = $this->notifyUrl; + foreach ($packages as &$data) { + if (!isset($data['notification-url'])) { + $data['notification-url'] = $this->notifyUrl; + } } - $package = $this->loader->load($data, $class); - if (isset($this->sourceMirrors[$package->getSourceType()])) { - $package->setSourceMirrors($this->sourceMirrors[$package->getSourceType()]); - } - $package->setDistMirrors($this->distMirrors); - $this->configurePackageTransportOptions($package); + $packages = $this->loader->loadPackages($packages, $class); - return $package; + foreach ($packages as $package) { + if (isset($this->sourceMirrors[$package->getSourceType()])) { + $package->setSourceMirrors($this->sourceMirrors[$package->getSourceType()]); + } + $package->setDistMirrors($this->distMirrors); + $this->configurePackageTransportOptions($package); + } + + return $packages; } catch (\Exception $e) { - throw new \RuntimeException('Could not load package '.(isset($data['name']) ? $data['name'] : json_encode($data)).' in '.$this->url.': ['.get_class($e).'] '.$e->getMessage(), 0, $e); + throw new \RuntimeException('Could not load packages '.(isset($packages[0]['name']) ? $packages[0]['name'] : json_encode($packages)).' in '.$this->url.': ['.get_class($e).'] '.$e->getMessage(), 0, $e); } } diff --git a/tests/Composer/Test/Repository/ComposerRepositoryTest.php b/tests/Composer/Test/Repository/ComposerRepositoryTest.php index 0ffd70751..3594de101 100644 --- a/tests/Composer/Test/Repository/ComposerRepositoryTest.php +++ b/tests/Composer/Test/Repository/ComposerRepositoryTest.php @@ -32,7 +32,7 @@ class ComposerRepositoryTest extends TestCase ); $repository = $this->getMockBuilder('Composer\Repository\ComposerRepository') - ->setMethods(array('loadRootServerFile', 'createPackage')) + ->setMethods(array('loadRootServerFile', 'createPackages')) ->setConstructorArgs(array( $repoConfig, new NullIO, @@ -47,16 +47,17 @@ class ComposerRepositoryTest extends TestCase ->method('loadRootServerFile') ->will($this->returnValue($repoPackages)); + $stubs = array(); foreach ($expected as $at => $arg) { - $stubPackage = $this->getPackage('stub/stub', '1.0.0'); - - $repository - ->expects($this->at($at + 2)) - ->method('createPackage') - ->with($this->identicalTo($arg), $this->equalTo('Composer\Package\CompletePackage')) - ->will($this->returnValue($stubPackage)); + $stubs[] = $this->getPackage('stub/stub', '1.0.0'); } + $repository + ->expects($this->at(2)) + ->method('createPackages') + ->with($this->identicalTo($expected), $this->equalTo('Composer\Package\CompletePackage')) + ->will($this->returnValue($stubs)); + // Triggers initialization $packages = $repository->getPackages();