Browse Source

Deduplicate link instances between versions of a given package

Jordi Boggiano 6 years ago
parent
commit
e8c6948770

+ 120 - 31
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 ($this->loadOptions && isset($config['transport-options'])) {
+            $package->setTransportOptions($config['transport-options']);
+        }
+
         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));
+                return new RootAliasPackage($package, $aliasNormalized, preg_replace('{(\.9{7})+}', '.x', $aliasNormalized));
             }
-        }
 
-        if ($this->loadOptions && isset($config['transport-options'])) {
-            $package->setTransportOptions($config['transport-options']);
+            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
      *

+ 98 - 70
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()]);
+            $packages = $this->loader->loadPackages($packages, $class);
+
+            foreach ($packages as $package) {
+                if (isset($this->sourceMirrors[$package->getSourceType()])) {
+                    $package->setSourceMirrors($this->sourceMirrors[$package->getSourceType()]);
+                }
+                $package->setDistMirrors($this->distMirrors);
+                $this->configurePackageTransportOptions($package);
             }
-            $package->setDistMirrors($this->distMirrors);
-            $this->configurePackageTransportOptions($package);
 
-            return $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);
         }
     }
 

+ 9 - 8
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();