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