From 507415e40477c79527751fddbac21cbe617af16c Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Tue, 27 Oct 2015 14:43:04 +0000 Subject: [PATCH] Make VersionSelector prefer stable by default and allow specifying a PHP version that must be matched, fixes #4318 --- src/Composer/Command/InitCommand.php | 11 +-- src/Composer/Command/RequireCommand.php | 10 ++- src/Composer/Package/BasePackage.php | 5 ++ .../Package/Version/VersionSelector.php | 16 +++- .../Package/Version/VersionSelectorTest.php | 85 ++++++++++++++++--- 5 files changed, 107 insertions(+), 20 deletions(-) diff --git a/src/Composer/Command/InitCommand.php b/src/Composer/Command/InitCommand.php index 795d72958..cd3f7395d 100644 --- a/src/Composer/Command/InitCommand.php +++ b/src/Composer/Command/InitCommand.php @@ -321,7 +321,7 @@ EOT return $this->repos; } - protected function determineRequirements(InputInterface $input, OutputInterface $output, $requires = array()) + protected function determineRequirements(InputInterface $input, OutputInterface $output, $requires = array(), $phpVersion = null) { if ($requires) { $requires = $this->normalizeRequirements($requires); @@ -331,7 +331,7 @@ EOT foreach ($requires as $requirement) { if (!isset($requirement['version'])) { // determine the best version automatically - $version = $this->findBestVersionForPackage($input, $requirement['name']); + $version = $this->findBestVersionForPackage($input, $requirement['name'], $phpVersion); $requirement['version'] = $version; $io->writeError(sprintf( @@ -426,7 +426,7 @@ EOT ); if (false === $constraint) { - $constraint = $this->findBestVersionForPackage($input, $package); + $constraint = $this->findBestVersionForPackage($input, $package, $phpVersion); $io->writeError(sprintf( 'Using version %s for %s', @@ -591,14 +591,15 @@ EOT * * @param InputInterface $input * @param string $name + * @param string $phpVersion * @throws \InvalidArgumentException * @return string */ - private function findBestVersionForPackage(InputInterface $input, $name) + private function findBestVersionForPackage(InputInterface $input, $name, $phpVersion) { // find the latest version allowed in this pool $versionSelector = new VersionSelector($this->getPool($input)); - $package = $versionSelector->findBestCandidate($name); + $package = $versionSelector->findBestCandidate($name, null, $phpVersion); if (!$package) { throw new \InvalidArgumentException(sprintf( diff --git a/src/Composer/Command/RequireCommand.php b/src/Composer/Command/RequireCommand.php index c7bcc8735..79f281595 100644 --- a/src/Composer/Command/RequireCommand.php +++ b/src/Composer/Command/RequireCommand.php @@ -96,12 +96,18 @@ EOT $composer = $this->getComposer(); $repos = $composer->getRepositoryManager()->getRepositories(); + $platformOverrides = $composer->getConfig()->get('platform') ?: array(); + // initialize $this->repos as it is used by the parent InitCommand $this->repos = new CompositeRepository(array_merge( - array(new PlatformRepository), + array(new PlatformRepository(array(), $platformOverrides)), $repos )); - $requirements = $this->determineRequirements($input, $output, $input->getArgument('packages')); + $phpPackages = $this->repos->findPackages('php'); + $phpPackage = reset($phpPackages); + $phpVersion = $phpPackage->getVersion(); + unset($phpPackage, $phpPackages); + $requirements = $this->determineRequirements($input, $output, $input->getArgument('packages'), $phpVersion); $requireKey = $input->getOption('dev') ? 'require-dev' : 'require'; $removeKey = $input->getOption('dev') ? 'require' : 'require-dev'; diff --git a/src/Composer/Package/BasePackage.php b/src/Composer/Package/BasePackage.php index 0369a36dd..3dd390aa0 100644 --- a/src/Composer/Package/BasePackage.php +++ b/src/Composer/Package/BasePackage.php @@ -223,6 +223,11 @@ abstract class BasePackage implements PackageInterface return $this->getPrettyVersion() . ' ' . $this->getSourceReference(); } + public function getStabilityPriority() + { + return self::$stabilities[$this->getStability()]; + } + public function __clone() { $this->repository = null; diff --git a/src/Composer/Package/Version/VersionSelector.php b/src/Composer/Package/Version/VersionSelector.php index a22cf2c70..03bb62fe7 100644 --- a/src/Composer/Package/Version/VersionSelector.php +++ b/src/Composer/Package/Version/VersionSelector.php @@ -17,6 +17,8 @@ use Composer\Package\PackageInterface; use Composer\Package\Loader\ArrayLoader; use Composer\Package\Dumper\ArrayDumper; use Composer\Semver\VersionParser as SemverVersionParser; +use Composer\Semver\Semver; +use Composer\Semver\Constraint\Constraint; /** * Selects the best possible version for a package @@ -41,13 +43,22 @@ class VersionSelector * * @param string $packageName * @param string $targetPackageVersion + * @param string $targetPhpVersion * @return PackageInterface|bool */ - public function findBestCandidate($packageName, $targetPackageVersion = null) + public function findBestCandidate($packageName, $targetPackageVersion = null, $targetPhpVersion = null, $preferStable = true) { $constraint = $targetPackageVersion ? $this->getParser()->parseConstraints($targetPackageVersion) : null; $candidates = $this->pool->whatProvides(strtolower($packageName), $constraint, true); + if ($targetPhpVersion) { + $phpConstraint = new Constraint('==', $this->getParser()->normalize($targetPhpVersion)); + $candidates = array_filter($candidates, function ($pkg) use ($phpConstraint) { + $reqs = $pkg->getRequires(); + return !isset($reqs['php']) || $reqs['php']->getConstraint()->matches($phpConstraint); + }); + } + if (!$candidates) { return false; } @@ -55,6 +66,9 @@ class VersionSelector // select highest version if we have many $package = reset($candidates); foreach ($candidates as $candidate) { + if ($preferStable && $package->getStabilityPriority() < $candidate->getStabilityPriority()) { + continue; + } if (version_compare($package->getVersion(), $candidate->getVersion(), '<')) { $package = $candidate; } diff --git a/tests/Composer/Test/Package/Version/VersionSelectorTest.php b/tests/Composer/Test/Package/Version/VersionSelectorTest.php index a48cc805d..f4d441ee8 100644 --- a/tests/Composer/Test/Package/Version/VersionSelectorTest.php +++ b/tests/Composer/Test/Package/Version/VersionSelectorTest.php @@ -13,6 +13,8 @@ namespace Composer\Test\Package\Version; use Composer\Package\Version\VersionSelector; +use Composer\Package\Package; +use Composer\Package\Link; use Composer\Semver\VersionParser; class VersionSelectorTest extends \PHPUnit_Framework_TestCase @@ -25,9 +27,9 @@ class VersionSelectorTest extends \PHPUnit_Framework_TestCase { $packageName = 'foobar'; - $package1 = $this->createMockPackage('1.2.1'); - $package2 = $this->createMockPackage('1.2.2'); - $package3 = $this->createMockPackage('1.2.0'); + $package1 = $this->createPackage('1.2.1'); + $package2 = $this->createPackage('1.2.2'); + $package3 = $this->createPackage('1.2.0'); $packages = array($package1, $package2, $package3); $pool = $this->createMockPool(); @@ -40,7 +42,70 @@ class VersionSelectorTest extends \PHPUnit_Framework_TestCase $best = $versionSelector->findBestCandidate($packageName); // 1.2.2 should be returned because it's the latest of the returned versions - $this->assertEquals($package2, $best, 'Latest version should be 1.2.2'); + $this->assertSame($package2, $best, 'Latest version should be 1.2.2'); + } + + public function testLatestVersionIsReturnedThatMatchesPhpRequirement() + { + $packageName = 'foobar'; + + $parser = new VersionParser; + $package1 = $this->createPackage('1.0.0'); + $package2 = $this->createPackage('2.0.0'); + $package1->setRequires(array('php' => new Link($packageName, 'php', $parser->parseConstraints('>=5.4'), 'requires', '>=5.4'))); + $package2->setRequires(array('php' => new Link($packageName, 'php', $parser->parseConstraints('>=5.6'), 'requires', '>=5.6'))); + $packages = array($package1, $package2); + + $pool = $this->createMockPool(); + $pool->expects($this->once()) + ->method('whatProvides') + ->with($packageName, null, true) + ->will($this->returnValue($packages)); + + $versionSelector = new VersionSelector($pool); + $best = $versionSelector->findBestCandidate($packageName, null, '5.5.0'); + + $this->assertSame($package1, $best, 'Latest version supporting php 5.5 should be returned (1.0.0)'); + } + + public function testMostStableVersionIsReturned() + { + $packageName = 'foobar'; + + $package1 = $this->createPackage('1.0.0'); + $package2 = $this->createPackage('1.1.0-beta'); + $packages = array($package1, $package2); + + $pool = $this->createMockPool(); + $pool->expects($this->once()) + ->method('whatProvides') + ->with($packageName, null, true) + ->will($this->returnValue($packages)); + + $versionSelector = new VersionSelector($pool); + $best = $versionSelector->findBestCandidate($packageName); + + $this->assertSame($package1, $best, 'Latest most stable version should be returned (1.0.0)'); + } + + public function testHighestVersionIsReturned() + { + $packageName = 'foobar'; + + $package1 = $this->createPackage('1.0.0'); + $package2 = $this->createPackage('1.1.0-beta'); + $packages = array($package1, $package2); + + $pool = $this->createMockPool(); + $pool->expects($this->once()) + ->method('whatProvides') + ->with($packageName, null, true) + ->will($this->returnValue($packages)); + + $versionSelector = new VersionSelector($pool); + $best = $versionSelector->findBestCandidate($packageName, null, null, false); + + $this->assertSame($package2, $best, 'Latest version should be returned (1.1.0-beta)'); } public function testFalseReturnedOnNoPackages() @@ -86,7 +151,7 @@ class VersionSelectorTest extends \PHPUnit_Framework_TestCase $recommended = $versionSelector->findRecommendedRequireVersion($package); // assert that the recommended version is what we expect - $this->assertEquals($expectedVersion, $recommended); + $this->assertSame($expectedVersion, $recommended); } public function getRecommendedRequireVersionPackages() @@ -124,14 +189,10 @@ class VersionSelectorTest extends \PHPUnit_Framework_TestCase ); } - private function createMockPackage($version) + private function createPackage($version) { - $package = $this->getMock('\Composer\Package\PackageInterface'); - $package->expects($this->any()) - ->method('getVersion') - ->will($this->returnValue($version)); - - return $package; + $parser = new VersionParser(); + return new Package('foo', $parser->normalize($version), $version); } private function createMockPool()