Pārlūkot izejas kodu

Merge branch 'newrepo'

Jordi Boggiano 12 gadi atpakaļ
vecāks
revīzija
573e4b2a7c

+ 8 - 0
src/Composer/Cache.php

@@ -65,4 +65,12 @@ class Cache
             return sha1_file($this->root . $file);
         }
     }
+
+    public function sha256($file)
+    {
+        $file = preg_replace('{[^a-z0-9.]}i', '-', $file);
+        if ($this->enabled && file_exists($this->root . $file)) {
+            return hash_file('sha256', $this->root . $file);
+        }
+    }
 }

+ 17 - 15
src/Composer/DependencyResolver/Decisions.php

@@ -29,12 +29,7 @@ class Decisions implements \Iterator, \Countable
     public function __construct($pool)
     {
         $this->pool = $pool;
-
-        if (version_compare(PHP_VERSION, '5.3.4', '>=')) {
-            $this->decisionMap = new \SplFixedArray($this->pool->getMaxId() + 1);
-        } else {
-            $this->decisionMap = array_fill(0, $this->pool->getMaxId() + 1, 0);
-        }
+        $this->decisionMap = array();
     }
 
     public function decide($literal, $level, $why)
@@ -51,8 +46,8 @@ class Decisions implements \Iterator, \Countable
         $packageId = abs($literal);
 
         return (
-            $literal > 0 && $this->decisionMap[$packageId] > 0 ||
-            $literal < 0 && $this->decisionMap[$packageId] < 0
+            $literal > 0 && isset($this->decisionMap[$packageId]) && $this->decisionMap[$packageId] > 0 ||
+            $literal < 0 && isset($this->decisionMap[$packageId]) && $this->decisionMap[$packageId] < 0
         );
     }
 
@@ -61,29 +56,36 @@ class Decisions implements \Iterator, \Countable
         $packageId = abs($literal);
 
         return (
-            ($this->decisionMap[$packageId] > 0 && $literal < 0) ||
-            ($this->decisionMap[$packageId] < 0 && $literal > 0)
+            (isset($this->decisionMap[$packageId]) && $this->decisionMap[$packageId] > 0 && $literal < 0) ||
+            (isset($this->decisionMap[$packageId]) && $this->decisionMap[$packageId] < 0 && $literal > 0)
         );
     }
 
     public function decided($literalOrPackageId)
     {
-        return $this->decisionMap[abs($literalOrPackageId)] != 0;
+        return !empty($this->decisionMap[abs($literalOrPackageId)]);
     }
 
     public function undecided($literalOrPackageId)
     {
-        return $this->decisionMap[abs($literalOrPackageId)] == 0;
+        return empty($this->decisionMap[abs($literalOrPackageId)]);
     }
 
     public function decidedInstall($literalOrPackageId)
     {
-        return $this->decisionMap[abs($literalOrPackageId)] > 0;
+        $packageId = abs($literalOrPackageId);
+
+        return isset($this->decisionMap[$packageId]) && $this->decisionMap[$packageId] > 0;
     }
 
     public function decisionLevel($literalOrPackageId)
     {
-        return abs($this->decisionMap[abs($literalOrPackageId)]);
+        $packageId = abs($literalOrPackageId);
+        if (isset($this->decisionMap[$packageId])) {
+            return abs($this->decisionMap[$packageId]);
+        }
+
+        return 0;
     }
 
     public function decisionRule($literalOrPackageId)
