1: <?php
2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14:
15: namespace Cake\Collection;
16:
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 RecursiveIteratorIterator;
35: use Traversable;
36:
37: 38: 39:
40: trait CollectionTrait
41: {
42: use ExtractTrait;
43:
44: 45: 46: 47: 48: 49: 50: 51: 52:
53: protected function newCollection(...$args)
54: {
55: return new Collection(...$args);
56: }
57:
58: 59: 60:
61: public function each(callable $c)
62: {
63: foreach ($this->optimizeUnwrap() as $k => $v) {
64: $c($v, $k);
65: }
66:
67: return $this;
68: }
69:
70: 71: 72: 73: 74:
75: public function filter(callable $c = null)
76: {
77: if ($c === null) {
78: $c = function ($v) {
79: return (bool)$v;
80: };
81: }
82:
83: return new FilterIterator($this->unwrap(), $c);
84: }
85:
86: 87: 88: 89: 90:
91: public function reject(callable $c)
92: {
93: return new FilterIterator($this->unwrap(), function ($key, $value, $items) use ($c) {
94: return !$c($key, $value, $items);
95: });
96: }
97:
98: 99: 100:
101: public function every(callable $c)
102: {
103: foreach ($this->optimizeUnwrap() as $key => $value) {
104: if (!$c($value, $key)) {
105: return false;
106: }
107: }
108:
109: return true;
110: }
111:
112: 113: 114:
115: public function some(callable $c)
116: {
117: foreach ($this->optimizeUnwrap() as $key => $value) {
118: if ($c($value, $key) === true) {
119: return true;
120: }
121: }
122:
123: return false;
124: }
125:
126: 127: 128:
129: public function contains($value)
130: {
131: foreach ($this->optimizeUnwrap() as $v) {
132: if ($value === $v) {
133: return true;
134: }
135: }
136:
137: return false;
138: }
139:
140: 141: 142: 143: 144:
145: public function map(callable $c)
146: {
147: return new ReplaceIterator($this->unwrap(), $c);
148: }
149:
150: 151: 152:
153: public function reduce(callable $c, $zero = null)
154: {
155: $isFirst = false;
156: if (func_num_args() < 2) {
157: $isFirst = true;
158: }
159:
160: $result = $zero;
161: foreach ($this->optimizeUnwrap() as $k => $value) {
162: if ($isFirst) {
163: $result = $value;
164: $isFirst = false;
165: continue;
166: }
167: $result = $c($result, $value, $k);
168: }
169:
170: return $result;
171: }
172:
173: 174: 175:
176: public function extract($matcher)
177: {
178: $extractor = new ExtractIterator($this->unwrap(), $matcher);
179: if (is_string($matcher) && strpos($matcher, '{*}') !== false) {
180: $extractor = $extractor
181: ->filter(function ($data) {
182: return $data !== null && ($data instanceof Traversable || is_array($data));
183: })
184: ->unfold();
185: }
186:
187: return $extractor;
188: }
189:
190: 191: 192:
193: public function max($callback, $type = \SORT_NUMERIC)
194: {
195: return (new SortIterator($this->unwrap(), $callback, \SORT_DESC, $type))->first();
196: }
197:
198: 199: 200:
201: public function min($callback, $type = \SORT_NUMERIC)
202: {
203: return (new SortIterator($this->unwrap(), $callback, \SORT_ASC, $type))->first();
204: }
205:
206: 207: 208:
209: public function avg($matcher = null)
210: {
211: $result = $this;
212: if ($matcher != null) {
213: $result = $result->extract($matcher);
214: }
215: $result = $result
216: ->reduce(function ($acc, $current) {
217: list($count, $sum) = $acc;
218:
219: return [$count + 1, $sum + $current];
220: }, [0, 0]);
221:
222: if ($result[0] === 0) {
223: return null;
224: }
225:
226: return $result[1] / $result[0];
227: }
228:
229: 230: 231:
232: public function median($matcher = null)
233: {
234: $elements = $this;
235: if ($matcher != null) {
236: $elements = $elements->extract($matcher);
237: }
238: $values = $elements->toList();
239: sort($values);
240: $count = count($values);
241:
242: if ($count === 0) {
243: return null;
244: }
245:
246: $middle = (int)($count / 2);
247:
248: if ($count % 2) {
249: return $values[$middle];
250: }
251:
252: return ($values[$middle - 1] + $values[$middle]) / 2;
253: }
254:
255: 256: 257:
258: public function sortBy($callback, $dir = \SORT_DESC, $type = \SORT_NUMERIC)
259: {
260: return new SortIterator($this->unwrap(), $callback, $dir, $type);
261: }
262:
263: 264: 265:
266: public function groupBy($callback)
267: {
268: $callback = $this->_propertyExtractor($callback);
269: $group = [];
270: foreach ($this->optimizeUnwrap() as $value) {
271: $group[$callback($value)][] = $value;
272: }
273:
274: return $this->newCollection($group);
275: }
276:
277: 278: 279:
280: public function indexBy($callback)
281: {
282: $callback = $this->_propertyExtractor($callback);
283: $group = [];
284: foreach ($this->optimizeUnwrap() as $value) {
285: $group[$callback($value)] = $value;
286: }
287:
288: return $this->newCollection($group);
289: }
290:
291: 292: 293:
294: public function countBy($callback)
295: {
296: $callback = $this->_propertyExtractor($callback);
297:
298: $mapper = function ($value, $key, $mr) use ($callback) {
299:
300: $mr->emitIntermediate($value, $callback($value));
301: };
302:
303: $reducer = function ($values, $key, $mr) {
304:
305: $mr->emit(count($values), $key);
306: };
307:
308: return $this->newCollection(new MapReduce($this->unwrap(), $mapper, $reducer));
309: }
310:
311: 312: 313:
314: public function sumOf($matcher = null)
315: {
316: if ($matcher === null) {
317: return array_sum($this->toList());
318: }
319:
320: $callback = $this->_propertyExtractor($matcher);
321: $sum = 0;
322: foreach ($this->optimizeUnwrap() as $k => $v) {
323: $sum += $callback($v, $k);
324: }
325:
326: return $sum;
327: }
328:
329: 330: 331:
332: public function shuffle()
333: {
334: $elements = $this->toArray();
335: shuffle($elements);
336:
337: return $this->newCollection($elements);
338: }
339:
340: 341: 342:
343: public function sample($size = 10)
344: {
345: return $this->newCollection(new LimitIterator($this->shuffle(), 0, $size));
346: }
347:
348: 349: 350:
351: public function take($size = 1, $from = 0)
352: {
353: return $this->newCollection(new LimitIterator($this, $from, $size));
354: }
355:
356: 357: 358:
359: public function skip($howMany)
360: {
361: return $this->newCollection(new LimitIterator($this, $howMany));
362: }
363:
364: 365: 366:
367: public function match(array $conditions)
368: {
369: return $this->filter($this->_createMatcherFilter($conditions));
370: }
371:
372: 373: 374:
375: public function firstMatch(array $conditions)
376: {
377: return $this->match($conditions)->first();
378: }
379:
380: 381: 382:
383: public function first()
384: {
385: $iterator = new LimitIterator($this, 0, 1);
386: foreach ($iterator as $result) {
387: return $result;
388: }
389: }
390:
391: 392: 393:
394: public function last()
395: {
396: $iterator = $this->optimizeUnwrap();
397: if (is_array($iterator)) {
398: return array_pop($iterator);
399: }
400:
401: if ($iterator instanceof Countable) {
402: $count = count($iterator);
403: if ($count === 0) {
404: return null;
405: }
406: $iterator = new LimitIterator($iterator, $count - 1, 1);
407: }
408:
409: $result = null;
410: foreach ($iterator as $result) {
411:
412: }
413:
414: return $result;
415: }
416:
417: 418: 419:
420: public function takeLast($howMany)
421: {
422: if ($howMany < 1) {
423: throw new \InvalidArgumentException("The takeLast method requires a number greater than 0.");
424: }
425:
426: $iterator = $this->optimizeUnwrap();
427: if (is_array($iterator)) {
428: return $this->newCollection(array_slice($iterator, $howMany * -1));
429: }
430:
431: if ($iterator instanceof Countable) {
432: $count = count($iterator);
433:
434: if ($count === 0) {
435: return $this->newCollection([]);
436: }
437:
438: $iterator = new LimitIterator($iterator, max(0, $count - $howMany), $howMany);
439:
440: return $this->newCollection($iterator);
441: }
442:
443: $generator = function ($iterator, $howMany) {
444: $result = [];
445: $bucket = 0;
446: $offset = 0;
447:
448: 449: 450: 451: 452: 453: 454: 455: 456: 457: 458: 459: 460: 461: 462: 463: 464: 465: 466: 467: 468: 469: 470: 471: 472: 473: 474: 475: 476: 477: 478: 479: 480: 481: 482: 483: 484: 485: 486: 487: 488: 489: 490: 491: 492:
493:
494: foreach ($iterator as $k => $item) {
495: $result[$bucket] = [$k, $item];
496: $bucket = (++$bucket) % $howMany;
497: $offset++;
498: }
499:
500: $offset = $offset % $howMany;
501: $head = array_slice($result, $offset);
502: $tail = array_slice($result, 0, $offset);
503:
504: foreach ($head as $v) {
505: yield $v[0] => $v[1];
506: }
507:
508: foreach ($tail as $v) {
509: yield $v[0] => $v[1];
510: }
511: };
512:
513: return $this->newCollection($generator($iterator, $howMany));
514: }
515:
516: 517: 518:
519: public function append($items)
520: {
521: $list = new AppendIterator();
522: $list->append($this->unwrap());
523: $list->append($this->newCollection($items)->unwrap());
524:
525: return $this->newCollection($list);
526: }
527:
528: 529: 530:
531: public function appendItem($item, $key = null)
532: {
533: if ($key !== null) {
534: $data = [$key => $item];
535: } else {
536: $data = [$item];
537: }
538:
539: return $this->append($data);
540: }
541:
542: 543: 544:
545: public function prepend($items)
546: {
547: return $this->newCollection($items)->append($this);
548: }
549:
550: 551: 552:
553: public function prependItem($item, $key = null)
554: {
555: if ($key !== null) {
556: $data = [$key => $item];
557: } else {
558: $data = [$item];
559: }
560:
561: return $this->prepend($data);
562: }
563:
564: 565: 566:
567: public function combine($keyPath, $valuePath, $groupPath = null)
568: {
569: $options = [
570: 'keyPath' => $this->_propertyExtractor($keyPath),
571: 'valuePath' => $this->_propertyExtractor($valuePath),
572: 'groupPath' => $groupPath ? $this->_propertyExtractor($groupPath) : null
573: ];
574:
575: $mapper = function ($value, $key, $mapReduce) use ($options) {
576:
577: $rowKey = $options['keyPath'];
578: $rowVal = $options['valuePath'];
579:
580: if (!$options['groupPath']) {
581: $mapReduce->emit($rowVal($value, $key), $rowKey($value, $key));
582:
583: return null;
584: }
585:
586: $key = $options['groupPath']($value, $key);
587: $mapReduce->emitIntermediate(
588: [$rowKey($value, $key) => $rowVal($value, $key)],
589: $key
590: );
591: };
592:
593: $reducer = function ($values, $key, $mapReduce) {
594: $result = [];
595: foreach ($values as $value) {
596: $result += $value;
597: }
598:
599: $mapReduce->emit($result, $key);
600: };
601:
602: return $this->newCollection(new MapReduce($this->unwrap(), $mapper, $reducer));
603: }
604:
605: 606: 607:
608: public function nest($idPath, $parentPath, $nestingKey = 'children')
609: {
610: $parents = [];
611: $idPath = $this->_propertyExtractor($idPath);
612: $parentPath = $this->_propertyExtractor($parentPath);
613: $isObject = true;
614:
615: $mapper = function ($row, $key, $mapReduce) use (&$parents, $idPath, $parentPath, $nestingKey) {
616: $row[$nestingKey] = [];
617: $id = $idPath($row, $key);
618: $parentId = $parentPath($row, $key);
619: $parents[$id] =& $row;
620:
621: $mapReduce->emitIntermediate($id, $parentId);
622: };
623:
624: $reducer = function ($values, $key, $mapReduce) use (&$parents, &$isObject, $nestingKey) {
625: static $foundOutType = false;
626: if (!$foundOutType) {
627: $isObject = is_object(current($parents));
628: $foundOutType = true;
629: }
630: if (empty($key) || !isset($parents[$key])) {
631: foreach ($values as $id) {
632: $parents[$id] = $isObject ? $parents[$id] : new ArrayIterator($parents[$id], 1);
633:
634: $mapReduce->emit($parents[$id]);
635: }
636:
637: return null;
638: }
639:
640: $children = [];
641: foreach ($values as $id) {
642: $children[] =& $parents[$id];
643: }
644: $parents[$key][$nestingKey] = $children;
645: };
646:
647: return $this->newCollection(new MapReduce($this->unwrap(), $mapper, $reducer))
648: ->map(function ($value) use (&$isObject) {
649:
650: return $isObject ? $value : $value->getArrayCopy();
651: });
652: }
653:
654: 655: 656: 657: 658:
659: public function insert($path, $values)
660: {
661: return new InsertIterator($this->unwrap(), $path, $values);
662: }
663:
664: 665: 666:
667: public function toArray($preserveKeys = true)
668: {
669: $iterator = $this->unwrap();
670: if ($iterator instanceof ArrayIterator) {
671: $items = $iterator->getArrayCopy();
672:
673: return $preserveKeys ? $items : array_values($items);
674: }
675:
676:
677: if ($preserveKeys && get_class($iterator) === 'RecursiveIteratorIterator') {
678: $preserveKeys = false;
679: }
680:
681: return iterator_to_array($this, $preserveKeys);
682: }
683:
684: 685: 686:
687: public function toList()
688: {
689: return $this->toArray(false);
690: }
691:
692: 693: 694:
695: public function jsonSerialize()
696: {
697: return $this->toArray();
698: }
699:
700: 701: 702:
703: public function compile($preserveKeys = true)
704: {
705: return $this->newCollection($this->toArray($preserveKeys));
706: }
707:
708: 709: 710:
711: public function lazy()
712: {
713: $generator = function () {
714: foreach ($this->unwrap() as $k => $v) {
715: yield $k => $v;
716: }
717: };
718:
719: return $this->newCollection($generator());
720: }
721:
722: 723: 724: 725: 726:
727: public function buffered()
728: {
729: return new BufferedIterator($this->unwrap());
730: }
731:
732: 733: 734: 735: 736:
737: public function listNested($dir = 'desc', $nestingKey = 'children')
738: {
739: $dir = strtolower($dir);
740: $modes = [
741: 'desc' => TreeIterator::SELF_FIRST,
742: 'asc' => TreeIterator::CHILD_FIRST,
743: 'leaves' => TreeIterator::LEAVES_ONLY
744: ];
745:
746: return new TreeIterator(
747: new NestIterator($this, $nestingKey),
748: isset($modes[$dir]) ? $modes[$dir] : $dir
749: );
750: }
751:
752: 753: 754: 755: 756:
757: public function stopWhen($condition)
758: {
759: if (!is_callable($condition)) {
760: $condition = $this->_createMatcherFilter($condition);
761: }
762:
763: return new StoppableIterator($this->unwrap(), $condition);
764: }
765:
766: 767: 768:
769: public function unfold(callable $transformer = null)
770: {
771: if ($transformer === null) {
772: $transformer = function ($item) {
773: return $item;
774: };
775: }
776:
777: return $this->newCollection(
778: new RecursiveIteratorIterator(
779: new UnfoldIterator($this->unwrap(), $transformer),
780: RecursiveIteratorIterator::LEAVES_ONLY
781: )
782: );
783: }
784:
785: 786: 787:
788: public function through(callable $handler)
789: {
790: $result = $handler($this);
791:
792: return $result instanceof CollectionInterface ? $result : $this->newCollection($result);
793: }
794:
795: 796: 797:
798: public function zip($items)
799: {
800: return new ZipIterator(array_merge([$this->unwrap()], func_get_args()));
801: }
802:
803: 804: 805:
806: public function zipWith($items, $callable)
807: {
808: if (func_num_args() > 2) {
809: $items = func_get_args();
810: $callable = array_pop($items);
811: } else {
812: $items = [$items];
813: }
814:
815: return new ZipIterator(array_merge([$this->unwrap()], $items), $callable);
816: }
817:
818: 819: 820:
821: public function chunk($chunkSize)
822: {
823: return $this->map(function ($v, $k, $iterator) use ($chunkSize) {
824: $values = [$v];
825: for ($i = 1; $i < $chunkSize; $i++) {
826: $iterator->next();
827: if (!$iterator->valid()) {
828: break;
829: }
830: $values[] = $iterator->current();
831: }
832:
833: return $values;
834: });
835: }
836:
837: 838: 839:
840: public function chunkWithKeys($chunkSize, $preserveKeys = true)
841: {
842: return $this->map(function ($v, $k, $iterator) use ($chunkSize, $preserveKeys) {
843: $key = 0;
844: if ($preserveKeys) {
845: $key = $k;
846: }
847: $values = [$key => $v];
848: for ($i = 1; $i < $chunkSize; $i++) {
849: $iterator->next();
850: if (!$iterator->valid()) {
851: break;
852: }
853: if ($preserveKeys) {
854: $values[$iterator->key()] = $iterator->current();
855: } else {
856: $values[] = $iterator->current();
857: }
858: }
859:
860: return $values;
861: });
862: }
863:
864: 865: 866:
867: public function isEmpty()
868: {
869: foreach ($this as $el) {
870: return false;
871: }
872:
873: return true;
874: }
875:
876: 877: 878:
879: public function unwrap()
880: {
881: $iterator = $this;
882: while (get_class($iterator) === 'Cake\Collection\Collection') {
883: $iterator = $iterator->getInnerIterator();
884: }
885:
886: if ($iterator !== $this && $iterator instanceof CollectionInterface) {
887: $iterator = $iterator->unwrap();
888: }
889:
890: return $iterator;
891: }
892:
893: 894: 895: 896: 897: 898:
899:
900: public function _unwrap()
901: {
902: deprecationWarning('CollectionTrait::_unwrap() is deprecated. Use CollectionTrait::unwrap() instead.');
903:
904: return $this->unwrap();
905: }
906:
907: 908: 909: 910: 911: 912:
913: public function cartesianProduct(callable $operation = null, callable $filter = null)
914: {
915: if ($this->isEmpty()) {
916: return $this->newCollection([]);
917: }
918:
919: $collectionArrays = [];
920: $collectionArraysKeys = [];
921: $collectionArraysCounts = [];
922:
923: foreach ($this->toList() as $value) {
924: $valueCount = count($value);
925: if ($valueCount !== count($value, COUNT_RECURSIVE)) {
926: throw new LogicException('Cannot find the cartesian product of a multidimensional array');
927: }
928:
929: $collectionArraysKeys[] = array_keys($value);
930: $collectionArraysCounts[] = $valueCount;
931: $collectionArrays[] = $value;
932: }
933:
934: $result = [];
935: $lastIndex = count($collectionArrays) - 1;
936:
937: $currentIndexes = array_fill(0, $lastIndex + 1, 0);
938:
939: $changeIndex = $lastIndex;
940:
941: while (!($changeIndex === 0 && $currentIndexes[0] === $collectionArraysCounts[0])) {
942: $currentCombination = array_map(function ($value, $keys, $index) {
943: return $value[$keys[$index]];
944: }, $collectionArrays, $collectionArraysKeys, $currentIndexes);
945:
946: if ($filter === null || $filter($currentCombination)) {
947: $result[] = ($operation === null) ? $currentCombination : $operation($currentCombination);
948: }
949:
950: $currentIndexes[$lastIndex]++;
951:
952: for ($changeIndex = $lastIndex; $currentIndexes[$changeIndex] === $collectionArraysCounts[$changeIndex] && $changeIndex > 0; $changeIndex--) {
953: $currentIndexes[$changeIndex] = 0;
954: $currentIndexes[$changeIndex - 1]++;
955: }
956: }
957:
958: return $this->newCollection($result);
959: }
960:
961: 962: 963: 964: 965: 966:
967: public function transpose()
968: {
969: $arrayValue = $this->toList();
970: $length = count(current($arrayValue));
971: $result = [];
972: foreach ($arrayValue as $column => $row) {
973: if (count($row) != $length) {
974: throw new LogicException('Child arrays do not have even length');
975: }
976: }
977:
978: for ($column = 0; $column < $length; $column++) {
979: $result[] = array_column($arrayValue, $column);
980: }
981:
982: return $this->newCollection($result);
983: }
984:
985: 986: 987: 988: 989:
990: public function count()
991: {
992: $traversable = $this->optimizeUnwrap();
993:
994: if (is_array($traversable)) {
995: return count($traversable);
996: }
997:
998: return iterator_count($traversable);
999: }
1000:
1001: 1002: 1003: 1004: 1005:
1006: public function countKeys()
1007: {
1008: return count($this->toArray());
1009: }
1010:
1011: 1012: 1013: 1014: 1015: 1016:
1017: protected function optimizeUnwrap()
1018: {
1019: $iterator = $this->unwrap();
1020:
1021: if (get_class($iterator) === ArrayIterator::class) {
1022: $iterator = $iterator->getArrayCopy();
1023: }
1024:
1025: return $iterator;
1026: }
1027: }
1028: