diff --git a/src/Composer/Command/CreateProjectCommand.php b/src/Composer/Command/CreateProjectCommand.php index 817bcf4cd..28ae38c55 100644 --- a/src/Composer/Command/CreateProjectCommand.php +++ b/src/Composer/Command/CreateProjectCommand.php @@ -276,6 +276,7 @@ EOT $pool = new Pool($stability); $pool->addRepository($sourceRepo); + $pool->loadRecursively(array($name)); // find the latest version if there are multiple $versionSelector = new VersionSelector($pool); diff --git a/src/Composer/DependencyResolver/Pool.php b/src/Composer/DependencyResolver/Pool.php index 000d63805..222c414cb 100644 --- a/src/Composer/DependencyResolver/Pool.php +++ b/src/Composer/DependencyResolver/Pool.php @@ -99,40 +99,9 @@ class Pool if ($repo instanceof ComposerRepository && $repo->hasProviders()) { $this->providerRepos[] = $repo; $repo->setRootAliases($rootAliases); - $repo->resetPackageIds(); } else { foreach ($repo->getPackages() as $package) { - $names = $package->getNames(); - $stability = $package->getStability(); - if ($exempt || $this->isPackageAcceptable($names, $stability)) { - $package->setId($this->id++); - $this->packages[] = $package; - $this->packageByExactName[$package->getName()][$package->id] = $package; - - foreach ($names as $provided) { - $this->packageByName[$provided][] = $package; - } - - // handle root package aliases - $name = $package->getName(); - if (isset($rootAliases[$name][$package->getVersion()])) { - $alias = $rootAliases[$name][$package->getVersion()]; - if ($package instanceof AliasPackage) { - $package = $package->getAliasOf(); - } - $aliasPackage = new AliasPackage($package, $alias['alias_normalized'], $alias['alias']); - $aliasPackage->setRootPackageAlias(true); - $aliasPackage->setId($this->id++); - - $package->getRepository()->addPackage($aliasPackage); - $this->packages[] = $aliasPackage; - $this->packageByExactName[$aliasPackage->getName()][$aliasPackage->id] = $aliasPackage; - - foreach ($aliasPackage->getNames() as $name) { - $this->packageByName[$name][] = $aliasPackage; - } - } - } + $this->loadPackage($package, $rootAliases, $exempt); } } } @@ -160,6 +129,38 @@ class Pool return $this->packages[$id - 1]; } + /** + * Ensures that all given names and their requirements are loaded. + * + * @param array $packageNames A list of names that need to be available + */ + public function loadRecursively(array $packageNames) + { + $loadedMap = array(); + do { + $newPackageNames = array(); + $loadedCount = count($loadedMap); + + foreach ($this->providerRepos as $repo) { + $packages = $repo->loadRecursively( + $packageNames, + array($this, 'isPackageAcceptable') + ); + + foreach ($packages as $package) { + $name = $package->getName(); + if (!isset($loadedMap[$name])) { + $loadedMap[$name] = true; + $newPackageNames[] = $name; + } + $this->loadPackage($package, $repo->getRootAliases()); + } + } + + $packageNames = $newPackageNames; + } while (count($loadedMap) > $loadedCount); + } + /** * Searches all packages providing the given package name and match the constraint * @@ -180,6 +181,44 @@ class Pool return $this->providerCache[$name][$key] = $this->computeWhatProvides($name, $constraint, $mustMatchName); } + private function loadPackage(PackageInterface $package, array $rootAliases, $acceptableExemption = false) + { + $names = $package->getNames(); + $stability = $package->getStability(); + + if (!$acceptableExemption && !$this->isPackageAcceptable($names, $stability)) { + return; + } + + $package->setId($this->id++); + $this->packages[] = $package; + $this->packageByExactName[$package->getName()][$package->id] = $package; + + foreach ($names as $provided) { + $this->packageByName[$provided][] = $package; + } + + // handle root package aliases + $name = $package->getName(); + if (isset($rootAliases[$name][$package->getVersion()])) { + $alias = $rootAliases[$name][$package->getVersion()]; + if ($package instanceof AliasPackage) { + $package = $package->getAliasOf(); + } + $aliasPackage = new AliasPackage($package, $alias['alias_normalized'], $alias['alias']); + $aliasPackage->setRootPackageAlias(true); + $aliasPackage->setId($this->id++); + + $package->getRepository()->addPackage($aliasPackage); + $this->packages[] = $aliasPackage; + $this->packageByExactName[$aliasPackage->getName()][$aliasPackage->id] = $aliasPackage; + + foreach ($aliasPackage->getNames() as $name) { + $this->packageByName[$name][] = $aliasPackage; + } + } + } + /** * @see whatProvides */ @@ -187,25 +226,12 @@ class Pool { $candidates = array(); - foreach ($this->providerRepos as $repo) { - foreach ($repo->whatProvides($this, $name) as $candidate) { - $candidates[] = $candidate; - if ($candidate->id < 1) { - $candidate->setId($this->id++); - $this->packages[$this->id - 2] = $candidate; - } - } - } - if ($mustMatchName) { - $candidates = array_filter($candidates, function ($candidate) use ($name) { - return $candidate->getName() == $name; - }); if (isset($this->packageByExactName[$name])) { - $candidates = array_merge($candidates, $this->packageByExactName[$name]); + $candidates = $this->packageByExactName[$name]; } } elseif (isset($this->packageByName[$name])) { - $candidates = array_merge($candidates, $this->packageByName[$name]); + $candidates = $this->packageByName[$name]; } $matches = $provideMatches = array(); diff --git a/src/Composer/DependencyResolver/Solver.php b/src/Composer/DependencyResolver/Solver.php index 6975df2cd..bcfd08388 100644 --- a/src/Composer/DependencyResolver/Solver.php +++ b/src/Composer/DependencyResolver/Solver.php @@ -169,6 +169,18 @@ class Solver $this->jobs = $request->getJobs(); $this->setupInstalledMap(); + + $packageNames = array(); + foreach ($this->jobs as $job) { + switch ($job['cmd']) { + case 'install': + $packageNames[$job['packageName']] = true; + break; + } + } + + $this->pool->loadRecursively(array_keys($packageNames)); + $this->rules = $this->ruleSetGenerator->getRulesFor($this->jobs, $this->installedMap, $ignorePlatformReqs); $this->checkForRootRequireProblems($ignorePlatformReqs); $this->decisions = new Decisions($this->pool); diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index 47b215c74..619dc65c8 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -43,10 +43,9 @@ class ComposerRepository extends ArrayRepository protected $searchUrl; protected $hasProviders = false; protected $providersUrl; + protected $loadedMap = array(); protected $lazyProvidersUrl; protected $providerListing; - protected $providers = array(); - protected $providersByUid = array(); protected $loader; protected $rootAliases; protected $allowSslDowngrade = false; @@ -96,6 +95,51 @@ class ComposerRepository extends ArrayRepository $this->rootAliases = $rootAliases; } + public function getRootAliases() + { + return $this->rootAliases; + } + + /** + * Load all packages with given names and dependencies + * + * @param array $packageNames + * @param callable|null $acceptableCallback Callback to filter packages + * + * @return array The loaded package objects + */ + public function loadRecursively(array $packageNames, $acceptableCallback) + { + $workQueue = new \SplQueue; + + foreach ($packageNames as $packageName) { + $workQueue->enqueue($packageName); + } + + $loadedPackages = array(); + + while (!$workQueue->isEmpty()) { + $packageName = $workQueue->dequeue(); + if (isset($this->loadedMap[$packageName])) { + continue; + } + + $this->loadedMap[$packageName] = true; + + $packages = $this->loadName($packageName, $acceptableCallback); + + foreach ($packages as $package) { + $loadedPackages[] = $package; + $requires = $package->getRequires(); + foreach ($requires as $link) { + $workQueue->enqueue($link->getTarget()); + } + } + } + + return $loadedPackages; + } + /** * {@inheritDoc} */ @@ -111,7 +155,7 @@ class ComposerRepository extends ArrayRepository foreach ($this->getProviderNames() as $providerName) { if ($name === $providerName) { - $packages = $this->whatProvides(new Pool('dev'), $providerName); + $packages = $this->loadName($providerName, null, false); foreach ($packages as $package) { if ($name == $package->getName() && $version === $package->getVersion()) { return $package; @@ -142,7 +186,7 @@ class ComposerRepository extends ArrayRepository foreach ($this->getProviderNames() as $providerName) { if ($name === $providerName) { - $packages = $this->whatProvides(new Pool('dev'), $providerName); + $packages = $this->loadName($providerName, null, false); foreach ($packages as $package) { if ($name == $package->getName() && (null === $version || $version === $package->getVersion())) { $packages[] = $package; @@ -240,22 +284,17 @@ class ComposerRepository extends ArrayRepository return $this->hasProviders; } - public function resetPackageIds() - { - foreach ($this->providersByUid as $package) { - if ($package instanceof AliasPackage) { - $package->getAliasOf()->setId(-1); - } - $package->setId(-1); - } - } - - public function whatProvides(Pool $pool, $name) + /** + * Loads package data for a given package name or provider name + * + * @param string $name + * @param callable|null $acceptableCallback A callback to check if a package should be loaded + * @param bool $exactMatch Whether packages only providing the name should be ignored + * + * @return array All packages that were loaded + */ + protected function loadName($name, $acceptableCallback, $exactMatch = true) { - if (isset($this->providers[$name])) { - return $this->providers[$name]; - } - // skip platform packages if (preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $name) || '__root__' === $name) { return array(); @@ -296,69 +335,32 @@ class ComposerRepository extends ArrayRepository $packages = $this->fetchFile($url, $cacheKey, $hash); } - $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']]; - } - // check for root aliases - if (isset($this->providersByUid[$version['uid'].'-root'])) { - $this->providers[$name][$version['uid'].'-root'] = $this->providersByUid[$version['uid'].'-root']; - } - } - } else { - if (!$pool->isPackageAcceptable(strtolower($version['name']), VersionParser::parseStability($version['version']))) { - continue; - } - - // load acceptable packages in the providers - $package = $this->createPackage($version, 'Composer\Package\Package'); - $package->setRepository($this); - - if ($package instanceof AliasPackage) { - $aliased = $package->getAliasOf(); - $aliased->setRepository($this); - - $this->providers[$name][$version['uid']] = $aliased; - $this->providers[$name][$version['uid'].'-alias'] = $package; - - // 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; - } + $loadedPackages = array(); + foreach ($packages['packages'] as $packageName => $versions) { + if ($exactMatch && $packageName !== $name) { + continue; + } - // handle root package aliases - unset($rootAliasData); + foreach ($versions as $version) { + if ($acceptableCallback && !call_user_func( + $acceptableCallback, strtolower($version['name']), VersionParser::parseStability($version['version']) + )) { + continue; + } - 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()]; - } + // load acceptable packages in the providers + $package = $this->createPackage($version, 'Composer\Package\Package'); + $this->addPackage($package); - if (isset($rootAliasData)) { - $alias = $this->createAliasPackage($package, $rootAliasData['alias_normalized'], $rootAliasData['alias']); - $alias->setRepository($this); + $loadedPackages[] = $package; - $this->providers[$name][$version['uid'].'-root'] = $alias; - $this->providersByUid[$version['uid'].'-root'] = $alias; - } + if ($package instanceof AliasPackage) { + $loadedPackages[] = $package->getAliasOf(); } } } - return $this->providers[$name]; + return $loadedPackages; } /** diff --git a/tests/Composer/Test/Repository/ComposerRepositoryTest.php b/tests/Composer/Test/Repository/ComposerRepositoryTest.php index 5109ee41f..02d546068 100644 --- a/tests/Composer/Test/Repository/ComposerRepositoryTest.php +++ b/tests/Composer/Test/Repository/ComposerRepositoryTest.php @@ -96,7 +96,7 @@ class ComposerRepositoryTest extends TestCase ); } - public function testWhatProvides() + public function testLoadRecursively() { $repo = $this->getMockBuilder('Composer\Repository\ComposerRepository') ->disableOriginalConstructor() @@ -124,45 +124,41 @@ class ComposerRepositoryTest extends TestCase ->method('fetchFile') ->will($this->returnValue(array( 'packages' => array( - array(array( - 'uid' => 1, - 'name' => 'a', - 'version' => 'dev-master', - 'extra' => array('branch-alias' => array('dev-master' => '1.0.x-dev')), - )), - array(array( - 'uid' => 2, - 'name' => 'a', - 'version' => 'dev-develop', - 'extra' => array('branch-alias' => array('dev-develop' => '1.1.x-dev')), - )), - array(array( - 'uid' => 3, - 'name' => 'a', - 'version' => '0.6', - )), + 'a' => array( + 'dev-master' => array( + 'uid' => 1, + 'name' => 'a', + 'version' => 'dev-master', + 'extra' => array('branch-alias' => array('dev-master' => '1.0.x-dev')), + ), + 'dev-develop' => array( + 'uid' => 2, + 'name' => 'a', + 'version' => 'dev-develop', + 'extra' => array('branch-alias' => array('dev-develop' => '1.1.x-dev')), + ), + '0.6' => array( + 'uid' => 3, + 'name' => 'a', + 'version' => '0.6', + ), + ), ) ))); - $pool = $this->getMock('Composer\DependencyResolver\Pool'); - $pool->expects($this->any()) - ->method('isPackageAcceptable') - ->will($this->returnValue(true)); - $versionParser = new VersionParser(); - $repo->setRootAliases(array( - 'a' => array( - $versionParser->normalize('0.6') => array('alias' => 'dev-feature', 'alias_normalized' => $versionParser->normalize('dev-feature')), - $versionParser->normalize('1.1.x-dev') => array('alias' => '1.0', 'alias_normalized' => $versionParser->normalize('1.0')), - ), - )); - - $packages = $repo->whatProvides($pool, 'a'); - $this->assertCount(7, $packages); - $this->assertEquals(array('1', '1-alias', '2', '2-alias', '2-root', '3', '3-root'), array_keys($packages)); - $this->assertInstanceOf('Composer\Package\AliasPackage', $packages['2-root']); - $this->assertSame($packages['2'], $packages['2-root']->getAliasOf()); - $this->assertSame($packages['2'], $packages['2-alias']->getAliasOf()); + $that = $this; + $packages = $repo->loadRecursively(array('a'), function ($name, $stability) use ($that) { + $that->assertEquals('a', $name); + return true; + }); + + $this->assertCount(5, $packages); + $this->assertEquals(array('1.0.x-dev', 'dev-master', '1.1.x-dev', 'dev-develop', '0.6'), array_map(function ($p) { + return $p->getPrettyVersion(); + }, $packages)); + $this->assertInstanceOf('Composer\Package\AliasPackage', $packages[2]); + $this->assertSame($packages[3], $packages[2]->getAliasOf()); } }