VersionSelector.php 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. <?php
  2. /*
  3. * This file is part of Composer.
  4. *
  5. * (c) Nils Adermann <naderman@naderman.de>
  6. * Jordi Boggiano <j.boggiano@seld.be>
  7. *
  8. * For the full copyright and license information, please view the LICENSE
  9. * file that was distributed with this source code.
  10. */
  11. namespace Composer\Package\Version;
  12. use Composer\DependencyResolver\Pool;
  13. use Composer\Package\BasePackage;
  14. use Composer\Package\PackageInterface;
  15. use Composer\Plugin\PluginInterface;
  16. use Composer\Composer;
  17. use Composer\Package\Loader\ArrayLoader;
  18. use Composer\Package\Dumper\ArrayDumper;
  19. use Composer\Repository\RepositorySet;
  20. use Composer\Repository\PlatformRepository;
  21. use Composer\Semver\Constraint\Constraint;
  22. /**
  23. * Selects the best possible version for a package
  24. *
  25. * @author Ryan Weaver <ryan@knpuniversity.com>
  26. * @author Jordi Boggiano <j.boggiano@seld.be>
  27. */
  28. class VersionSelector
  29. {
  30. private $repositorySet;
  31. private $platformConstraints;
  32. private $parser;
  33. /**
  34. * @param PlatformRepository $platformRepo If passed in, the versions found will be filtered against their requirements to eliminate any not matching the current platform packages
  35. */
  36. public function __construct(RepositorySet $repositorySet, PlatformRepository $platformRepo = null)
  37. {
  38. $this->repositorySet = $repositorySet;
  39. if ($platformRepo) {
  40. $this->platformConstraints = array();
  41. foreach ($platformRepo->getPackages() as $package) {
  42. $this->platformConstraints[$package->getName()][] = new Constraint('==', $package->getVersion());
  43. }
  44. }
  45. }
  46. /**
  47. * Given a package name and optional version, returns the latest PackageInterface
  48. * that matches.
  49. *
  50. * @param string $packageName
  51. * @param string $targetPackageVersion
  52. * @param string $preferredStability
  53. * @param bool $ignorePlatformReqs
  54. * @return PackageInterface|false
  55. */
  56. public function findBestCandidate($packageName, $targetPackageVersion = null, $preferredStability = 'stable', $ignorePlatformReqs = false)
  57. {
  58. if (!isset(BasePackage::$stabilities[$preferredStability])) {
  59. // If you get this, maybe you are still relying on the Composer 1.x signature where the 3rd arg was the php version
  60. throw new \UnexpectedValueException('Expected a valid stability name as 3rd argument, got '.$preferredStability);
  61. }
  62. $constraint = $targetPackageVersion ? $this->getParser()->parseConstraints($targetPackageVersion) : null;
  63. $candidates = $this->repositorySet->findPackages(strtolower($packageName), $constraint);
  64. if ($this->platformConstraints && !$ignorePlatformReqs) {
  65. $platformConstraints = $this->platformConstraints;
  66. $candidates = array_filter($candidates, function ($pkg) use ($platformConstraints) {
  67. $reqs = $pkg->getRequires();
  68. foreach ($reqs as $name => $link) {
  69. if (isset($platformConstraints[$name])) {
  70. foreach ($platformConstraints[$name] as $constraint) {
  71. if ($link->getConstraint()->matches($constraint)) {
  72. continue 2;
  73. }
  74. }
  75. return false;
  76. }
  77. }
  78. return true;
  79. });
  80. }
  81. if (!$candidates) {
  82. return false;
  83. }
  84. // select highest version if we have many
  85. $package = reset($candidates);
  86. $minPriority = BasePackage::$stabilities[$preferredStability];
  87. foreach ($candidates as $candidate) {
  88. $candidatePriority = $candidate->getStabilityPriority();
  89. $currentPriority = $package->getStabilityPriority();
  90. // candidate is less stable than our preferred stability,
  91. // and current package is more stable than candidate, skip it
  92. if ($minPriority < $candidatePriority && $currentPriority < $candidatePriority) {
  93. continue;
  94. }
  95. // candidate is less stable than our preferred stability,
  96. // and current package is less stable than candidate, select candidate
  97. if ($minPriority < $candidatePriority && $candidatePriority < $currentPriority) {
  98. $package = $candidate;
  99. continue;
  100. }
  101. // candidate is more stable than our preferred stability,
  102. // and current package is less stable than preferred stability, select candidate
  103. if ($minPriority >= $candidatePriority && $minPriority < $currentPriority) {
  104. $package = $candidate;
  105. continue;
  106. }
  107. // select highest version of the two
  108. if (version_compare($package->getVersion(), $candidate->getVersion(), '<')) {
  109. $package = $candidate;
  110. }
  111. }
  112. return $package;
  113. }
  114. /**
  115. * Given a concrete version, this returns a ~ constraint (when possible)
  116. * that should be used, for example, in composer.json.
  117. *
  118. * For example:
  119. * * 1.2.1 -> ^1.2
  120. * * 1.2 -> ^1.2
  121. * * v3.2.1 -> ^3.2
  122. * * 2.0-beta.1 -> ^2.0@beta
  123. * * dev-master -> ^2.1@dev (dev version with alias)
  124. * * dev-master -> dev-master (dev versions are untouched)
  125. *
  126. * @param PackageInterface $package
  127. * @return string
  128. */
  129. public function findRecommendedRequireVersion(PackageInterface $package)
  130. {
  131. $version = $package->getVersion();
  132. if (!$package->isDev()) {
  133. return $this->transformVersion($version, $package->getPrettyVersion(), $package->getStability());
  134. }
  135. $loader = new ArrayLoader($this->getParser());
  136. $dumper = new ArrayDumper();
  137. $extra = $loader->getBranchAlias($dumper->dump($package));
  138. if ($extra) {
  139. $extra = preg_replace('{^(\d+\.\d+\.\d+)(\.9999999)-dev$}', '$1.0', $extra, -1, $count);
  140. if ($count) {
  141. $extra = str_replace('.9999999', '.0', $extra);
  142. return $this->transformVersion($extra, $extra, 'dev');
  143. }
  144. }
  145. return $package->getPrettyVersion();
  146. }
  147. private function transformVersion($version, $prettyVersion, $stability)
  148. {
  149. // attempt to transform 2.1.1 to 2.1
  150. // this allows you to upgrade through minor versions
  151. $semanticVersionParts = explode('.', $version);
  152. // check to see if we have a semver-looking version
  153. if (count($semanticVersionParts) == 4 && preg_match('{^0\D?}', $semanticVersionParts[3])) {
  154. // remove the last parts (i.e. the patch version number and any extra)
  155. if ($semanticVersionParts[0] === '0') {
  156. unset($semanticVersionParts[3]);
  157. } else {
  158. unset($semanticVersionParts[2], $semanticVersionParts[3]);
  159. }
  160. $version = implode('.', $semanticVersionParts);
  161. } else {
  162. return $prettyVersion;
  163. }
  164. // append stability flag if not default
  165. if ($stability != 'stable') {
  166. $version .= '@'.$stability;
  167. }
  168. // 2.1 -> ^2.1
  169. return '^' . $version;
  170. }
  171. private function getParser()
  172. {
  173. if ($this->parser === null) {
  174. $this->parser = new VersionParser();
  175. }
  176. return $this->parser;
  177. }
  178. }