CollectionTrait.php 28 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
  5. * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  6. *
  7. * Licensed under The MIT License
  8. * For full copyright and license information, please see the LICENSE.txt
  9. * Redistributions of files must retain the above copyright notice.
  10. *
  11. * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  12. * @link https://cakephp.org CakePHP(tm) Project
  13. * @since 3.0.0
  14. * @license https://opensource.org/licenses/mit-license.php MIT License
  15. */
  16. namespace Cake\Collection;
  17. use AppendIterator;
  18. use ArrayIterator;
  19. use Cake\Collection\Iterator\BufferedIterator;
  20. use Cake\Collection\Iterator\ExtractIterator;
  21. use Cake\Collection\Iterator\FilterIterator;
  22. use Cake\Collection\Iterator\InsertIterator;
  23. use Cake\Collection\Iterator\MapReduce;
  24. use Cake\Collection\Iterator\NestIterator;
  25. use Cake\Collection\Iterator\ReplaceIterator;
  26. use Cake\Collection\Iterator\SortIterator;
  27. use Cake\Collection\Iterator\StoppableIterator;
  28. use Cake\Collection\Iterator\TreeIterator;
  29. use Cake\Collection\Iterator\UnfoldIterator;
  30. use Cake\Collection\Iterator\ZipIterator;
  31. use Countable;
  32. use InvalidArgumentException;
  33. use LimitIterator;
  34. use LogicException;
  35. use OuterIterator;
  36. use RecursiveIteratorIterator;
  37. use RuntimeException;
  38. use Traversable;
  39. /**
  40. * Offers a handful of methods to manipulate iterators
  41. */
  42. trait CollectionTrait
  43. {
  44. use ExtractTrait;
  45. /**
  46. * Returns a new collection.
  47. *
  48. * Allows classes which use this trait to determine their own
  49. * type of returned collection interface
  50. *
  51. * @param mixed ...$args Constructor arguments.
  52. * @return \Cake\Collection\CollectionInterface
  53. */
  54. protected function newCollection(...$args): CollectionInterface
  55. {
  56. return new Collection(...$args);
  57. }
  58. /**
  59. * @inheritDoc
  60. */
  61. public function each(callable $callback)
  62. {
  63. foreach ($this->optimizeUnwrap() as $k => $v) {
  64. $callback($v, $k);
  65. }
  66. return $this;
  67. }
  68. /**
  69. * @inheritDoc
  70. */
  71. public function filter(?callable $callback = null): CollectionInterface
  72. {
  73. if ($callback === null) {
  74. $callback = function ($v) {
  75. return (bool)$v;
  76. };
  77. }
  78. return new FilterIterator($this->unwrap(), $callback);
  79. }
  80. /**
  81. * @inheritDoc
  82. */
  83. public function reject(callable $callback): CollectionInterface
  84. {
  85. return new FilterIterator($this->unwrap(), function ($key, $value, $items) use ($callback) {
  86. return !$callback($key, $value, $items);
  87. });
  88. }
  89. /**
  90. * @inheritDoc
  91. */
  92. public function every(callable $callback): bool
  93. {
  94. foreach ($this->optimizeUnwrap() as $key => $value) {
  95. if (!$callback($value, $key)) {
  96. return false;
  97. }
  98. }
  99. return true;
  100. }
  101. /**
  102. * @inheritDoc
  103. */
  104. public function some(callable $callback): bool
  105. {
  106. foreach ($this->optimizeUnwrap() as $key => $value) {
  107. if ($callback($value, $key) === true) {
  108. return true;
  109. }
  110. }
  111. return false;
  112. }
  113. /**
  114. * @inheritDoc
  115. */
  116. public function contains($value): bool
  117. {
  118. foreach ($this->optimizeUnwrap() as $v) {
  119. if ($value === $v) {
  120. return true;
  121. }
  122. }
  123. return false;
  124. }
  125. /**
  126. * @inheritDoc
  127. */
  128. public function map(callable $callback): CollectionInterface
  129. {
  130. return new ReplaceIterator($this->unwrap(), $callback);
  131. }
  132. /**
  133. * @inheritDoc
  134. */
  135. public function reduce(callable $callback, $initial = null)
  136. {
  137. $isFirst = false;
  138. if (func_num_args() < 2) {
  139. $isFirst = true;
  140. }
  141. $result = $initial;
  142. foreach ($this->optimizeUnwrap() as $k => $value) {
  143. if ($isFirst) {
  144. $result = $value;
  145. $isFirst = false;
  146. continue;
  147. }
  148. $result = $callback($result, $value, $k);
  149. }
  150. return $result;
  151. }
  152. /**
  153. * @inheritDoc
  154. */
  155. public function extract($path): CollectionInterface
  156. {
  157. $extractor = new ExtractIterator($this->unwrap(), $path);
  158. if (is_string($path) && strpos($path, '{*}') !== false) {
  159. $extractor = $extractor
  160. ->filter(function ($data) {
  161. return $data !== null && ($data instanceof Traversable || is_array($data));
  162. })
  163. ->unfold();
  164. }
  165. return $extractor;
  166. }
  167. /**
  168. * @inheritDoc
  169. */
  170. public function max($path, int $sort = \SORT_NUMERIC)
  171. {
  172. return (new SortIterator($this->unwrap(), $path, \SORT_DESC, $sort))->first();
  173. }
  174. /**
  175. * @inheritDoc
  176. */
  177. public function min($path, int $sort = \SORT_NUMERIC)
  178. {
  179. return (new SortIterator($this->unwrap(), $path, \SORT_ASC, $sort))->first();
  180. }
  181. /**
  182. * @inheritDoc
  183. */
  184. public function avg($path = null)
  185. {
  186. $result = $this;
  187. if ($path !== null) {
  188. $result = $result->extract($path);
  189. }
  190. $result = $result
  191. ->reduce(function ($acc, $current) {
  192. [$count, $sum] = $acc;
  193. return [$count + 1, $sum + $current];
  194. }, [0, 0]);
  195. if ($result[0] === 0) {
  196. return null;
  197. }
  198. return $result[1] / $result[0];
  199. }
  200. /**
  201. * @inheritDoc
  202. */
  203. public function median($path = null)
  204. {
  205. $items = $this;
  206. if ($path !== null) {
  207. $items = $items->extract($path);
  208. }
  209. $values = $items->toList();
  210. sort($values);
  211. $count = count($values);
  212. if ($count === 0) {
  213. return null;
  214. }
  215. $middle = (int)($count / 2);
  216. if ($count % 2) {
  217. return $values[$middle];
  218. }
  219. return ($values[$middle - 1] + $values[$middle]) / 2;
  220. }
  221. /**
  222. * @inheritDoc
  223. */
  224. public function sortBy($path, int $order = \SORT_DESC, int $sort = \SORT_NUMERIC): CollectionInterface
  225. {
  226. return new SortIterator($this->unwrap(), $path, $order, $sort);
  227. }
  228. /**
  229. * @inheritDoc
  230. */
  231. public function groupBy($path): CollectionInterface
  232. {
  233. $callback = $this->_propertyExtractor($path);
  234. $group = [];
  235. foreach ($this->optimizeUnwrap() as $value) {
  236. $pathValue = $callback($value);
  237. if ($pathValue === null) {
  238. throw new InvalidArgumentException(
  239. 'Cannot use a nonexistent path or null value. ' .
  240. 'Use a callback to provide default values if necessary.'
  241. );
  242. }
  243. $group[$pathValue][] = $value;
  244. }
  245. return $this->newCollection($group);
  246. }
  247. /**
  248. * @inheritDoc
  249. */
  250. public function indexBy($path): CollectionInterface
  251. {
  252. $callback = $this->_propertyExtractor($path);
  253. $group = [];
  254. foreach ($this->optimizeUnwrap() as $value) {
  255. $pathValue = $callback($value);
  256. if ($pathValue === null) {
  257. throw new InvalidArgumentException(
  258. 'Cannot use a nonexistent path or null value. ' .
  259. 'Use a callback to provide default values if necessary.'
  260. );
  261. }
  262. $group[$pathValue] = $value;
  263. }
  264. return $this->newCollection($group);
  265. }
  266. /**
  267. * @inheritDoc
  268. */
  269. public function countBy($path): CollectionInterface
  270. {
  271. $callback = $this->_propertyExtractor($path);
  272. $mapper = function ($value, $key, $mr) use ($callback): void {
  273. /** @var \Cake\Collection\Iterator\MapReduce $mr */
  274. $mr->emitIntermediate($value, $callback($value));
  275. };
  276. $reducer = function ($values, $key, $mr): void {
  277. /** @var \Cake\Collection\Iterator\MapReduce $mr */
  278. $mr->emit(count($values), $key);
  279. };
  280. return $this->newCollection(new MapReduce($this->unwrap(), $mapper, $reducer));
  281. }
  282. /**
  283. * @inheritDoc
  284. */
  285. public function sumOf($path = null)
  286. {
  287. if ($path === null) {
  288. return array_sum($this->toList());
  289. }
  290. $callback = $this->_propertyExtractor($path);
  291. $sum = 0;
  292. foreach ($this->optimizeUnwrap() as $k => $v) {
  293. $sum += $callback($v, $k);
  294. }
  295. return $sum;
  296. }
  297. /**
  298. * @inheritDoc
  299. */
  300. public function shuffle(): CollectionInterface
  301. {
  302. $items = $this->toList();
  303. shuffle($items);
  304. return $this->newCollection($items);
  305. }
  306. /**
  307. * @inheritDoc
  308. */
  309. public function sample(int $length = 10): CollectionInterface
  310. {
  311. return $this->newCollection(new LimitIterator($this->shuffle(), 0, $length));
  312. }
  313. /**
  314. * @inheritDoc
  315. */
  316. public function take(int $length = 1, int $offset = 0): CollectionInterface
  317. {
  318. return $this->newCollection(new LimitIterator($this, $offset, $length));
  319. }
  320. /**
  321. * @inheritDoc
  322. */
  323. public function skip(int $length): CollectionInterface
  324. {
  325. return $this->newCollection(new LimitIterator($this, $length));
  326. }
  327. /**
  328. * @inheritDoc
  329. */
  330. public function match(array $conditions): CollectionInterface
  331. {
  332. return $this->filter($this->_createMatcherFilter($conditions));
  333. }
  334. /**
  335. * @inheritDoc
  336. */
  337. public function firstMatch(array $conditions)
  338. {
  339. return $this->match($conditions)->first();
  340. }
  341. /**
  342. * @inheritDoc
  343. */
  344. public function first()
  345. {
  346. $iterator = new LimitIterator($this, 0, 1);
  347. foreach ($iterator as $result) {
  348. return $result;
  349. }
  350. }
  351. /**
  352. * @inheritDoc
  353. */
  354. public function last()
  355. {
  356. $iterator = $this->optimizeUnwrap();
  357. if (is_array($iterator)) {
  358. return array_pop($iterator);
  359. }
  360. if ($iterator instanceof Countable) {
  361. $count = count($iterator);
  362. if ($count === 0) {
  363. return null;
  364. }
  365. /** @var iterable $iterator */
  366. $iterator = new LimitIterator($iterator, $count - 1, 1);
  367. }
  368. $result = null;
  369. foreach ($iterator as $result) {
  370. // No-op
  371. }
  372. return $result;
  373. }
  374. /**
  375. * @inheritDoc
  376. */
  377. public function takeLast(int $length): CollectionInterface
  378. {
  379. if ($length < 1) {
  380. throw new InvalidArgumentException('The takeLast method requires a number greater than 0.');
  381. }
  382. $iterator = $this->optimizeUnwrap();
  383. if (is_array($iterator)) {
  384. return $this->newCollection(array_slice($iterator, $length * -1));
  385. }
  386. if ($iterator instanceof Countable) {
  387. $count = count($iterator);
  388. if ($count === 0) {
  389. return $this->newCollection([]);
  390. }
  391. $iterator = new LimitIterator($iterator, max(0, $count - $length), $length);
  392. return $this->newCollection($iterator);
  393. }
  394. $generator = function ($iterator, $length) {
  395. $result = [];
  396. $bucket = 0;
  397. $offset = 0;
  398. /**
  399. * Consider the collection of elements [1, 2, 3, 4, 5, 6, 7, 8, 9], in order
  400. * to get the last 4 elements, we can keep a buffer of 4 elements and
  401. * fill it circularly using modulo logic, we use the $bucket variable
  402. * to track the position to fill next in the buffer. This how the buffer
  403. * looks like after 4 iterations:
  404. *
  405. * 0) 1 2 3 4 -- $bucket now goes back to 0, we have filled 4 elementes
  406. * 1) 5 2 3 4 -- 5th iteration
  407. * 2) 5 6 3 4 -- 6th iteration
  408. * 3) 5 6 7 4 -- 7th iteration
  409. * 4) 5 6 7 8 -- 8th iteration
  410. * 5) 9 6 7 8
  411. *
  412. * We can see that at the end of the iterations, the buffer contains all
  413. * the last four elements, just in the wrong order. How do we keep the
  414. * original order? Well, it turns out that the number of iteration also
  415. * give us a clue on what's going on, Let's add a marker for it now:
  416. *
  417. * 0) 1 2 3 4
  418. * ^ -- The 0) above now becomes the $offset variable
  419. * 1) 5 2 3 4
  420. * ^ -- $offset = 1
  421. * 2) 5 6 3 4
  422. * ^ -- $offset = 2
  423. * 3) 5 6 7 4
  424. * ^ -- $offset = 3
  425. * 4) 5 6 7 8
  426. * ^ -- We use module logic for $offset too
  427. * and as you can see each time $offset is 0, then the buffer
  428. * is sorted exactly as we need.
  429. * 5) 9 6 7 8
  430. * ^ -- $offset = 1
  431. *
  432. * The $offset variable is a marker for splitting the buffer in two,
  433. * elements to the right for the marker are the head of the final result,
  434. * whereas the elements at the left are the tail. For example consider step 5)
  435. * which has an offset of 1:
  436. *
  437. * - $head = elements to the right = [6, 7, 8]
  438. * - $tail = elements to the left = [9]
  439. * - $result = $head + $tail = [6, 7, 8, 9]
  440. *
  441. * The logic above applies to collections of any size.
  442. */
  443. foreach ($iterator as $k => $item) {
  444. $result[$bucket] = [$k, $item];
  445. $bucket = (++$bucket) % $length;
  446. $offset++;
  447. }
  448. $offset = $offset % $length;
  449. $head = array_slice($result, $offset);
  450. $tail = array_slice($result, 0, $offset);
  451. foreach ($head as $v) {
  452. yield $v[0] => $v[1];
  453. }
  454. foreach ($tail as $v) {
  455. yield $v[0] => $v[1];
  456. }
  457. };
  458. return $this->newCollection($generator($iterator, $length));
  459. }
  460. /**
  461. * @inheritDoc
  462. */
  463. public function append($items): CollectionInterface
  464. {
  465. $list = new AppendIterator();
  466. $list->append($this->unwrap());
  467. $list->append($this->newCollection($items)->unwrap());
  468. return $this->newCollection($list);
  469. }
  470. /**
  471. * @inheritDoc
  472. */
  473. public function appendItem($item, $key = null): CollectionInterface
  474. {
  475. if ($key !== null) {
  476. $data = [$key => $item];
  477. } else {
  478. $data = [$item];
  479. }
  480. return $this->append($data);
  481. }
  482. /**
  483. * @inheritDoc
  484. */
  485. public function prepend($items): CollectionInterface
  486. {
  487. return $this->newCollection($items)->append($this);
  488. }
  489. /**
  490. * @inheritDoc
  491. */
  492. public function prependItem($item, $key = null): CollectionInterface
  493. {
  494. if ($key !== null) {
  495. $data = [$key => $item];
  496. } else {
  497. $data = [$item];
  498. }
  499. return $this->prepend($data);
  500. }
  501. /**
  502. * @inheritDoc
  503. */
  504. public function combine($keyPath, $valuePath, $groupPath = null): CollectionInterface
  505. {
  506. $options = [
  507. 'keyPath' => $this->_propertyExtractor($keyPath),
  508. 'valuePath' => $this->_propertyExtractor($valuePath),
  509. 'groupPath' => $groupPath ? $this->_propertyExtractor($groupPath) : null,
  510. ];
  511. $mapper = function ($value, $key, MapReduce $mapReduce) use ($options) {
  512. $rowKey = $options['keyPath'];
  513. $rowVal = $options['valuePath'];
  514. if (!$options['groupPath']) {
  515. $mapReduce->emit($rowVal($value, $key), $rowKey($value, $key));
  516. return null;
  517. }
  518. $key = $options['groupPath']($value, $key);
  519. $mapReduce->emitIntermediate(
  520. [$rowKey($value, $key) => $rowVal($value, $key)],
  521. $key
  522. );
  523. };
  524. $reducer = function ($values, $key, MapReduce $mapReduce): void {
  525. $result = [];
  526. foreach ($values as $value) {
  527. $result += $value;
  528. }
  529. $mapReduce->emit($result, $key);
  530. };
  531. return $this->newCollection(new MapReduce($this->unwrap(), $mapper, $reducer));
  532. }
  533. /**
  534. * @inheritDoc
  535. */
  536. public function nest($idPath, $parentPath, string $nestingKey = 'children'): CollectionInterface
  537. {
  538. $parents = [];
  539. $idPath = $this->_propertyExtractor($idPath);
  540. $parentPath = $this->_propertyExtractor($parentPath);
  541. $isObject = true;
  542. $mapper = function ($row, $key, MapReduce $mapReduce) use (&$parents, $idPath, $parentPath, $nestingKey): void {
  543. $row[$nestingKey] = [];
  544. $id = $idPath($row, $key);
  545. $parentId = $parentPath($row, $key);
  546. $parents[$id] = &$row;
  547. $mapReduce->emitIntermediate($id, $parentId);
  548. };
  549. $reducer = function ($values, $key, MapReduce $mapReduce) use (&$parents, &$isObject, $nestingKey) {
  550. static $foundOutType = false;
  551. if (!$foundOutType) {
  552. $isObject = is_object(current($parents));
  553. $foundOutType = true;
  554. }
  555. if (empty($key) || !isset($parents[$key])) {
  556. foreach ($values as $id) {
  557. /** @psalm-suppress PossiblyInvalidArgument */
  558. $parents[$id] = $isObject ? $parents[$id] : new ArrayIterator($parents[$id], 1);
  559. $mapReduce->emit($parents[$id]);
  560. }
  561. return null;
  562. }
  563. $children = [];
  564. foreach ($values as $id) {
  565. $children[] = &$parents[$id];
  566. }
  567. $parents[$key][$nestingKey] = $children;
  568. };
  569. return $this->newCollection(new MapReduce($this->unwrap(), $mapper, $reducer))
  570. ->map(function ($value) use (&$isObject) {
  571. /** @var \ArrayIterator $value */
  572. return $isObject ? $value : $value->getArrayCopy();
  573. });
  574. }
  575. /**
  576. * @inheritDoc
  577. */
  578. public function insert(string $path, $values): CollectionInterface
  579. {
  580. return new InsertIterator($this->unwrap(), $path, $values);
  581. }
  582. /**
  583. * @inheritDoc
  584. */
  585. public function toArray(bool $preserveKeys = true): array
  586. {
  587. $iterator = $this->unwrap();
  588. if ($iterator instanceof ArrayIterator) {
  589. $items = $iterator->getArrayCopy();
  590. return $preserveKeys ? $items : array_values($items);
  591. }
  592. // RecursiveIteratorIterator can return duplicate key values causing
  593. // data loss when converted into an array
  594. if ($preserveKeys && get_class($iterator) === RecursiveIteratorIterator::class) {
  595. $preserveKeys = false;
  596. }
  597. return iterator_to_array($this, $preserveKeys);
  598. }
  599. /**
  600. * @inheritDoc
  601. */
  602. public function toList(): array
  603. {
  604. return $this->toArray(false);
  605. }
  606. /**
  607. * @inheritDoc
  608. */
  609. public function jsonSerialize(): array
  610. {
  611. return $this->toArray();
  612. }
  613. /**
  614. * @inheritDoc
  615. */
  616. public function compile(bool $preserveKeys = true): CollectionInterface
  617. {
  618. return $this->newCollection($this->toArray($preserveKeys));
  619. }
  620. /**
  621. * @inheritDoc
  622. */
  623. public function lazy(): CollectionInterface
  624. {
  625. $generator = function () {
  626. foreach ($this->unwrap() as $k => $v) {
  627. yield $k => $v;
  628. }
  629. };
  630. return $this->newCollection($generator());
  631. }
  632. /**
  633. * @inheritDoc
  634. */
  635. public function buffered(): CollectionInterface
  636. {
  637. return new BufferedIterator($this->unwrap());
  638. }
  639. /**
  640. * @inheritDoc
  641. */
  642. public function listNested($order = 'desc', $nestingKey = 'children'): CollectionInterface
  643. {
  644. if (is_string($order)) {
  645. $order = strtolower($order);
  646. $modes = [
  647. 'desc' => RecursiveIteratorIterator::SELF_FIRST,
  648. 'asc' => RecursiveIteratorIterator::CHILD_FIRST,
  649. 'leaves' => RecursiveIteratorIterator::LEAVES_ONLY,
  650. ];
  651. if (!isset($modes[$order])) {
  652. throw new RuntimeException(sprintf(
  653. "Invalid direction `%s` provided. Must be one of: 'desc', 'asc', 'leaves'",
  654. $order
  655. ));
  656. }
  657. $order = $modes[$order];
  658. }
  659. return new TreeIterator(
  660. new NestIterator($this, $nestingKey),
  661. $order
  662. );
  663. }
  664. /**
  665. * @inheritDoc
  666. */
  667. public function stopWhen($condition): CollectionInterface
  668. {
  669. if (!is_callable($condition)) {
  670. $condition = $this->_createMatcherFilter($condition);
  671. }
  672. return new StoppableIterator($this->unwrap(), $condition);
  673. }
  674. /**
  675. * @inheritDoc
  676. */
  677. public function unfold(?callable $callback = null): CollectionInterface
  678. {
  679. if ($callback === null) {
  680. $callback = function ($item) {
  681. return $item;
  682. };
  683. }
  684. return $this->newCollection(
  685. new RecursiveIteratorIterator(
  686. new UnfoldIterator($this->unwrap(), $callback),
  687. RecursiveIteratorIterator::LEAVES_ONLY
  688. )
  689. );
  690. }
  691. /**
  692. * @inheritDoc
  693. */
  694. public function through(callable $callback): CollectionInterface
  695. {
  696. $result = $callback($this);
  697. return $result instanceof CollectionInterface ? $result : $this->newCollection($result);
  698. }
  699. /**
  700. * @inheritDoc
  701. */
  702. public function zip(iterable $items): CollectionInterface
  703. {
  704. return new ZipIterator(array_merge([$this->unwrap()], func_get_args()));
  705. }
  706. /**
  707. * @inheritDoc
  708. */
  709. public function zipWith(iterable $items, $callback): CollectionInterface
  710. {
  711. if (func_num_args() > 2) {
  712. $items = func_get_args();
  713. $callback = array_pop($items);
  714. } else {
  715. $items = [$items];
  716. }
  717. return new ZipIterator(array_merge([$this->unwrap()], $items), $callback);
  718. }
  719. /**
  720. * @inheritDoc
  721. */
  722. public function chunk(int $chunkSize): CollectionInterface
  723. {
  724. return $this->map(function ($v, $k, $iterator) use ($chunkSize) {
  725. $values = [$v];
  726. for ($i = 1; $i < $chunkSize; $i++) {
  727. $iterator->next();
  728. if (!$iterator->valid()) {
  729. break;
  730. }
  731. $values[] = $iterator->current();
  732. }
  733. return $values;
  734. });
  735. }
  736. /**
  737. * @inheritDoc
  738. */
  739. public function chunkWithKeys(int $chunkSize, bool $preserveKeys = true): CollectionInterface
  740. {
  741. return $this->map(function ($v, $k, $iterator) use ($chunkSize, $preserveKeys) {
  742. $key = 0;
  743. if ($preserveKeys) {
  744. $key = $k;
  745. }
  746. $values = [$key => $v];
  747. for ($i = 1; $i < $chunkSize; $i++) {
  748. $iterator->next();
  749. if (!$iterator->valid()) {
  750. break;
  751. }
  752. if ($preserveKeys) {
  753. $values[$iterator->key()] = $iterator->current();
  754. } else {
  755. $values[] = $iterator->current();
  756. }
  757. }
  758. return $values;
  759. });
  760. }
  761. /**
  762. * @inheritDoc
  763. */
  764. public function isEmpty(): bool
  765. {
  766. foreach ($this as $el) {
  767. return false;
  768. }
  769. return true;
  770. }
  771. /**
  772. * @inheritDoc
  773. */
  774. public function unwrap(): Traversable
  775. {
  776. $iterator = $this;
  777. while (
  778. get_class($iterator) === Collection::class
  779. && $iterator instanceof OuterIterator
  780. ) {
  781. $iterator = $iterator->getInnerIterator();
  782. }
  783. if ($iterator !== $this && $iterator instanceof CollectionInterface) {
  784. $iterator = $iterator->unwrap();
  785. }
  786. return $iterator;
  787. }
  788. /**
  789. * {@inheritDoc}
  790. *
  791. * @param callable|null $operation A callable that allows you to customize the product result.
  792. * @param callable|null $filter A filtering callback that must return true for a result to be part
  793. * of the final results.
  794. * @return \Cake\Collection\CollectionInterface
  795. * @throws \LogicException
  796. */
  797. public function cartesianProduct(?callable $operation = null, ?callable $filter = null): CollectionInterface
  798. {
  799. if ($this->isEmpty()) {
  800. return $this->newCollection([]);
  801. }
  802. $collectionArrays = [];
  803. $collectionArraysKeys = [];
  804. $collectionArraysCounts = [];
  805. foreach ($this->toList() as $value) {
  806. $valueCount = count($value);
  807. if ($valueCount !== count($value, COUNT_RECURSIVE)) {
  808. throw new LogicException('Cannot find the cartesian product of a multidimensional array');
  809. }
  810. $collectionArraysKeys[] = array_keys($value);
  811. $collectionArraysCounts[] = $valueCount;
  812. $collectionArrays[] = $value;
  813. }
  814. $result = [];
  815. $lastIndex = count($collectionArrays) - 1;
  816. // holds the indexes of the arrays that generate the current combination
  817. $currentIndexes = array_fill(0, $lastIndex + 1, 0);
  818. $changeIndex = $lastIndex;
  819. while (!($changeIndex === 0 && $currentIndexes[0] === $collectionArraysCounts[0])) {
  820. $currentCombination = array_map(function ($value, $keys, $index) {
  821. return $value[$keys[$index]];
  822. }, $collectionArrays, $collectionArraysKeys, $currentIndexes);
  823. if ($filter === null || $filter($currentCombination)) {
  824. $result[] = $operation === null ? $currentCombination : $operation($currentCombination);
  825. }
  826. $currentIndexes[$lastIndex]++;
  827. for (
  828. $changeIndex = $lastIndex;
  829. $currentIndexes[$changeIndex] === $collectionArraysCounts[$changeIndex] && $changeIndex > 0;
  830. $changeIndex--
  831. ) {
  832. $currentIndexes[$changeIndex] = 0;
  833. $currentIndexes[$changeIndex - 1]++;
  834. }
  835. }
  836. return $this->newCollection($result);
  837. }
  838. /**
  839. * {@inheritDoc}
  840. *
  841. * @return \Cake\Collection\CollectionInterface
  842. * @throws \LogicException
  843. */
  844. public function transpose(): CollectionInterface
  845. {
  846. $arrayValue = $this->toList();
  847. $length = count(current($arrayValue));
  848. $result = [];
  849. foreach ($arrayValue as $row) {
  850. if (count($row) !== $length) {
  851. throw new LogicException('Child arrays do not have even length');
  852. }
  853. }
  854. for ($column = 0; $column < $length; $column++) {
  855. $result[] = array_column($arrayValue, $column);
  856. }
  857. return $this->newCollection($result);
  858. }
  859. /**
  860. * @inheritDoc
  861. */
  862. public function count(): int
  863. {
  864. $traversable = $this->optimizeUnwrap();
  865. if (is_array($traversable)) {
  866. return count($traversable);
  867. }
  868. return iterator_count($traversable);
  869. }
  870. /**
  871. * @inheritDoc
  872. */
  873. public function countKeys(): int
  874. {
  875. return count($this->toArray());
  876. }
  877. /**
  878. * Unwraps this iterator and returns the simplest
  879. * traversable that can be used for getting the data out
  880. *
  881. * @return iterable
  882. */
  883. protected function optimizeUnwrap(): iterable
  884. {
  885. /** @var \ArrayObject $iterator */
  886. $iterator = $this->unwrap();
  887. if (get_class($iterator) === ArrayIterator::class) {
  888. $iterator = $iterator->getArrayCopy();
  889. }
  890. return $iterator;
  891. }
  892. }