MultiConstraint.php 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. <?php
  2. /*
  3. * This file is part of composer/semver.
  4. *
  5. * (c) Composer <https://github.com/composer>
  6. *
  7. * For the full copyright and license information, please view
  8. * the LICENSE file that was distributed with this source code.
  9. */
  10. namespace Composer\Semver\Constraint;
  11. /**
  12. * Defines a conjunctive or disjunctive set of constraints.
  13. */
  14. class MultiConstraint implements ConstraintInterface
  15. {
  16. /**
  17. * @var ConstraintInterface[]
  18. * @phpstan-var non-empty-array<ConstraintInterface>
  19. */
  20. protected $constraints;
  21. /** @var string|null */
  22. protected $prettyString;
  23. /** @var string|null */
  24. protected $string;
  25. /** @var bool */
  26. protected $conjunctive;
  27. /** @var Bound|null */
  28. protected $lowerBound;
  29. /** @var Bound|null */
  30. protected $upperBound;
  31. /**
  32. * @param ConstraintInterface[] $constraints A set of constraints
  33. * @param bool $conjunctive Whether the constraints should be treated as conjunctive or disjunctive
  34. *
  35. * @throws \InvalidArgumentException If less than 2 constraints are passed
  36. */
  37. public function __construct(array $constraints, $conjunctive = true)
  38. {
  39. if (\count($constraints) < 2) {
  40. throw new \InvalidArgumentException(
  41. 'Must provide at least two constraints for a MultiConstraint. Use '.
  42. 'the regular Constraint class for one constraint only or MatchAllConstraint for none. You may use '.
  43. 'MultiConstraint::create() which optimizes and handles those cases automatically.'
  44. );
  45. }
  46. $this->constraints = $constraints;
  47. $this->conjunctive = $conjunctive;
  48. }
  49. /**
  50. * @return ConstraintInterface[]
  51. */
  52. public function getConstraints()
  53. {
  54. return $this->constraints;
  55. }
  56. /**
  57. * @return bool
  58. */
  59. public function isConjunctive()
  60. {
  61. return $this->conjunctive;
  62. }
  63. /**
  64. * @return bool
  65. */
  66. public function isDisjunctive()
  67. {
  68. return !$this->conjunctive;
  69. }
  70. /**
  71. * {@inheritDoc}
  72. */
  73. public function compile($otherOperator)
  74. {
  75. $parts = array();
  76. foreach ($this->constraints as $constraint) {
  77. $code = $constraint->compile($otherOperator);
  78. if ($code === 'true') {
  79. if (!$this->conjunctive) {
  80. return 'true';
  81. }
  82. } elseif ($code === 'false') {
  83. if ($this->conjunctive) {
  84. return 'false';
  85. }
  86. } else {
  87. $parts[] = '('.$code.')';
  88. }
  89. }
  90. if (!$parts) {
  91. return $this->conjunctive ? 'true' : 'false';
  92. }
  93. return $this->conjunctive ? implode('&&', $parts) : implode('||', $parts);
  94. }
  95. /**
  96. * @param ConstraintInterface $provider
  97. *
  98. * @return bool
  99. */
  100. public function matches(ConstraintInterface $provider)
  101. {
  102. if (false === $this->conjunctive) {
  103. foreach ($this->constraints as $constraint) {
  104. if ($provider->matches($constraint)) {
  105. return true;
  106. }
  107. }
  108. return false;
  109. }
  110. // when matching a conjunctive and a disjunctive multi constraint we have to iterate over the disjunctive one
  111. // otherwise we'd return true if different parts of the disjunctive constraint match the conjunctive one
  112. // which would lead to incorrect results, e.g. [>1 and <2] would match [<1 or >2] although they do not intersect
  113. if ($provider instanceof MultiConstraint && $provider->isDisjunctive()) {
  114. return $provider->matches($this);
  115. }
  116. foreach ($this->constraints as $constraint) {
  117. if (!$provider->matches($constraint)) {
  118. return false;
  119. }
  120. }
  121. return true;
  122. }
  123. /**
  124. * {@inheritDoc}
  125. */
  126. public function setPrettyString($prettyString)
  127. {
  128. $this->prettyString = $prettyString;
  129. }
  130. /**
  131. * {@inheritDoc}
  132. */
  133. public function getPrettyString()
  134. {
  135. if ($this->prettyString) {
  136. return $this->prettyString;
  137. }
  138. return (string) $this;
  139. }
  140. /**
  141. * {@inheritDoc}
  142. */
  143. public function __toString()
  144. {
  145. if ($this->string !== null) {
  146. return $this->string;
  147. }
  148. $constraints = array();
  149. foreach ($this->constraints as $constraint) {
  150. $constraints[] = (string) $constraint;
  151. }
  152. return $this->string = '[' . implode($this->conjunctive ? ' ' : ' || ', $constraints) . ']';
  153. }
  154. /**
  155. * {@inheritDoc}
  156. */
  157. public function getLowerBound()
  158. {
  159. $this->extractBounds();
  160. if (null === $this->lowerBound) {
  161. throw new \LogicException('extractBounds should have populated the lowerBound property');
  162. }
  163. return $this->lowerBound;
  164. }
  165. /**
  166. * {@inheritDoc}
  167. */
  168. public function getUpperBound()
  169. {
  170. $this->extractBounds();
  171. if (null === $this->upperBound) {
  172. throw new \LogicException('extractBounds should have populated the upperBound property');
  173. }
  174. return $this->upperBound;
  175. }
  176. /**
  177. * Tries to optimize the constraints as much as possible, meaning
  178. * reducing/collapsing congruent constraints etc.
  179. * Does not necessarily return a MultiConstraint instance if
  180. * things can be reduced to a simple constraint
  181. *
  182. * @param ConstraintInterface[] $constraints A set of constraints
  183. * @param bool $conjunctive Whether the constraints should be treated as conjunctive or disjunctive
  184. *
  185. * @return ConstraintInterface
  186. */
  187. public static function create(array $constraints, $conjunctive = true)
  188. {
  189. if (0 === \count($constraints)) {
  190. return new MatchAllConstraint();
  191. }
  192. if (1 === \count($constraints)) {
  193. return $constraints[0];
  194. }
  195. $optimized = self::optimizeConstraints($constraints, $conjunctive);
  196. if ($optimized !== null) {
  197. list($constraints, $conjunctive) = $optimized;
  198. if (\count($constraints) === 1) {
  199. return $constraints[0];
  200. }
  201. }
  202. return new self($constraints, $conjunctive);
  203. }
  204. /**
  205. * @param ConstraintInterface[] $constraints
  206. * @param bool $conjunctive
  207. * @return ?array
  208. *
  209. * @phpstan-return array{0: list<ConstraintInterface>, 1: bool}|null
  210. */
  211. private static function optimizeConstraints(array $constraints, $conjunctive)
  212. {
  213. // parse the two OR groups and if they are contiguous we collapse
  214. // them into one constraint
  215. // [>= 1 < 2] || [>= 2 < 3] || [>= 3 < 4] => [>= 1 < 4]
  216. if (!$conjunctive) {
  217. $left = $constraints[0];
  218. $mergedConstraints = array();
  219. $optimized = false;
  220. for ($i = 1, $l = \count($constraints); $i < $l; $i++) {
  221. $right = $constraints[$i];
  222. if (
  223. $left instanceof self
  224. && $left->conjunctive
  225. && $right instanceof self
  226. && $right->conjunctive
  227. && \count($left->constraints) === 2
  228. && \count($right->constraints) === 2
  229. && ($left0 = (string) $left->constraints[0])
  230. && $left0[0] === '>' && $left0[1] === '='
  231. && ($left1 = (string) $left->constraints[1])
  232. && $left1[0] === '<'
  233. && ($right0 = (string) $right->constraints[0])
  234. && $right0[0] === '>' && $right0[1] === '='
  235. && ($right1 = (string) $right->constraints[1])
  236. && $right1[0] === '<'
  237. && substr($left1, 2) === substr($right0, 3)
  238. ) {
  239. $optimized = true;
  240. $left = new MultiConstraint(
  241. array(
  242. $left->constraints[0],
  243. $right->constraints[1],
  244. ),
  245. true);
  246. } else {
  247. $mergedConstraints[] = $left;
  248. $left = $right;
  249. }
  250. }
  251. if ($optimized) {
  252. $mergedConstraints[] = $left;
  253. return array($mergedConstraints, false);
  254. }
  255. }
  256. // TODO: Here's the place to put more optimizations
  257. return null;
  258. }
  259. /**
  260. * @return void
  261. */
  262. private function extractBounds()
  263. {
  264. if (null !== $this->lowerBound) {
  265. return;
  266. }
  267. foreach ($this->constraints as $constraint) {
  268. if (null === $this->lowerBound || null === $this->upperBound) {
  269. $this->lowerBound = $constraint->getLowerBound();
  270. $this->upperBound = $constraint->getUpperBound();
  271. continue;
  272. }
  273. if ($constraint->getLowerBound()->compareTo($this->lowerBound, $this->isConjunctive() ? '>' : '<')) {
  274. $this->lowerBound = $constraint->getLowerBound();
  275. }
  276. if ($constraint->getUpperBound()->compareTo($this->upperBound, $this->isConjunctive() ? '<' : '>')) {
  277. $this->upperBound = $constraint->getUpperBound();
  278. }
  279. }
  280. }
  281. }