From aea2e901a93f6923417d01fe03f6910137ae557c Mon Sep 17 00:00:00 2001 From: Ryan Weaver Date: Mon, 14 Jul 2014 21:25:44 -0500 Subject: [PATCH] Extracting logic into a new class related to selecting the latest version Also refactored InitCommand slightly so that you can use this "latest version" functionality when searching for a package as well. --- src/Composer/Command/CreateProjectCommand.php | 18 +--- src/Composer/Command/InitCommand.php | 93 +++++++++++-------- .../Package/Version/VersionSelector.php | 71 ++++++++++++++ .../Package/Version/VersionSelectorTest.php | 71 ++++++++++++++ 4 files changed, 201 insertions(+), 52 deletions(-) create mode 100644 src/Composer/Package/Version/VersionSelector.php create mode 100644 tests/Composer/Test/Package/Version/VersionSelectorTest.php diff --git a/src/Composer/Command/CreateProjectCommand.php b/src/Composer/Command/CreateProjectCommand.php index e8201cb2b..9928692eb 100644 --- a/src/Composer/Command/CreateProjectCommand.php +++ b/src/Composer/Command/CreateProjectCommand.php @@ -21,6 +21,7 @@ use Composer\IO\IOInterface; use Composer\Package\BasePackage; use Composer\DependencyResolver\Pool; use Composer\DependencyResolver\Operation\InstallOperation; +use Composer\Package\Version\VersionSelector; use Composer\Repository\ComposerRepository; use Composer\Repository\CompositeRepository; use Composer\Repository\FilesystemRepository; @@ -265,23 +266,14 @@ EOT $pool = new Pool($stability); $pool->addRepository($sourceRepo); - $constraint = $packageVersion ? $parser->parseConstraints($packageVersion) : null; - $candidates = $pool->whatProvides($name, $constraint, true); + // find the latest version if there are multiple + $versionSelector = new VersionSelector($pool); + $package = $versionSelector->findBestCandidate($name, $packageVersion); - if (!$candidates) { + if (!$package) { throw new \InvalidArgumentException("Could not find package $name" . ($packageVersion ? " with version $packageVersion." : " with stability $stability.")); } - // select highest version if we have many - // logic is repeated in InitCommand - $package = reset($candidates); - foreach ($candidates as $candidate) { - if (version_compare($package->getVersion(), $candidate->getVersion(), '<')) { - $package = $candidate; - } - } - unset($candidates); - if (null === $directory) { $parts = explode("/", $name, 2); $directory = getcwd() . DIRECTORY_SEPARATOR . array_pop($parts); diff --git a/src/Composer/Command/InitCommand.php b/src/Composer/Command/InitCommand.php index f0f41a006..b0ce567a8 100644 --- a/src/Composer/Command/InitCommand.php +++ b/src/Composer/Command/InitCommand.php @@ -16,6 +16,7 @@ use Composer\DependencyResolver\Pool; use Composer\Json\JsonFile; use Composer\Factory; use Composer\Package\BasePackage; +use Composer\Package\Version\VersionSelector; use Composer\Repository\CompositeRepository; use Composer\Repository\PlatformRepository; use Composer\Package\Version\VersionParser; @@ -323,42 +324,8 @@ EOT foreach ($requires as $requirement) { if (!isset($requirement['version'])) { - $candidates = $this->getPool()->whatProvides($requirement['name'], null, true); - - if (!$candidates) { - throw new \InvalidArgumentException(sprintf( - 'Could not find any versions for package "%s". Perhaps the name is wrong?', - $requirement['name'] - )); - } - - // select highest version if we have many - // logic is repeated in CreateProjectCommand - $package = reset($candidates); - foreach ($candidates as $candidate) { - if (version_compare($package->getVersion(), $candidate->getVersion(), '<')) { - $package = $candidate; - } - } - - if (!$package) { - throw new \Exception(sprintf( - 'No version of the package "%s" could be found that meets your minimum stability requirements of "%s"', - $requirement['name'], - $this->getComposer()->getPackage()->getMinimumStability() - )); - } - - $version = $package->getPrettyVersion(); - if (!$package->isDev()) { - // remove the v prefix if there is one - if (substr($version, 0, 1) == 'v') { - $version = substr($version, 1); - } - - // 2.1.0 -> ~2.1.0, 2.0-beta.1 -> ~2.0-beta.1 - $version = '~'.$version; - } + // determine the best version automatically + $version = $this->findBestVersionForPackage($requirement['name']); $requirement['version'] = $version; $output->writeln(sprintf( @@ -420,7 +387,7 @@ EOT $package = $dialog->askAndValidate($output, $dialog->getQuestion('Enter package # to add, or the complete package name if it is not listed', false, ':'), $validator, 3); } - // no constraint yet, prompt user + // no constraint yet, determine the best version automatically if (false !== $package && false === strpos($package, ' ')) { $validator = function ($input) { $input = trim($input); @@ -428,9 +395,20 @@ EOT return $input ?: false; }; - $constraint = $dialog->askAndValidate($output, $dialog->getQuestion('Enter the version constraint to require', false, ':'), $validator, 3); + $constraint = $dialog->askAndValidate( + $output, + $dialog->getQuestion('Enter the version constraint to require (or leave blank to use the latest version)', false, ':'), + $validator, + 3) + ; if (false === $constraint) { - continue; + $constraint = $this->findBestVersionForPackage($package); + + $output->writeln(sprintf( + 'Using version %s for %s', + $constraint, + $package + )); } $package .= ' '.$constraint; @@ -555,4 +533,41 @@ EOT return false !== filter_var($email, FILTER_VALIDATE_EMAIL); } + + /** + * Given a package name, this determines the best version to use in the require key. + * + * This returns a version with the ~ operator prefixed when possible. + * + * @param string $name + * @return string + * @throws \InvalidArgumentException + */ + protected function findBestVersionForPackage($name) + { + // find the latest version allowed in this pool + $versionSelector = new VersionSelector($this->getPool()); + $package = $versionSelector->findBestCandidate($name); + + if (!$package) { + throw new \InvalidArgumentException(sprintf( + 'Could not find package %s at any version for your minimum-stability (%s). Check the package spelling or your minimum-stability', + $name, + $this->getComposer()->getPackage()->getMinimumStability() + )); + } + + $version = $package->getPrettyVersion(); + if (!$package->isDev()) { + // remove the v prefix if there is one + if (substr($version, 0, 1) == 'v') { + $version = substr($version, 1); + } + + // 2.1.0 -> ~2.1.0, 2.0-beta.1 -> ~2.0-beta.1 + $version = '~'.$version; + } + + return $version; + } } diff --git a/src/Composer/Package/Version/VersionSelector.php b/src/Composer/Package/Version/VersionSelector.php new file mode 100644 index 000000000..ad9c2ab71 --- /dev/null +++ b/src/Composer/Package/Version/VersionSelector.php @@ -0,0 +1,71 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Package\Version; + +use Composer\DependencyResolver\Pool; +use Composer\Package\PackageInterface; + +/** + * Selects the best possible version for a package + * + * @author Ryan Weaver + */ +class VersionSelector +{ + private $pool; + + private $parser; + + public function __construct(Pool $pool) + { + $this->pool = $pool; + } + + /** + * Given a package name and optional version, returns the latest PackageInterface + * that matches. + * + * @param string $packageName + * @param string $targetPackageVersion + * @return PackageInterface|bool + */ + public function findBestCandidate($packageName, $targetPackageVersion = null) + { + $constraint = $targetPackageVersion ? $this->getParser()->parseConstraints($targetPackageVersion) : null; + $candidates = $this->pool->whatProvides($packageName, $constraint, true); + + if (!$candidates) { + return false; + } + + // select highest version if we have many + // logic is repeated in InitCommand + $package = reset($candidates); + foreach ($candidates as $candidate) { + if (version_compare($package->getVersion(), $candidate->getVersion(), '<')) { + $package = $candidate; + } + } + + return $package; + } + + private function getParser() + { + if ($this->parser === null) { + $this->parser = new VersionParser(); + } + + return $this->parser; + } +} diff --git a/tests/Composer/Test/Package/Version/VersionSelectorTest.php b/tests/Composer/Test/Package/Version/VersionSelectorTest.php new file mode 100644 index 000000000..636645807 --- /dev/null +++ b/tests/Composer/Test/Package/Version/VersionSelectorTest.php @@ -0,0 +1,71 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Test\Package\Version; + +use Composer\Package\Version\VersionSelector; + +class VersionSelectorTest extends \PHPUnit_Framework_TestCase +{ + // A) multiple versions, get the latest one + // B) targetPackageVersion will pass to pool + // C) No results, throw exception + + public function testLatestVersionIsReturned() + { + $packageName = 'foobar'; + + $package1 = $this->createMockPackage('1.2.1'); + $package2 = $this->createMockPackage('1.2.2'); + $package3 = $this->createMockPackage('1.2.0'); + $packages = array($package1, $package2, $package3); + + $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); + + // 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'); + } + + public function testFalseReturnedOnNoPackages() + { + $pool = $this->createMockPool(); + $pool->expects($this->once()) + ->method('whatProvides') + ->will($this->returnValue(array())); + + $versionSelector = new VersionSelector($pool); + $best = $versionSelector->findBestCandidate('foobaz'); + $this->assertFalse($best, 'No versions are available returns false'); + } + + private function createMockPackage($version) + { + $package = $this->getMock('\Composer\Package\PackageInterface'); + $package->expects($this->any()) + ->method('getVersion') + ->will($this->returnValue($version)); + + return $package; + } + + private function createMockPool() + { + return $this->getMock('Composer\DependencyResolver\Pool', array(), array(), '', true); + } +}