CollectionTrait.php 27 KB

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