AbstractCollection.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365
  1. <?php
  2. /**
  3. * This file is part of the ramsey/collection library
  4. *
  5. * For the full copyright and license information, please view the LICENSE
  6. * file that was distributed with this source code.
  7. *
  8. * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com>
  9. * @license http://opensource.org/licenses/MIT MIT
  10. */
  11. declare(strict_types=1);
  12. namespace Ramsey\Collection;
  13. use Closure;
  14. use Ramsey\Collection\Exception\CollectionMismatchException;
  15. use Ramsey\Collection\Exception\InvalidArgumentException;
  16. use Ramsey\Collection\Exception\InvalidPropertyOrMethod;
  17. use Ramsey\Collection\Exception\NoSuchElementException;
  18. use Ramsey\Collection\Exception\UnsupportedOperationException;
  19. use Ramsey\Collection\Tool\TypeTrait;
  20. use Ramsey\Collection\Tool\ValueExtractorTrait;
  21. use Ramsey\Collection\Tool\ValueToStringTrait;
  22. use function array_filter;
  23. use function array_key_first;
  24. use function array_key_last;
  25. use function array_map;
  26. use function array_merge;
  27. use function array_reduce;
  28. use function array_search;
  29. use function array_udiff;
  30. use function array_uintersect;
  31. use function in_array;
  32. use function is_int;
  33. use function is_object;
  34. use function spl_object_id;
  35. use function sprintf;
  36. use function usort;
  37. /**
  38. * This class provides a basic implementation of `CollectionInterface`, to
  39. * minimize the effort required to implement this interface
  40. *
  41. * @template T
  42. * @extends AbstractArray<T>
  43. * @implements CollectionInterface<T>
  44. */
  45. abstract class AbstractCollection extends AbstractArray implements CollectionInterface
  46. {
  47. use TypeTrait;
  48. use ValueToStringTrait;
  49. use ValueExtractorTrait;
  50. /**
  51. * @throws InvalidArgumentException if $element is of the wrong type.
  52. */
  53. public function add(mixed $element): bool
  54. {
  55. $this[] = $element;
  56. return true;
  57. }
  58. public function contains(mixed $element, bool $strict = true): bool
  59. {
  60. return in_array($element, $this->data, $strict);
  61. }
  62. /**
  63. * @throws InvalidArgumentException if $element is of the wrong type.
  64. */
  65. public function offsetSet(mixed $offset, mixed $value): void
  66. {
  67. if ($this->checkType($this->getType(), $value) === false) {
  68. throw new InvalidArgumentException(
  69. 'Value must be of type ' . $this->getType() . '; value is '
  70. . $this->toolValueToString($value),
  71. );
  72. }
  73. if ($offset === null) {
  74. $this->data[] = $value;
  75. } else {
  76. $this->data[$offset] = $value;
  77. }
  78. }
  79. public function remove(mixed $element): bool
  80. {
  81. if (($position = array_search($element, $this->data, true)) !== false) {
  82. unset($this[$position]);
  83. return true;
  84. }
  85. return false;
  86. }
  87. /**
  88. * @throws InvalidPropertyOrMethod if the $propertyOrMethod does not exist
  89. * on the elements in this collection.
  90. * @throws UnsupportedOperationException if unable to call column() on this
  91. * collection.
  92. *
  93. * @inheritDoc
  94. */
  95. public function column(string $propertyOrMethod): array
  96. {
  97. $temp = [];
  98. foreach ($this->data as $item) {
  99. $temp[] = $this->extractValue($item, $propertyOrMethod);
  100. }
  101. return $temp;
  102. }
  103. /**
  104. * @return T
  105. *
  106. * @throws NoSuchElementException if this collection is empty.
  107. */
  108. public function first(): mixed
  109. {
  110. $firstIndex = array_key_first($this->data);
  111. if ($firstIndex === null) {
  112. throw new NoSuchElementException('Can\'t determine first item. Collection is empty');
  113. }
  114. return $this->data[$firstIndex];
  115. }
  116. /**
  117. * @return T
  118. *
  119. * @throws NoSuchElementException if this collection is empty.
  120. */
  121. public function last(): mixed
  122. {
  123. $lastIndex = array_key_last($this->data);
  124. if ($lastIndex === null) {
  125. throw new NoSuchElementException('Can\'t determine last item. Collection is empty');
  126. }
  127. return $this->data[$lastIndex];
  128. }
  129. /**
  130. * @return CollectionInterface<T>
  131. *
  132. * @throws InvalidPropertyOrMethod if the $propertyOrMethod does not exist
  133. * on the elements in this collection.
  134. * @throws UnsupportedOperationException if unable to call sort() on this
  135. * collection.
  136. */
  137. public function sort(?string $propertyOrMethod = null, Sort $order = Sort::Ascending): CollectionInterface
  138. {
  139. $collection = clone $this;
  140. usort(
  141. $collection->data,
  142. function (mixed $a, mixed $b) use ($propertyOrMethod, $order): int {
  143. $aValue = $this->extractValue($a, $propertyOrMethod);
  144. $bValue = $this->extractValue($b, $propertyOrMethod);
  145. return ($aValue <=> $bValue) * ($order === Sort::Descending ? -1 : 1);
  146. },
  147. );
  148. return $collection;
  149. }
  150. /**
  151. * @param callable(T): bool $callback A callable to use for filtering elements.
  152. *
  153. * @return CollectionInterface<T>
  154. */
  155. public function filter(callable $callback): CollectionInterface
  156. {
  157. $collection = clone $this;
  158. $collection->data = array_merge([], array_filter($collection->data, $callback));
  159. return $collection;
  160. }
  161. /**
  162. * @return CollectionInterface<T>
  163. *
  164. * @throws InvalidPropertyOrMethod if the $propertyOrMethod does not exist
  165. * on the elements in this collection.
  166. * @throws UnsupportedOperationException if unable to call where() on this
  167. * collection.
  168. */
  169. public function where(?string $propertyOrMethod, mixed $value): CollectionInterface
  170. {
  171. return $this->filter(
  172. fn (mixed $item): bool => $this->extractValue($item, $propertyOrMethod) === $value,
  173. );
  174. }
  175. /**
  176. * @param callable(T): TCallbackReturn $callback A callable to apply to each
  177. * item of the collection.
  178. *
  179. * @return CollectionInterface<TCallbackReturn>
  180. *
  181. * @template TCallbackReturn
  182. */
  183. public function map(callable $callback): CollectionInterface
  184. {
  185. return new Collection('mixed', array_map($callback, $this->data));
  186. }
  187. /**
  188. * @param callable(TCarry, T): TCarry $callback A callable to apply to each
  189. * item of the collection to reduce it to a single value.
  190. * @param TCarry $initial This is the initial value provided to the callback.
  191. *
  192. * @return TCarry
  193. *
  194. * @template TCarry
  195. */
  196. public function reduce(callable $callback, mixed $initial): mixed
  197. {
  198. return array_reduce($this->data, $callback, $initial);
  199. }
  200. /**
  201. * @param CollectionInterface<T> $other The collection to check for divergent
  202. * items.
  203. *
  204. * @return CollectionInterface<T>
  205. *
  206. * @throws CollectionMismatchException if the compared collections are of
  207. * differing types.
  208. */
  209. public function diff(CollectionInterface $other): CollectionInterface
  210. {
  211. $this->compareCollectionTypes($other);
  212. $diffAtoB = array_udiff($this->data, $other->toArray(), $this->getComparator());
  213. $diffBtoA = array_udiff($other->toArray(), $this->data, $this->getComparator());
  214. $collection = clone $this;
  215. $collection->data = array_merge($diffAtoB, $diffBtoA);
  216. return $collection;
  217. }
  218. /**
  219. * @param CollectionInterface<T> $other The collection to check for
  220. * intersecting items.
  221. *
  222. * @return CollectionInterface<T>
  223. *
  224. * @throws CollectionMismatchException if the compared collections are of
  225. * differing types.
  226. */
  227. public function intersect(CollectionInterface $other): CollectionInterface
  228. {
  229. $this->compareCollectionTypes($other);
  230. $collection = clone $this;
  231. $collection->data = array_uintersect($this->data, $other->toArray(), $this->getComparator());
  232. return $collection;
  233. }
  234. /**
  235. * @param CollectionInterface<T> ...$collections The collections to merge.
  236. *
  237. * @return CollectionInterface<T>
  238. *
  239. * @throws CollectionMismatchException if unable to merge any of the given
  240. * collections or items within the given collections due to type
  241. * mismatch errors.
  242. */
  243. public function merge(CollectionInterface ...$collections): CollectionInterface
  244. {
  245. $mergedCollection = clone $this;
  246. foreach ($collections as $index => $collection) {
  247. if (!$collection instanceof static) {
  248. throw new CollectionMismatchException(
  249. sprintf('Collection with index %d must be of type %s', $index, static::class),
  250. );
  251. }
  252. // When using generics (Collection.php, Set.php, etc),
  253. // we also need to make sure that the internal types match each other
  254. if ($this->getUniformType($collection) !== $this->getUniformType($this)) {
  255. throw new CollectionMismatchException(
  256. sprintf(
  257. 'Collection items in collection with index %d must be of type %s',
  258. $index,
  259. $this->getType(),
  260. ),
  261. );
  262. }
  263. foreach ($collection as $key => $value) {
  264. if (is_int($key)) {
  265. $mergedCollection[] = $value;
  266. } else {
  267. $mergedCollection[$key] = $value;
  268. }
  269. }
  270. }
  271. return $mergedCollection;
  272. }
  273. /**
  274. * @param CollectionInterface<T> $other
  275. *
  276. * @throws CollectionMismatchException
  277. */
  278. private function compareCollectionTypes(CollectionInterface $other): void
  279. {
  280. if (!$other instanceof static) {
  281. throw new CollectionMismatchException('Collection must be of type ' . static::class);
  282. }
  283. // When using generics (Collection.php, Set.php, etc),
  284. // we also need to make sure that the internal types match each other
  285. if ($this->getUniformType($other) !== $this->getUniformType($this)) {
  286. throw new CollectionMismatchException('Collection items must be of type ' . $this->getType());
  287. }
  288. }
  289. private function getComparator(): Closure
  290. {
  291. return function (mixed $a, mixed $b): int {
  292. // If the two values are object, we convert them to unique scalars.
  293. // If the collection contains mixed values (unlikely) where some are objects
  294. // and some are not, we leave them as they are.
  295. // The comparator should still work and the result of $a < $b should
  296. // be consistent but unpredictable since not documented.
  297. if (is_object($a) && is_object($b)) {
  298. $a = spl_object_id($a);
  299. $b = spl_object_id($b);
  300. }
  301. return $a === $b ? 0 : ($a < $b ? 1 : -1);
  302. };
  303. }
  304. /**
  305. * @param CollectionInterface<mixed> $collection
  306. */
  307. private function getUniformType(CollectionInterface $collection): string
  308. {
  309. return match ($collection->getType()) {
  310. 'integer' => 'int',
  311. 'boolean' => 'bool',
  312. 'double' => 'float',
  313. default => $collection->getType(),
  314. };
  315. }
  316. }