PageRenderTime 27ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 1ms

/src/Composer/DependencyResolver/Problem.php

http://github.com/composer/composer
PHP | 397 lines | 266 code | 58 blank | 73 comment | 56 complexity | 38dea989f30bea9b773234898f9c250a MD5 | raw file
  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\DependencyResolver;
  12. use Composer\Package\CompletePackageInterface;
  13. use Composer\Package\AliasPackage;
  14. use Composer\Repository\RepositorySet;
  15. use Composer\Repository\LockArrayRepository;
  16. use Composer\Semver\Constraint\Constraint;
  17. use Composer\Package\Version\VersionParser;
  18. /**
  19. * Represents a problem detected while solving dependencies
  20. *
  21. * @author Nils Adermann <naderman@naderman.de>
  22. */
  23. class Problem
  24. {
  25. /**
  26. * A map containing the id of each rule part of this problem as a key
  27. * @var array
  28. */
  29. protected $reasonSeen;
  30. /**
  31. * A set of reasons for the problem, each is a rule or a root require and a rule
  32. * @var array
  33. */
  34. protected $reasons = array();
  35. protected $section = 0;
  36. /**
  37. * Add a rule as a reason
  38. *
  39. * @param Rule $rule A rule which is a reason for this problem
  40. */
  41. public function addRule(Rule $rule)
  42. {
  43. $this->addReason(spl_object_hash($rule), $rule);
  44. }
  45. /**
  46. * Retrieve all reasons for this problem
  47. *
  48. * @return array The problem's reasons
  49. */
  50. public function getReasons()
  51. {
  52. return $this->reasons;
  53. }
  54. /**
  55. * A human readable textual representation of the problem's reasons
  56. *
  57. * @param array $installedMap A map of all present packages
  58. * @return string
  59. */
  60. public function getPrettyString(RepositorySet $repositorySet, Request $request, Pool $pool, $isVerbose, array $installedMap = array(), array $learnedPool = array())
  61. {
  62. // TODO doesn't this entirely defeat the purpose of the problem sections? what's the point of sections?
  63. $reasons = call_user_func_array('array_merge', array_reverse($this->reasons));
  64. if (count($reasons) === 1) {
  65. reset($reasons);
  66. $rule = current($reasons);
  67. if (!in_array($rule->getReason(), array(Rule::RULE_ROOT_REQUIRE, Rule::RULE_FIXED), true)) {
  68. throw new \LogicException("Single reason problems must contain a request rule.");
  69. }
  70. $reasonData = $rule->getReasonData();
  71. $packageName = $reasonData['packageName'];
  72. $constraint = $reasonData['constraint'];
  73. if (isset($constraint)) {
  74. $packages = $pool->whatProvides($packageName, $constraint);
  75. } else {
  76. $packages = array();
  77. }
  78. if (empty($packages)) {
  79. return "\n ".implode(self::getMissingPackageReason($repositorySet, $request, $pool, $isVerbose, $packageName, $constraint));
  80. }
  81. }
  82. $messages = array();
  83. $templates = array();
  84. $parser = new VersionParser;
  85. $deduplicatableRuleTypes = array(Rule::RULE_PACKAGE_REQUIRES, Rule::RULE_PACKAGE_CONFLICT);
  86. foreach ($reasons as $rule) {
  87. $message = $rule->getPrettyString($repositorySet, $request, $pool, $isVerbose, $installedMap, $learnedPool);
  88. if (in_array($rule->getReason(), $deduplicatableRuleTypes, true) && preg_match('{^(?P<package>\S+) (?P<version>\S+) (?P<type>requires|conflicts)}', $message, $m)) {
  89. $template = preg_replace('{^\S+ \S+ }', '%s%s ', $message);
  90. $messages[] = $template;
  91. $templates[$template][$m[1]][$parser->normalize($m[2])] = $m[2];
  92. } else {
  93. $messages[] = $message;
  94. }
  95. }
  96. $result = array();
  97. foreach (array_unique($messages) as $message) {
  98. if (isset($templates[$message])) {
  99. foreach ($templates[$message] as $package => $versions) {
  100. uksort($versions, 'version_compare');
  101. if (!$isVerbose) {
  102. $versions = self::condenseVersionList($versions, 1);
  103. }
  104. if (count($versions) > 1) {
  105. // remove the s from requires/conflicts to correct grammar
  106. $message = preg_replace('{^(%s%s (?:require|conflict))s}', '$1', $message);
  107. $result[] = sprintf($message, $package, '['.implode(', ', $versions).']');
  108. } else {
  109. $result[] = sprintf($message, $package, ' '.reset($versions));
  110. }
  111. }
  112. } else {
  113. $result[] = $message;
  114. }
  115. }
  116. return "\n - ".implode("\n - ", $result);
  117. }
  118. public function isCausedByLock()
  119. {
  120. foreach ($this->reasons as $sectionRules) {
  121. foreach ($sectionRules as $rule) {
  122. if ($rule->isCausedByLock()) {
  123. return true;
  124. }
  125. }
  126. }
  127. }
  128. /**
  129. * Store a reason descriptor but ignore duplicates
  130. *
  131. * @param string $id A canonical identifier for the reason
  132. * @param Rule $reason The reason descriptor
  133. */
  134. protected function addReason($id, Rule $reason)
  135. {
  136. // TODO: if a rule is part of a problem description in two sections, isn't this going to remove a message
  137. // that is important to understand the issue?
  138. if (!isset($this->reasonSeen[$id])) {
  139. $this->reasonSeen[$id] = true;
  140. $this->reasons[$this->section][] = $reason;
  141. }
  142. }
  143. public function nextSection()
  144. {
  145. $this->section++;
  146. }
  147. /**
  148. * @internal
  149. */
  150. public static function getMissingPackageReason(RepositorySet $repositorySet, Request $request, Pool $pool, $isVerbose, $packageName, $constraint = null)
  151. {
  152. // handle php/hhvm
  153. if ($packageName === 'php' || $packageName === 'php-64bit' || $packageName === 'hhvm') {
  154. $version = phpversion();
  155. $available = $pool->whatProvides($packageName);
  156. if (count($available)) {
  157. $firstAvailable = reset($available);
  158. $version = $firstAvailable->getPrettyVersion();
  159. $extra = $firstAvailable->getExtra();
  160. if ($firstAvailable instanceof CompletePackageInterface && isset($extra['config.platform']) && $extra['config.platform'] === true) {
  161. $version .= '; ' . str_replace('Package ', '', $firstAvailable->getDescription());
  162. }
  163. }
  164. $msg = "- Root composer.json requires ".$packageName.self::constraintToText($constraint).' but ';
  165. if (defined('HHVM_VERSION') || (count($available) && $packageName === 'hhvm')) {
  166. return array($msg, 'your HHVM version does not satisfy that requirement.');
  167. }
  168. if ($packageName === 'hhvm') {
  169. return array($msg, 'you are running this with PHP and not HHVM.');
  170. }
  171. return array($msg, 'your '.$packageName.' version ('. $version .') does not satisfy that requirement.');
  172. }
  173. // handle php extensions
  174. if (0 === stripos($packageName, 'ext-')) {
  175. if (false !== strpos($packageName, ' ')) {
  176. return array('- ', "PHP extension ".$packageName.' should be required as '.str_replace(' ', '-', $packageName).'.');
  177. }
  178. $ext = substr($packageName, 4);
  179. $error = extension_loaded($ext) ? 'it has the wrong version ('.(phpversion($ext) ?: '0').') installed' : 'it is missing from your system';
  180. return array("- Root composer.json requires PHP extension ".$packageName.self::constraintToText($constraint).' but ', $error.'. Install or enable PHP\'s '.$ext.' extension.');
  181. }
  182. // handle linked libs
  183. if (0 === stripos($packageName, 'lib-')) {
  184. if (strtolower($packageName) === 'lib-icu') {
  185. $error = extension_loaded('intl') ? 'it has the wrong version installed, try upgrading the intl extension.' : 'it is missing from your system, make sure the intl extension is loaded.';
  186. return array("- Root composer.json requires linked library ".$packageName.self::constraintToText($constraint).' but ', $error);
  187. }
  188. return array("- Root composer.json requires linked library ".$packageName.self::constraintToText($constraint).' but ', 'it has the wrong version installed or is missing from your system, make sure to load the extension providing it.');
  189. }
  190. $fixedPackage = null;
  191. foreach ($request->getFixedPackages() as $package) {
  192. if ($package->getName() === $packageName) {
  193. $fixedPackage = $package;
  194. if ($pool->isUnacceptableFixedPackage($package)) {
  195. return array("- ", $package->getPrettyName().' is fixed to '.$package->getPrettyVersion().' (lock file version) by a partial update but that version is rejected by your minimum-stability. Make sure you list it as an argument for the update command.');
  196. }
  197. break;
  198. }
  199. }
  200. // first check if the actual requested package is found in normal conditions
  201. // if so it must mean it is rejected by another constraint than the one given here
  202. if ($packages = $repositorySet->findPackages($packageName, $constraint)) {
  203. $rootReqs = $repositorySet->getRootRequires();
  204. if (isset($rootReqs[$packageName])) {
  205. $filtered = array_filter($packages, function ($p) use ($rootReqs, $packageName) {
  206. return $rootReqs[$packageName]->matches(new Constraint('==', $p->getVersion()));
  207. });
  208. if (0 === count($filtered)) {
  209. return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages, $isVerbose).' but '.(self::hasMultipleNames($packages) ? 'these conflict' : 'it conflicts').' with your root composer.json require ('.$rootReqs[$packageName]->getPrettyString().').');
  210. }
  211. }
  212. if ($fixedPackage) {
  213. $fixedConstraint = new Constraint('==', $fixedPackage->getVersion());
  214. $filtered = array_filter($packages, function ($p) use ($fixedConstraint) {
  215. return $fixedConstraint->matches(new Constraint('==', $p->getVersion()));
  216. });
  217. if (0 === count($filtered)) {
  218. return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages, $isVerbose).' but the package is fixed to '.$fixedPackage->getPrettyVersion().' (lock file version) by a partial update and that version does not match. Make sure you list it as an argument for the update command.');
  219. }
  220. }
  221. $nonLockedPackages = array_filter($packages, function ($p) {
  222. return !$p->getRepository() instanceof LockArrayRepository;
  223. });
  224. if (!$nonLockedPackages) {
  225. return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages, $isVerbose).' in lock file but not in remote repositories, make sure you avoid updating this package to keep the one from lock file.');
  226. }
  227. return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages, $isVerbose).' but '.(self::hasMultipleNames($packages) ? 'these conflict' : 'it conflicts').' with another require.');
  228. }
  229. // check if the package is found when bypassing stability checks
  230. if ($packages = $repositorySet->findPackages($packageName, $constraint, RepositorySet::ALLOW_UNACCEPTABLE_STABILITIES)) {
  231. return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages, $isVerbose).' but '.(self::hasMultipleNames($packages) ? 'these do' : 'it does').' not match your minimum-stability.');
  232. }
  233. // check if the package is found when bypassing the constraint check
  234. if ($packages = $repositorySet->findPackages($packageName, null)) {
  235. // we must first verify if a valid package would be found in a lower priority repository
  236. if ($allReposPackages = $repositorySet->findPackages($packageName, $constraint, RepositorySet::ALLOW_SHADOWED_REPOSITORIES)) {
  237. $higherRepoPackages = $repositorySet->findPackages($packageName, null);
  238. $nextRepoPackages = array();
  239. $nextRepo = null;
  240. foreach ($allReposPackages as $package) {
  241. if ($nextRepo === null || $nextRepo === $package->getRepository()) {
  242. $nextRepoPackages[] = $package;
  243. $nextRepo = $package->getRepository();
  244. } else {
  245. break;
  246. }
  247. }
  248. return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', it is ', 'satisfiable by '.self::getPackageList($nextRepoPackages, $isVerbose).' from '.$nextRepo->getRepoName().' but '.self::getPackageList($higherRepoPackages, $isVerbose).' from '.reset($higherRepoPackages)->getRepository()->getRepoName().' has higher repository priority. The packages with higher priority do not match your constraint and are therefore not installable. See https://getcomposer.org/repoprio for details and assistance.');
  249. }
  250. return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages, $isVerbose).' but '.(self::hasMultipleNames($packages) ? 'these do' : 'it does').' not match the constraint.');
  251. }
  252. if (!preg_match('{^[A-Za-z0-9_./-]+$}', $packageName)) {
  253. $illegalChars = preg_replace('{[A-Za-z0-9_./-]+}', '', $packageName);
  254. return array("- Root composer.json requires $packageName, it ", 'could not be found, it looks like its name is invalid, "'.$illegalChars.'" is not allowed in package names.');
  255. }
  256. if ($providers = $repositorySet->getProviders($packageName)) {
  257. $maxProviders = 20;
  258. $providersStr = implode(array_map(function ($p) {
  259. $description = $p['description'] ? ' '.substr($p['description'], 0, 100) : '';
  260. return " - ${p['name']}".$description."\n";
  261. }, count($providers) > $maxProviders+1 ? array_slice($providers, 0, $maxProviders) : $providers));
  262. if (count($providers) > $maxProviders+1) {
  263. $providersStr .= ' ... and '.(count($providers)-$maxProviders).' more.'."\n";
  264. }
  265. return array("- Root composer.json requires $packageName".self::constraintToText($constraint).", it ", "could not be found in any version, but the following packages provide it:\n".$providersStr." Consider requiring one of these to satisfy the $packageName requirement.");
  266. }
  267. return array("- Root composer.json requires $packageName, it ", "could not be found in any version, there may be a typo in the package name.");
  268. }
  269. /**
  270. * @internal
  271. */
  272. public static function getPackageList(array $packages, $isVerbose)
  273. {
  274. $prepared = array();
  275. foreach ($packages as $package) {
  276. $prepared[$package->getName()]['name'] = $package->getPrettyName();
  277. $prepared[$package->getName()]['versions'][$package->getVersion()] = $package->getPrettyVersion().($package instanceof AliasPackage ? ' (alias of '.$package->getAliasOf()->getPrettyVersion().')' : '');
  278. }
  279. foreach ($prepared as $name => $package) {
  280. // remove the implicit dev-master alias to avoid cruft in the display
  281. if (isset($package['versions'][VersionParser::DEV_MASTER_ALIAS]) && isset($package['versions']['dev-master'])) {
  282. unset($package['versions'][VersionParser::DEV_MASTER_ALIAS]);
  283. }
  284. uksort($package['versions'], 'version_compare');
  285. if (!$isVerbose) {
  286. $package['versions'] = self::condenseVersionList($package['versions'], 4);
  287. }
  288. $prepared[$name] = $package['name'].'['.implode(', ', $package['versions']).']';
  289. }
  290. return implode(', ', $prepared);
  291. }
  292. /**
  293. * @param string[] $versions an array of pretty versions, with normalized versions as keys
  294. * @return list<string> a list of pretty versions and '...' where versions were removed
  295. */
  296. private static function condenseVersionList(array $versions, $max)
  297. {
  298. if (count($versions) <= $max) {
  299. return $versions;
  300. }
  301. $filtered = array();
  302. $byMajor = array();
  303. foreach ($versions as $version => $pretty) {
  304. $byMajor[preg_replace('{^(\d+)\..*}', '$1', $version)][] = $pretty;
  305. }
  306. foreach ($byMajor as $versionsForMajor) {
  307. if (count($versionsForMajor) > $max) {
  308. $filtered[] = $versionsForMajor[0];
  309. $filtered[] = '...';
  310. $filtered[] = $versionsForMajor[count($versionsForMajor) - 1];
  311. } else {
  312. $filtered = array_merge($filtered, $versionsForMajor);
  313. }
  314. }
  315. return $filtered;
  316. }
  317. private static function hasMultipleNames(array $packages)
  318. {
  319. $name = null;
  320. foreach ($packages as $package) {
  321. if ($name === null || $name === $package->getName()) {
  322. $name = $package->getName();
  323. } else {
  324. return true;
  325. }
  326. }
  327. return false;
  328. }
  329. /**
  330. * Turns a constraint into text usable in a sentence describing a request
  331. *
  332. * @param \Composer\Semver\Constraint\ConstraintInterface $constraint
  333. * @return string
  334. */
  335. protected static function constraintToText($constraint)
  336. {
  337. return $constraint ? ' '.$constraint->getPrettyString() : '';
  338. }
  339. }