@@ -179,7 +181,7 @@ class Decisions implements \Iterator, \Countable
     {
         $packageId = abs($literal);
 
-        $previousDecision = $this->decisionMap[$packageId];
+        $previousDecision = isset($this->decisionMap[$packageId]) ? $this->decisionMap[$packageId] : null;
         if ($previousDecision != 0) {
             $literalString = $this->pool->literalToString($literal);
             $package = $this->pool->literalToPackage($literal);

+ 47 - 32
src/Composer/DependencyResolver/Pool.php

@@ -20,6 +20,7 @@ use Composer\Package\LinkConstraint\LinkConstraintInterface;
 use Composer\Package\LinkConstraint\VersionConstraint;
 use Composer\Repository\RepositoryInterface;
 use Composer\Repository\CompositeRepository;
+use Composer\Repository\ComposerRepository;
 use Composer\Repository\InstalledRepositoryInterface;
 use Composer\Repository\StreamableRepositoryInterface;
 use Composer\Repository\PlatformRepository;
@@ -39,11 +40,14 @@ class Pool
     const MATCH_REPLACE = 3;
 
     protected $repositories = array();
+    protected $composerRepos = array();
     protected $packages = array();
     protected $packageByName = array();
     protected $acceptableStabilities;
     protected $stabilityFlags;
     protected $versionParser;
+    protected $providerCache = array();
+    protected $id = 1;
 
     public function __construct($minimumStability = 'stable', array $stabilityFlags = array())
     {
@@ -72,18 +76,21 @@ class Pool
             $repos = array($repo);
         }
 
-        $id = count($this->packages) + 1;
         foreach ($repos as $repo) {
             $this->repositories[] = $repo;
 
             $exempt = $repo instanceof PlatformRepository || $repo instanceof InstalledRepositoryInterface;
-            if ($repo instanceof StreamableRepositoryInterface) {
+
+            if ($repo instanceof ComposerRepository && $repo->hasProviders()) {
+                $this->composerRepos[] = $repo;
+                $repo->resetPackageIds();
+            } elseif ($repo instanceof StreamableRepositoryInterface) {
                 foreach ($repo->getMinimalPackages() as $package) {
                     $name = $package['name'];
                     $version = $package['version'];
                     $stability = VersionParser::parseStability($version);
                     if ($exempt || $this->isPackageAcceptable($name, $stability)) {
-                        $package['id'] = $id++;
+                        $package['id'] = $this->id++;
                         $this->packages[] = $package;
 
                         // collect names
@@ -102,7 +109,7 @@ class Pool
                         }
 
                         foreach (array_keys($names) as $provided) {
-                            $this->packageByName[$provided][] =& $this->packages[$id-2];
+                            $this->packageByName[$provided][] =& $this->packages[$this->id - 2];
                         }
 
                         // handle root package aliases
@@ -119,12 +126,12 @@ class Pool
                             $alias['version'] = $rootAliasData['alias_normalized'];
                             $alias['alias'] = $rootAliasData['alias'];
                             $alias['alias_of'] = $package['id'];
-                            $alias['id'] = $id++;
+                            $alias['id'] = $this->id++;
                             $alias['root_alias'] = true;
                             $this->packages[] = $alias;
 
                             foreach (array_keys($names) as $name) {
-                                $this->packageByName[$name][] =& $this->packages[$id-2];
+                                $this->packageByName[$name][] =& $this->packages[$this->id - 2];
                             }
                         }
 
@@ -135,11 +142,11 @@ class Pool
                             $alias['version'] = $package['alias_normalized'];
                             $alias['alias'] = $package['alias'];
                             $alias['alias_of'] = $package['id'];
-                            $alias['id'] = $id++;
+                            $alias['id'] = $this->id++;
                             $this->packages[] = $alias;
 
                             foreach (array_keys($names) as $name) {
-                                $this->packageByName[$name][] =& $this->packages[$id-2];
+                                $this->packageByName[$name][] =& $this->packages[$this->id - 2];
                             }
                         }
                     }
@@ -149,7 +156,7 @@ class Pool
                     $name = $package->getName();
                     $stability = $package->getStability();
                     if ($exempt || $this->isPackageAcceptable($name, $stability)) {
-                        $package->setId($id++);
+                        $package->setId($this->id++);
                         $this->packages[] = $package;
 
                         foreach ($package->getNames() as $name) {
@@ -163,7 +170,7 @@ class Pool
                             $package->setPrettyAlias($alias['alias']);
                             $package->getRepository()->addPackage($aliasPackage = new AliasPackage($package, $alias['alias_normalized'], $alias['alias']));
                             $aliasPackage->setRootPackageAlias(true);
-                            $aliasPackage->setId($id++);
+                            $aliasPackage->setId($this->id++);
 
                             $this->packages[] = $aliasPackage;
 
@@ -201,16 +208,6 @@ class Pool
         return $this->packages[$id - 1];
     }
 
-    /**
-    * Retrieves the highest id assigned to a package in this pool
-    *
-    * @return int Highest package id
-    */
-    public function getMaxId()
-    {
-        return count($this->packages);
-    }
-
     /**
      * Searches all packages providing the given package name and match the constraint
      *
@@ -221,11 +218,33 @@ class Pool
      */
     public function whatProvides($name, LinkConstraintInterface $constraint = null)
     {
-        if (!isset($this->packageByName[$name])) {
-            return array();
+        if (isset($this->providerCache[$name][(string) $constraint])) {
+            return $this->providerCache[$name][(string) $constraint];
         }
 
-        $candidates = $this->packageByName[$name];
+        return $this->providerCache[$name][(string) $constraint] = $this->computeWhatProvides($name, $constraint);
+    }
+
+    /**
+     * @see whatProvides
+     */
+    private function computeWhatProvides($name, $constraint)
+    {
+        $candidates = array();
+
+        foreach ($this->composerRepos as $repo) {
+            foreach ($repo->whatProvides($this, $name) as $candidate) {
+                $candidates[] = $candidate;
+                if ($candidate->getId() < 1) {
+                    $candidate->setId($this->id++);
+                    $this->packages[$this->id - 2] = $candidate;
+                }
+            }
+        }
+
+        if (isset($this->packageByName[$name])) {
+            $candidates = array_merge($candidates, $this->packageByName[$name]);
+        }
 
         if (null === $constraint) {
             foreach ($candidates as $key => $candidate) {
@@ -298,7 +317,7 @@ class Pool
         return $prefix.' '.$package->getPrettyString();
     }
 
-    private function isPackageAcceptable($name, $stability)
+    public function isPackageAcceptable($name, $stability)
     {
         // allow if package matches the global stability requirement and has no exception
         if (!isset($this->stabilityFlags[$name]) && isset($this->acceptableStabilities[$stability])) {
@@ -368,16 +387,12 @@ class Pool
             $replaces = $candidate->getReplaces();
         }
 
-        foreach ($provides as $link) {
-            if ($link->getTarget() === $name && $constraint->matches($link->getConstraint())) {
-                return self::MATCH_PROVIDE;
-            }
+        if (isset($provides[$name]) && $constraint->matches($provides[$name]->getConstraint())) {
+            return self::MATCH_PROVIDE;
         }
 
-        foreach ($replaces as $link) {
-            if ($link->getTarget() === $name && $constraint->matches($link->getConstraint())) {
-                return self::MATCH_REPLACE;
-            }
+        if (isset($replaces[$name]) && $constraint->matches($replaces[$name]->getConstraint())) {
+            return self::MATCH_REPLACE;
         }
 
         return self::MATCH_NONE;

+ 2 - 1
src/Composer/DependencyResolver/Solver.php

@@ -53,7 +53,8 @@ class Solver
     {
         $decisionStart = count($this->decisions) - 1;
 
-        for ($ruleIndex = 0; $ruleIndex < count($this->rules); $ruleIndex++) {
+        $rulesCount = count($this->rules);
+        for ($ruleIndex = 0; $ruleIndex < $rulesCount; $ruleIndex++) {
             $rule = $this->rules->ruleById($ruleIndex);
 
             if (!$rule->isAssertion() || $rule->isDisabled()) {

+ 1 - 1
src/Composer/Json/JsonFile.php

@@ -85,7 +85,7 @@ class JsonFile
                 $json = file_get_contents($this->path);
             }
         } catch (TransportException $e) {
-            throw new \RuntimeException($e->getMessage());
+            throw new \RuntimeException($e->getMessage(), 0, $e);
         } catch (\Exception $e) {
             throw new \RuntimeException('Could not read '.$this->path."\n\n".$e->getMessage());
         }

+ 1 - 1
src/Composer/Package/Version/VersionParser.php

@@ -189,7 +189,7 @@ class VersionParser
             } else {
                 $parsedConstraint = $this->parseConstraints($constraint);
             }
-            $res[] = new Link($source, $target, $parsedConstraint, $description, $constraint);
+            $res[strtolower($target)] = new Link($source, $target, $parsedConstraint, $description, $constraint);
         }
 
         return $res;

+ 139 - 4
src/Composer/Repository/ComposerRepository.php

@@ -14,12 +14,15 @@ namespace Composer\Repository;
 
 use Composer\Package\Loader\ArrayLoader;
 use Composer\Package\PackageInterface;
+use Composer\Package\AliasPackage;
 use Composer\Package\Version\VersionParser;
+use Composer\DependencyResolver\Pool;
 use Composer\Json\JsonFile;
 use Composer\Cache;
 use Composer\Config;
 use Composer\IO\IOInterface;
 use Composer\Util\RemoteFilesystem;
+use Composer\Downloader\TransportException;
 
 /**
  * @author Jordi Boggiano <j.boggiano@seld.be>
@@ -29,13 +32,19 @@ class ComposerRepository extends ArrayRepository implements NotifiableRepository
     protected $config;
     protected $options;
     protected $url;
+    protected $baseUrl;
     protected $io;
     protected $cache;
     protected $notifyUrl;
+    protected $hasProviders = false;
+    protected $providerListing;
+    protected $providers = array();
+    protected $providersByUid = array();
     protected $loader;
     private $rawData;
     private $minimalPackages;
     private $degradedMode = false;
+    private $rootData;
 
     public function __construct(array $repoConfig, IOInterface $io, Config $config)
     {
@@ -60,6 +69,7 @@ class ComposerRepository extends ArrayRepository implements NotifiableRepository
         $this->config = $config;
         $this->options = $repoConfig['options'];
         $this->url = $repoConfig['url'];
+        $this->baseUrl = rtrim(preg_replace('{^(.*)(?:/packages.json)?(?:[?#].*)?$}', '$1', $this->url), '/');
         $this->io = $io;
         $this->cache = new Cache($io, $config->get('home').'/cache/'.preg_replace('{[^a-z0-9.]}i', '-', $this->url));
         $this->loader = new ArrayLoader();
@@ -182,6 +192,90 @@ class ComposerRepository extends ArrayRepository implements NotifiableRepository
         return $aliasPackage;
     }
 
+    public function hasProviders()
+    {
+        $this->loadRootServerFile();
+
+        return $this->hasProviders;
+    }
+
+    public function resetPackageIds()
+    {
+        foreach ($this->providersByUid as $package) {
+            $package->setId(-1);
+        }
+    }
+
+    public function whatProvides(Pool $pool, $name)
+    {
+        // skip platform packages
+        if ($name === 'php' || in_array(substr($name, 0, 4), array('ext-', 'lib-'), true) || $name === '__root__') {
+            return array();
+        }
+
+        if (isset($this->providers[$name])) {
+            return $this->providers[$name];
+        }
+
+        if (null === $this->providerListing) {
+            $this->loadProviderListings($this->loadRootServerFile());
+        }
+
+        $url = 'p/'.$name.'.json';
+
+        // package does not exist in this repo
+        if (!isset($this->providerListing[$url])) {
+            return array();
+        }
+
+        if ($this->cache->sha256($url) === $this->providerListing[$url]['sha256']) {
+            $packages = json_decode($this->cache->read($url), true);
+        } else {
+            $packages = $this->fetchFile($url, null, $this->providerListing[$url]['sha256']);
+        }
+
+        $this->providers[$name] = array();
+        foreach ($packages['packages'] as $versions) {
+            foreach ($versions as $version) {
+                // avoid loading the same objects twice
+                if (isset($this->providersByUid[$version['uid']])) {
+                    // skip if already assigned
+                    if (!isset($this->providers[$name][$version['uid']])) {
+                        // expand alias in two packages
+                        if ($this->providersByUid[$version['uid']] instanceof AliasPackage) {
+                            $this->providers[$name][$version['uid']] = $this->providersByUid[$version['uid']]->getAliasOf();
+                            $this->providers[$name][$version['uid'].'-alias'] = $this->providersByUid[$version['uid']];
+                        } else {
+                            $this->providers[$name][$version['uid']] = $this->providersByUid[$version['uid']];
+                        }
+                    }
+                } else {
+                    if (!$pool->isPackageAcceptable($version['name'], VersionParser::parseStability($version['version']))) {
+                        continue;
+                    }
+
+                    // load acceptable packages in the providers
+                    $package = $this->createPackage($version, 'Composer\Package\Package');
+                    $package->setRepository($this);
+
+                    $this->providers[$name][$version['uid']] = $package;
+                    $this->providersByUid[$version['uid']] = $package;
+
+                    if ($package->getAlias()) {
+                        $alias = $this->createAliasPackage($package);
+                        $alias->setRepository($this);
+
+                        $this->providers[$name][$version['uid'].'-alias'] = $alias;
+                        // override provider with its alias so it can be expanded in the if block above
+                        $this->providersByUid[$version['uid']] = $alias;
+                    }
+                }
+            }
+        }
+
+        return $this->providers[$name];
+    }
+
     /**
      * {@inheritDoc}
      */
@@ -196,8 +290,12 @@ class ComposerRepository extends ArrayRepository implements NotifiableRepository
         }
     }
 
-    protected function loadDataFromServer()
+    protected function loadRootServerFile()
     {
+        if (null !== $this->rootData) {
+            return $this->rootData;
+        }
+
         if (!extension_loaded('openssl') && 'https' === substr($this->url, 0, 5)) {
             throw new \RuntimeException('You must enable the openssl extension in your php.ini to load information from '.$this->url);
         }
@@ -220,9 +318,42 @@ class ComposerRepository extends ArrayRepository implements NotifiableRepository
             }
         }
 
+        if (!empty($data['providers']) || !empty($data['providers-includes'])) {
+            $this->hasProviders = true;
+        }
+
+        return $this->rootData = $data;
+    }
+
+    protected function loadDataFromServer()
+    {
+        $data = $this->loadRootServerFile();
+
         return $this->loadIncludes($data);
     }
 
+    protected function loadProviderListings($data)
+    {
+        if (isset($data['providers'])) {
+            if (!is_array($this->providerListing)) {
+                $this->providerListing = array();
+            }
+            $this->providerListing = array_merge($this->providerListing, $data['providers']);
+        }
+
+        if (isset($data['providers-includes'])) {
+            foreach ($data['providers-includes'] as $include => $metadata) {
+                if ($this->cache->sha256($include) === $metadata['sha256']) {
+                    $includedData = json_decode($this->cache->read($include), true);
+                } else {
+                    $includedData = $this->fetchFile($include, null, $metadata['sha256']);
+                }
+
+                $this->loadProviderListings($includedData);
+            }
+        }
+    }
+
     protected function loadIncludes($data)
     {
         $packages = array();
@@ -269,11 +400,11 @@ class ComposerRepository extends ArrayRepository implements NotifiableRepository
         }
     }
 
-    protected function fetchFile($filename, $cacheKey = null)
+    protected function fetchFile($filename, $cacheKey = null, $sha256 = null)
     {
         if (!$cacheKey) {
             $cacheKey = $filename;
-            $filename = $this->url.'/'.$filename;
+            $filename = $this->baseUrl.'/'.$filename;
         }
 
         $retries = 3;
@@ -281,7 +412,11 @@ class ComposerRepository extends ArrayRepository implements NotifiableRepository
             try {
                 $json = new JsonFile($filename, new RemoteFilesystem($this->io, $this->options));
                 $data = $json->read();
-                $this->cache->write($cacheKey, json_encode($data));
+                $encoded = json_encode($data);
+                if ($sha256 && $sha256 !== hash('sha256', $encoded)) {
+                    throw new \UnexpectedValueException('The contents of '.$filename.' do not match its signature, this may be due to a temporary glitch or a man-in-the-middle attack, aborting for safety. Please try running Composer again.');
+                }
+                $this->cache->write($cacheKey, $encoded);
 
                 break;
             } catch (\Exception $e) {

+ 0 - 16
tests/Composer/Test/DependencyResolver/PoolTest.php

@@ -129,20 +129,4 @@ class PoolTest extends TestCase
 
         $this->assertEquals(array(), $pool->whatProvides('foo'));
     }
-
-    public function testGetMaxId()
-    {
-        $pool = new Pool;
-        $repository = new ArrayRepository;
-        $firstPackage = $this->getPackage('foo', '1');
-        $secondPackage = $this->getPackage('foo1', '1');
-
-        $this->assertEquals(0, $pool->getMaxId());
-
-        $repository->addPackage($firstPackage);
-        $repository->addPackage($secondPackage);
-        $pool->addRepository($repository);
-
-        $this->assertEquals(2, $pool->getMaxId());
-    }
 }

+ 66 - 59
tests/Composer/Test/DependencyResolver/SolverTest.php

@@ -19,6 +19,7 @@ use Composer\DependencyResolver\Solver;
 use Composer\DependencyResolver\SolverProblemsException;
 use Composer\Package\Link;
 use Composer\Test\TestCase;
+use Composer\Package\LinkConstraint\MultiConstraint;
 
 class SolverTest extends TestCase
 {
@@ -103,7 +104,7 @@ class SolverTest extends TestCase
         $this->repo->addPackage($packageB = $this->getPackage('B', '1.0'));
         $this->repo->addPackage($newPackageB = $this->getPackage('B', '1.1'));
 
-        $packageA->setRequires(array(new Link('A', 'B', $this->getVersionConstraint('<', '1.1'), 'requires')));
+        $packageA->setRequires(array('b' => new Link('A', 'B', $this->getVersionConstraint('<', '1.1'), 'requires')));
 
         $this->reposComplete();
 
@@ -124,9 +125,11 @@ class SolverTest extends TestCase
         $this->repo->addPackage($newPackageB13 = $this->getPackage('B', '1.3'));
 
         $packageA->setRequires(array(
-            new Link('A', 'B', $this->getVersionConstraint('<=', '1.3'), 'requires'),
-            new Link('A', 'B', $this->getVersionConstraint('<>', '1.3'), 'requires'),
-            new Link('A', 'B', $this->getVersionConstraint('!=', '1.2'), 'requires'),
+            'b' => new Link('A', 'B', new MultiConstraint(array(
+                $this->getVersionConstraint('<=', '1.3'),
+                $this->getVersionConstraint('<>', '1.3'),
+                $this->getVersionConstraint('!=', '1.2'),
+            )), 'requires'),
         ));
 
         $this->reposComplete();
@@ -146,11 +149,11 @@ class SolverTest extends TestCase
         $this->repo->addPackage($packageC = $this->getPackage('C', '1.0'));
 
         $packageB->setRequires(array(
-            new Link('B', 'A', $this->getVersionConstraint('>=', '1.0'), 'requires'),
-            new Link('B', 'C', $this->getVersionConstraint('>=', '1.0'), 'requires'),
+            'a' => new Link('B', 'A', $this->getVersionConstraint('>=', '1.0'), 'requires'),
+            'c' => new Link('B', 'C', $this->getVersionConstraint('>=', '1.0'), 'requires'),
         ));
         $packageC->setRequires(array(
-            new Link('C', 'A', $this->getVersionConstraint('>=', '1.0'), 'requires'),
+            'a' => new Link('C', 'A', $this->getVersionConstraint('>=', '1.0'), 'requires'),
         ));
 
         $this->reposComplete();
@@ -216,7 +219,7 @@ class SolverTest extends TestCase
         $this->repo->addPackage($newPackageB = $this->getPackage('B', '1.1'));
         $this->reposComplete();
 
-        $packageA->setRequires(array(new Link('A', 'B', $this->getVersionConstraint('>=', '1.0.0.0'), 'requires')));
+        $packageA->setRequires(array('b' => new Link('A', 'B', $this->getVersionConstraint('>=', '1.0.0.0'), 'requires')));
 
         $this->request->install('A', $this->getVersionConstraint('=', '1.0.0.0'));
         $this->request->install('B', $this->getVersionConstraint('=', '1.1.0.0'));
@@ -249,8 +252,8 @@ class SolverTest extends TestCase
         $this->repo->addPackage($newPackageA = $this->getPackage('A', '1.1'));
         $this->repo->addPackage($newPackageB = $this->getPackage('B', '1.1'));
 
-        $packageA->setRequires(array(new Link('A', 'B', null, 'requires')));
-        $newPackageA->setRequires(array(new Link('A', 'B', null, 'requires')));
+        $packageA->setRequires(array('b' => new Link('A', 'B', null, 'requires')));
+        $newPackageA->setRequires(array('b' => new Link('A', 'B', null, 'requires')));
 
         $this->reposComplete();
 
@@ -361,7 +364,7 @@ class SolverTest extends TestCase
         $this->repo->addPackage($newPackageB = $this->getPackage('B', '1.1'));
         $this->repo->addPackage($packageC = $this->getPackage('C', '1.1'));
         $this->repo->addPackage($this->getPackage('D', '1.0'));
-        $packageA->setRequires(array(new Link('A', 'B', $this->getVersionConstraint('<', '1.1'), 'requires')));
+        $packageA->setRequires(array('b' => new Link('A', 'B', $this->getVersionConstraint('<', '1.1'), 'requires')));
 
         $this->reposComplete();
 
@@ -384,8 +387,8 @@ class SolverTest extends TestCase
         $this->repo->addPackage($middlePackageB = $this->getPackage('B', '1.0'));
         $this->repo->addPackage($newPackageB = $this->getPackage('B', '1.1'));
         $this->repo->addPackage($oldPackageB = $this->getPackage('B', '0.9'));
-        $packageA->setRequires(array(new Link('A', 'B', $this->getVersionConstraint('<', '1.1'), 'requires')));
-        $packageA->setConflicts(array(new Link('A', 'B', $this->getVersionConstraint('<', '1.0'), 'conflicts')));
+        $packageA->setRequires(array('b' => new Link('A', 'B', $this->getVersionConstraint('<', '1.1'), 'requires')));
+        $packageA->setConflicts(array('b' => new Link('A', 'B', $this->getVersionConstraint('<', '1.0'), 'conflicts')));
 
         $this->reposComplete();
 
@@ -401,7 +404,7 @@ class SolverTest extends TestCase
     {
         $this->repoInstalled->addPackage($packageA = $this->getPackage('A', '1.0'));
         $this->repo->addPackage($packageB = $this->getPackage('B', '1.0'));
-        $packageB->setReplaces(array(new Link('B', 'A', null)));
+        $packageB->setReplaces(array('a' => new Link('B', 'A', null)));
 
         $this->reposComplete();
 
@@ -430,8 +433,8 @@ class SolverTest extends TestCase
     {
         $this->repo->addPackage($packageA = $this->getPackage('A', '1.0'));
         $this->repo->addPackage($packageQ = $this->getPackage('Q', '1.0'));
-        $packageA->setRequires(array(new Link('A', 'B', $this->getVersionConstraint('>=', '1.0'), 'requires')));
-        $packageQ->setProvides(array(new Link('Q', 'B', $this->getVersionConstraint('=', '1.0'), 'provides')));
+        $packageA->setRequires(array('b' => new Link('A', 'B', $this->getVersionConstraint('>=', '1.0'), 'requires')));
+        $packageQ->setProvides(array('b' => new Link('Q', 'B', $this->getVersionConstraint('=', '1.0'), 'provides')));
 
         $this->reposComplete();
 
@@ -448,8 +451,8 @@ class SolverTest extends TestCase
         $this->repo->addPackage($packageA = $this->getPackage('A', '1.0'));
         $this->repo->addPackage($packageQ = $this->getPackage('Q', '1.0'));
         $this->repo->addPackage($packageB = $this->getPackage('B', '1.0'));
-        $packageA->setRequires(array(new Link('A', 'B', $this->getVersionConstraint('>=', '1.0'), 'requires')));
-        $packageQ->setReplaces(array(new Link('Q', 'B', $this->getVersionConstraint('>=', '1.0'), 'replaces')));
+        $packageA->setRequires(array('b' => new Link('A', 'B', $this->getVersionConstraint('>=', '1.0'), 'requires')));
+        $packageQ->setReplaces(array('b' => new Link('Q', 'B', $this->getVersionConstraint('>=', '1.0'), 'replaces')));
 
         $this->reposComplete();
 
@@ -465,8 +468,8 @@ class SolverTest extends TestCase
     {
         $this->repo->addPackage($packageA = $this->getPackage('A', '1.0'));
         $this->repo->addPackage($packageQ = $this->getPackage('Q', '1.0'));
-        $packageA->setRequires(array(new Link('A', 'B', $this->getVersionConstraint('>=', '1.0'), 'requires')));
-        $packageQ->setReplaces(array(new Link('Q', 'B', $this->getVersionConstraint('>=', '1.0'), 'replaces')));
+        $packageA->setRequires(array('b' => new Link('A', 'B', $this->getVersionConstraint('>=', '1.0'), 'requires')));
+        $packageQ->setReplaces(array('b' => new Link('Q', 'B', $this->getVersionConstraint('>=', '1.0'), 'replaces')));
 
         $this->reposComplete();
 
@@ -483,8 +486,8 @@ class SolverTest extends TestCase
         $this->repo->addPackage($packageA = $this->getPackage('A', '1.0'));
         $this->repo->addPackage($packageQ = $this->getPackage('Q', '1.0'));
         $this->repo->addPackage($packageB = $this->getPackage('B', '1.0'));
-        $packageA->setRequires(array(new Link('A', 'B', $this->getVersionConstraint('>=', '1.0'), 'requires')));
-        $packageQ->setReplaces(array(new Link('Q', 'B', $this->getVersionConstraint('>=', '1.0'), 'replaces')));
+        $packageA->setRequires(array('b' => new Link('A', 'B', $this->getVersionConstraint('>=', '1.0'), 'requires')));
+        $packageQ->setReplaces(array('b' => new Link('Q', 'B', $this->getVersionConstraint('>=', '1.0'), 'replaces')));
 
         $this->reposComplete();
 
@@ -501,24 +504,28 @@ class SolverTest extends TestCase
     {
         $this->repo->addPackage($packageX = $this->getPackage('X', '1.0'));
         $packageX->setRequires(array(
-            new Link('X', 'A', $this->getVersionConstraint('>=', '2.0.0.0'), 'requires'),
-            new Link('X', 'B', $this->getVersionConstraint('>=', '2.0.0.0'), 'requires')));
+            'a' => new Link('X', 'A', $this->getVersionConstraint('>=', '2.0.0.0'), 'requires'),
+            'b' => new Link('X', 'B', $this->getVersionConstraint('>=', '2.0.0.0'), 'requires')
+        ));
 
         $this->repo->addPackage($packageA = $this->getPackage('A', '2.0.0'));
         $this->repo->addPackage($newPackageA = $this->getPackage('A', '2.1.0'));
         $this->repo->addPackage($newPackageB = $this->getPackage('B', '2.1.0'));
 
-        $packageA->setRequires(array(new Link('A', 'B', $this->getVersionConstraint('>=', '2.0.0.0'), 'requires')));
+        $packageA->setRequires(array('b' => new Link('A', 'B', $this->getVersionConstraint('>=', '2.0.0.0'), 'requires')));
 
         // new package A depends on version of package B that does not exist
         // => new package A is not installable
-        $newPackageA->setRequires(array(new Link('A', 'B', $this->getVersionConstraint('>=', '2.2.0.0'), 'requires')));
+        $newPackageA->setRequires(array('b' => new Link('A', 'B', $this->getVersionConstraint('>=', '2.2.0.0'), 'requires')));
 
         // add a package S replacing both A and B, so that S and B or S and A cannot be simultaneously installed
         // but an alternative option for A and B both exists
         // this creates a more difficult so solve conflict
         $this->repo->addPackage($packageS = $this->getPackage('S', '2.0.0'));
-        $packageS->setReplaces(array(new Link('S', 'A', $this->getVersionConstraint('>=', '2.0.0.0'), 'replaces'), new Link('S', 'B', $this->getVersionConstraint('>=', '2.0.0.0'), 'replaces')));
+        $packageS->setReplaces(array(
+            'a' => new Link('S', 'A', $this->getVersionConstraint('>=', '2.0.0.0'), 'replaces'),
+            'b' => new Link('S', 'B', $this->getVersionConstraint('>=', '2.0.0.0'), 'replaces')
+        ));
 
         $this->reposComplete();
 
@@ -536,8 +543,8 @@ class SolverTest extends TestCase
         $this->repo->addPackage($packageA = $this->getPackage('A', '1.0'));
         $this->repo->addPackage($packageB1 = $this->getPackage('B', '0.9'));
         $this->repo->addPackage($packageB2 = $this->getPackage('B', '1.1'));
-        $packageA->setRequires(array(new Link('A', 'B', $this->getVersionConstraint('>=', '1.0'), 'requires')));
-        $packageB2->setRequires(array(new Link('B', 'A', $this->getVersionConstraint('>=', '1.0'), 'requires')));
+        $packageA->setRequires(array('b' => new Link('A', 'B', $this->getVersionConstraint('>=', '1.0'), 'requires')));
+        $packageB2->setRequires(array('a' => new Link('B', 'A', $this->getVersionConstraint('>=', '1.0'), 'requires')));
 
         $this->reposComplete();
 
@@ -555,13 +562,13 @@ class SolverTest extends TestCase
         $this->repo->addPackage($packageB = $this->getPackage('B', '1.0'));
         $this->repo->addPackage($packageC = $this->getPackage('C', '1.0'));
         $this->repo->addPackage($packageD = $this->getPackage('D', '1.0'));
-        $packageA->setRequires(array(new Link('A', 'B', $this->getVersionConstraint('>=', '1.0'), 'requires')));
-        $packageB->setRequires(array(new Link('B', 'Virtual', $this->getVersionConstraint('>=', '1.0'), 'requires')));
-        $packageC->setProvides(array(new Link('C', 'Virtual', $this->getVersionConstraint('==', '1.0'), 'provides')));
-        $packageD->setProvides(array(new Link('D', 'Virtual', $this->getVersionConstraint('==', '1.0'), 'provides')));
+        $packageA->setRequires(array('b' => new Link('A', 'B', $this->getVersionConstraint('>=', '1.0'), 'requires')));
+        $packageB->setRequires(array('virtual' => new Link('B', 'Virtual', $this->getVersionConstraint('>=', '1.0'), 'requires')));
+        $packageC->setProvides(array('virtual' => new Link('C', 'Virtual', $this->getVersionConstraint('==', '1.0'), 'provides')));
+        $packageD->setProvides(array('virtual' => new Link('D', 'Virtual', $this->getVersionConstraint('==', '1.0'), 'provides')));
 
-        $packageC->setRequires(array(new Link('C', 'A', $this->getVersionConstraint('==', '1.0'), 'requires')));
-        $packageD->setRequires(array(new Link('D', 'A', $this->getVersionConstraint('==', '1.0'), 'requires')));
+        $packageC->setRequires(array('a' => new Link('C', 'A', $this->getVersionConstraint('==', '1.0'), 'requires')));
+        $packageD->setRequires(array('a' => new Link('D', 'A', $this->getVersionConstraint('==', '1.0'), 'requires')));
 
         $this->reposComplete();
 
@@ -586,18 +593,18 @@ class SolverTest extends TestCase
         $this->repo->addPackage($packageD2 = $this->getPackage('D', '1.1'));
 
         $packageA->setRequires(array(
-            new Link('A', 'B', $this->getVersionConstraint('>=', '1.0'), 'requires'),
-            new Link('A', 'C', $this->getVersionConstraint('>=', '1.0'), 'requires'),
+            'b' => new Link('A', 'B', $this->getVersionConstraint('>=', '1.0'), 'requires'),
+            'c' => new Link('A', 'C', $this->getVersionConstraint('>=', '1.0'), 'requires'),
         ));
 
         $packageD->setReplaces(array(
-            new Link('D', 'B', $this->getVersionConstraint('>=', '1.0'), 'replaces'),
-            new Link('D', 'C', $this->getVersionConstraint('>=', '1.0'), 'replaces'),
+            'b' => new Link('D', 'B', $this->getVersionConstraint('>=', '1.0'), 'replaces'),
+            'c' => new Link('D', 'C', $this->getVersionConstraint('>=', '1.0'), 'replaces'),
         ));
 
         $packageD2->setReplaces(array(
-            new Link('D', 'B', $this->getVersionConstraint('>=', '1.0'), 'replaces'),
-            new Link('D', 'C', $this->getVersionConstraint('>=', '1.0'), 'replaces'),
+            'b' => new Link('D', 'B', $this->getVersionConstraint('>=', '1.0'), 'replaces'),
+            'c' => new Link('D', 'C', $this->getVersionConstraint('>=', '1.0'), 'replaces'),
         ));
 
         $this->reposComplete();
@@ -621,19 +628,19 @@ class SolverTest extends TestCase
         $this->repo->addPackage($packageD = $this->getPackage('D', '2.0.9'));
 
         $packageC->setRequires(array(
-            new Link('C', 'A', $this->getVersionConstraint('>=', '2.0'), 'requires'),
-            new Link('C', 'D', $this->getVersionConstraint('>=', '2.0'), 'requires'),
+            'a' => new Link('C', 'A', $this->getVersionConstraint('>=', '2.0'), 'requires'),
+            'd' => new Link('C', 'D', $this->getVersionConstraint('>=', '2.0'), 'requires'),
         ));
 
         $packageD->setRequires(array(
-            new Link('D', 'A', $this->getVersionConstraint('>=', '2.1'), 'requires'),
-            new Link('D', 'B', $this->getVersionConstraint('>=', '2.0-dev'), 'requires'),
+            'a' => new Link('D', 'A', $this->getVersionConstraint('>=', '2.1'), 'requires'),
+            'b' => new Link('D', 'B', $this->getVersionConstraint('>=', '2.0-dev'), 'requires'),
         ));
 
-        $packageB1->setRequires(array(new Link('B', 'A', $this->getVersionConstraint('==', '2.1.0.0-dev'), 'requires')));
-        $packageB2->setRequires(array(new Link('B', 'A', $this->getVersionConstraint('==', '2.1.0.0-dev'), 'requires')));
+        $packageB1->setRequires(array('a' => new Link('B', 'A', $this->getVersionConstraint('==', '2.1.0.0-dev'), 'requires')));
+        $packageB2->setRequires(array('a' => new Link('B', 'A', $this->getVersionConstraint('==', '2.1.0.0-dev'), 'requires')));
 
-        $packageB2->setReplaces(array(new Link('B', 'D', $this->getVersionConstraint('==', '2.0.9.0'), 'replaces')));
+        $packageB2->setReplaces(array('d' => new Link('B', 'D', $this->getVersionConstraint('==', '2.0.9.0'), 'replaces')));
 
         $this->reposComplete();
 
@@ -650,7 +657,7 @@ class SolverTest extends TestCase
         $this->repo->addPackage($packageB = $this->getPackage('B', '1.0'));;
 
         $packageA->setConflicts(array(
-            new Link('A', 'B', $this->getVersionConstraint('>=', '1.0'), 'conflicts'),
+            'b' => new Link('A', 'B', $this->getVersionConstraint('>=', '1.0'), 'conflicts'),
         ));
 
         $this->reposComplete();
@@ -680,7 +687,7 @@ class SolverTest extends TestCase
         $this->repo->addPackage($packageB = $this->getPackage('B', '1.0'));
 
         $packageA->setRequires(array(
-            new Link('A', 'B', $this->getVersionConstraint('>=', '2.0'), 'requires'),
+            'b' => new Link('A', 'B', $this->getVersionConstraint('>=', '2.0'), 'requires'),
         ));
 
         $this->reposComplete();
@@ -717,16 +724,16 @@ class SolverTest extends TestCase
         $this->repo->addPackage($packageD = $this->getPackage('D', '1.0'));
 
         $packageA->setRequires(array(
-            new Link('A', 'B', $this->getVersionConstraint('>=', '1.0'), 'requires'),
+            'b' => new Link('A', 'B', $this->getVersionConstraint('>=', '1.0'), 'requires'),
         ));
         $packageB->setRequires(array(
-            new Link('B', 'C', $this->getVersionConstraint('>=', '1.0'), 'requires'),
+            'c' => new Link('B', 'C', $this->getVersionConstraint('>=', '1.0'), 'requires'),
         ));
         $packageC->setRequires(array(
-            new Link('C', 'D', $this->getVersionConstraint('>=', '1.0'), 'requires'),
+            'd' => new Link('C', 'D', $this->getVersionConstraint('>=', '1.0'), 'requires'),
         ));
         $packageD->setRequires(array(
-            new Link('D', 'B', $this->getVersionConstraint('<', '1.0'), 'requires'),
+            'b' => new Link('D', 'B', $this->getVersionConstraint('<', '1.0'), 'requires'),
         ));
 
         $this->reposComplete();
@@ -761,11 +768,11 @@ class SolverTest extends TestCase
         $this->repo->addPackage($packageTwigBridge = $this->getPackage('symfony/twig-bridge', '2.0'));
 
         $packageTwigBridge->setRequires(array(
-            new Link('symfony/twig-bridge', 'twig/twig', $this->getVersionConstraint('<', '2.0'), 'requires'),
+            'twig/twig' => new Link('symfony/twig-bridge', 'twig/twig', $this->getVersionConstraint('<', '2.0'), 'requires'),
         ));
 
         $packageSymfony->setReplaces(array(
-            new Link('symfony/symfony', 'symfony/twig-bridge', $this->getVersionConstraint('==', '2.0'), 'replaces'),
+            'symfony/twig-bridge' => new Link('symfony/symfony', 'symfony/twig-bridge', $this->getVersionConstraint('==', '2.0'), 'replaces'),
         ));
 
         $this->reposComplete();
@@ -786,10 +793,10 @@ class SolverTest extends TestCase
         $this->repo->addPackage($packageA2 = $this->getPackage('A', '2.0'));
 
         $packageA2->setRequires(array(
-            new Link('A', 'B', $this->getVersionConstraint('==', '2.0'), 'requires', '== 2.0'),
+            'b' => new Link('A', 'B', $this->getVersionConstraint('==', '2.0'), 'requires', '== 2.0'),
         ));
         $packageB->setRequires(array(
-            new Link('B', 'A', $this->getVersionConstraint('>=', '2.0'), 'requires'),
+            'a' => new Link('B', 'A', $this->getVersionConstraint('>=', '2.0'), 'requires'),
         ));
 
         $this->repo->addPackage($packageA2Alias = $this->getAliasPackage($packageA2, '1.1'));
@@ -811,7 +818,7 @@ class SolverTest extends TestCase
         $this->repo->addPackage($packageB = $this->getPackage('B', '1.0'));
 
         $packageB->setRequires(array(
-            new Link('B', 'A', $this->getVersionConstraint('<', '2.0'), 'requires'),
+            'a' => new Link('B', 'A', $this->getVersionConstraint('<', '2.0'), 'requires'),
         ));
 
         $this->repo->addPackage($packageAAlias = $this->getAliasPackage($packageA, '1.1'));

+ 1 - 1
tests/Composer/Test/Package/Loader/ArrayLoaderTest.php

@@ -34,7 +34,7 @@ class ArrayLoaderTest extends \PHPUnit_Framework_TestCase
 
         $package = $this->loader->load($config);
         $replaces = $package->getReplaces();
-        $this->assertEquals('== 1.2.3.4', (string) $replaces[0]->getConstraint());
+        $this->assertEquals('== 1.2.3.4', (string) $replaces['foo']->getConstraint());
     }
 
     public function testTypeDefault()