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:
49: public function each(callable $c)
50: {
51: foreach ($this->unwrap() as $k => $v) {
52: $c($v, $k);
53: }
54:
55: return $this;
56: }
57:
58: 59: 60: 61: 62:
63: public function filter(callable $c = null)
64: {
65: if ($c === null) {
66: $c = function ($v) {
67: return (bool)$v;
68: };
69: }
70:
71: return new FilterIterator($this->unwrap(), $c);
72: }
73:
74: 75: 76: 77: 78:
79: public function reject(callable $c)
80: {
81: return new FilterIterator($this->unwrap(), function ($key, $value, $items) use ($c) {
82: return !$c($key, $value, $items);
83: });
84: }
85:
86: 87: 88: 89:
90: public function every(callable $c)
91: {
92: $return = false;
93: foreach ($this->unwrap() as $key => $value) {
94: $return = true;
95: if (!$c($value, $key)) {
96: return false;
97: }
98: }
99:
100: return $return;
101: }
102:
103: 104: 105: 106:
107: public function some(callable $c)
108: {
109: foreach ($this->unwrap() as $key => $value) {
110: if ($c($value, $key) === true) {
111: return true;
112: }
113: }
114:
115: return false;
116: }
117:
118: 119: 120: 121:
122: public function contains($value)
123: {
124: foreach ($this->unwrap() as $v) {
125: if ($value === $v) {
126: return true;
127: }
128: }
129:
130: return false;
131: }
132:
133: 134: 135: 136: 137:
138: public function map(callable $c)
139: {
140: return new ReplaceIterator($this->unwrap(), $c);
141: }
142:
143: 144: 145: 146:
147: public function reduce(callable $c, $zero = null)
148: {
149: $isFirst = false;
150: if (func_num_args() < 2) {
151: $isFirst = true;
152: }
153:
154: $result = $zero;
155: foreach ($this->unwrap() as $k => $value) {
156: if ($isFirst) {
157: $result = $value;
158: $isFirst = false;
159: continue;
160: }
161: $result = $c($result, $value, $k);
162: }
163:
164: return $result;
165: }
166:
167: 168: 169: 170:
171: public function extract($matcher)
172: {
173: $extractor = new ExtractIterator($this->unwrap(), $matcher);
174: if (is_string($matcher) && strpos($matcher, '{*}') !== false) {
175: $extractor = $extractor
176: ->filter(function ($data) {
177: return $data !== null && ($data instanceof Traversable || is_array($data));
178: })
179: ->unfold();
180: }
181:
182: return $extractor;
183: }
184:
185: 186: 187: 188:
189: public function max($callback, $type = SORT_NUMERIC)
190: {
191: return (new SortIterator($this->unwrap(), $callback, SORT_DESC, $type))->first();
192: }
193:
194: 195: 196: 197:
198: public function min($callback, $type = SORT_NUMERIC)
199: {
200: return (new SortIterator($this->unwrap(), $callback, SORT_ASC, $type))->first();
201: }
202:
203: 204: 205: 206:
207: public function sortBy($callback, $dir = SORT_DESC, $type = SORT_NUMERIC)
208: {
209: return new SortIterator($this->unwrap(), $callback, $dir, $type);
210: }
211:
212: 213: 214: 215:
216: public function groupBy($callback)
217: {
218: $callback = $this->_propertyExtractor($callback);
219: $group = [];
220: foreach ($this as $value) {
221: $group[$callback($value)][] = $value;
222: }
223:
224: return new Collection($group);
225: }
226:
227: 228: 229: 230:
231: public function indexBy($callback)
232: {
233: $callback = $this->_propertyExtractor($callback);
234: $group = [];
235: foreach ($this as $value) {
236: $group[$callback($value)] = $value;
237: }
238:
239: return new Collection($group);
240: }
241:
242: 243: 244: 245:
246: public function countBy($callback)
247: {
248: $callback = $this->_propertyExtractor($callback);
249:
250: $mapper = function ($value, $key, $mr) use ($callback) {
251: $mr->emitIntermediate($value, $callback($value));
252: };
253:
254: $reducer = function ($values, $key, $mr) {
255: $mr->emit(count($values), $key);
256: };
257:
258: return new Collection(new MapReduce($this->unwrap(), $mapper, $reducer));
259: }
260:
261: 262: 263: 264:
265: public function sumOf($matcher = null)
266: {
267: if ($matcher === null) {
268: return array_sum($this->toList());
269: }
270:
271: $callback = $this->_propertyExtractor($matcher);
272: $sum = 0;
273: foreach ($this as $k => $v) {
274: $sum += $callback($v, $k);
275: }
276:
277: return $sum;
278: }
279:
280: 281: 282: 283:
284: public function shuffle()
285: {
286: $elements = $this->toArray();
287: shuffle($elements);
288:
289: return new Collection($elements);
290: }
291:
292: 293: 294: 295:
296: public function sample($size = 10)
297: {
298: return new Collection(new LimitIterator($this->shuffle(), 0, $size));
299: }
300:
301: 302: 303: 304:
305: public function take($size = 1, $from = 0)
306: {
307: return new Collection(new LimitIterator($this->unwrap(), $from, $size));
308: }
309:
310: 311: 312: 313:
314: public function skip($howMany)
315: {
316: return new Collection(new LimitIterator($this->unwrap(), $howMany));
317: }
318:
319: 320: 321: 322:
323: public function match(array $conditions)
324: {
325: return $this->filter($this->_createMatcherFilter($conditions));
326: }
327:
328: 329: 330: 331:
332: public function firstMatch(array $conditions)
333: {
334: return $this->match($conditions)->first();
335: }
336:
337: 338: 339: 340:
341: public function first()
342: {
343: foreach ($this->take(1) as $result) {
344: return $result;
345: }
346: }
347:
348: 349: 350: 351:
352: public function last()
353: {
354: $iterator = $this->unwrap();
355: $count = $iterator instanceof Countable ?
356: count($iterator) :
357: iterator_count($iterator);
358:
359: if ($count === 0) {
360: return null;
361: }
362:
363: foreach ($this->take(1, $count - 1) as $last) {
364: return $last;
365: }
366: }
367:
368: 369: 370: 371:
372: public function append($items)
373: {
374: $list = new AppendIterator();
375: $list->append($this->unwrap());
376: $list->append((new Collection($items))->unwrap());
377:
378: return new Collection($list);
379: }
380:
381: 382: 383: 384:
385: public function combine($keyPath, $valuePath, $groupPath = null)
386: {
387: $options = [
388: 'keyPath' => $this->_propertyExtractor($keyPath),
389: 'valuePath' => $this->_propertyExtractor($valuePath),
390: 'groupPath' => $groupPath ? $this->_propertyExtractor($groupPath) : null
391: ];
392:
393: $mapper = function ($value, $key, $mapReduce) use ($options) {
394: $rowKey = $options['keyPath'];
395: $rowVal = $options['valuePath'];
396:
397: if (!($options['groupPath'])) {
398: $mapReduce->emit($rowVal($value, $key), $rowKey($value, $key));
399:
400: return null;
401: }
402:
403: $key = $options['groupPath']($value, $key);
404: $mapReduce->emitIntermediate(
405: [$rowKey($value, $key) => $rowVal($value, $key)],
406: $key
407: );
408: };
409:
410: $reducer = function ($values, $key, $mapReduce) {
411: $result = [];
412: foreach ($values as $value) {
413: $result += $value;
414: }
415: $mapReduce->emit($result, $key);
416: };
417:
418: return new Collection(new MapReduce($this->unwrap(), $mapper, $reducer));
419: }
420:
421: 422: 423: 424:
425: public function nest($idPath, $parentPath, $nestingKey = 'children')
426: {
427: $parents = [];
428: $idPath = $this->_propertyExtractor($idPath);
429: $parentPath = $this->_propertyExtractor($parentPath);
430: $isObject = true;
431:
432: $mapper = function ($row, $key, $mapReduce) use (&$parents, $idPath, $parentPath, $nestingKey) {
433: $row[$nestingKey] = [];
434: $id = $idPath($row, $key);
435: $parentId = $parentPath($row, $key);
436: $parents[$id] =& $row;
437: $mapReduce->emitIntermediate($id, $parentId);
438: };
439:
440: $reducer = function ($values, $key, $mapReduce) use (&$parents, &$isObject, $nestingKey) {
441: static $foundOutType = false;
442: if (!$foundOutType) {
443: $isObject = is_object(current($parents));
444: $foundOutType = true;
445: }
446: if (empty($key) || !isset($parents[$key])) {
447: foreach ($values as $id) {
448: $parents[$id] = $isObject ? $parents[$id] : new ArrayIterator($parents[$id], 1);
449: $mapReduce->emit($parents[$id]);
450: }
451:
452: return null;
453: }
454:
455: $children = [];
456: foreach ($values as $id) {
457: $children[] =& $parents[$id];
458: }
459: $parents[$key][$nestingKey] = $children;
460: };
461:
462: return (new Collection(new MapReduce($this->unwrap(), $mapper, $reducer)))
463: ->map(function ($value) use (&$isObject) {
464: return $isObject ? $value : $value->getArrayCopy();
465: });
466: }
467:
468: 469: 470: 471: 472:
473: public function insert($path, $values)
474: {
475: return new InsertIterator($this->unwrap(), $path, $values);
476: }
477:
478: 479: 480: 481:
482: public function toArray($preserveKeys = true)
483: {
484: $iterator = $this->unwrap();
485: if ($iterator instanceof ArrayIterator) {
486: $items = $iterator->getArrayCopy();
487:
488: return $preserveKeys ? $items : array_values($items);
489: }
490:
491:
492: if ($preserveKeys && get_class($iterator) === 'RecursiveIteratorIterator') {
493: $preserveKeys = false;
494: }
495:
496: return iterator_to_array($this, $preserveKeys);
497: }
498:
499: 500: 501: 502:
503: public function toList()
504: {
505: return $this->toArray(false);
506: }
507:
508: 509: 510: 511:
512: public function jsonSerialize()
513: {
514: return $this->toArray();
515: }
516:
517: 518: 519: 520:
521: public function compile($preserveKeys = true)
522: {
523: return new Collection($this->toArray($preserveKeys));
524: }
525:
526: 527: 528: 529: 530:
531: public function buffered()
532: {
533: return new BufferedIterator($this);
534: }
535:
536: 537: 538: 539: 540:
541: public function listNested($dir = 'desc', $nestingKey = 'children')
542: {
543: $dir = strtolower($dir);
544: $modes = [
545: 'desc' => TreeIterator::SELF_FIRST,
546: 'asc' => TreeIterator::CHILD_FIRST,
547: 'leaves' => TreeIterator::LEAVES_ONLY
548: ];
549:
550: return new TreeIterator(
551: new NestIterator($this, $nestingKey),
552: isset($modes[$dir]) ? $modes[$dir] : $dir
553: );
554: }
555:
556: 557: 558: 559: 560:
561: public function stopWhen($condition)
562: {
563: if (!is_callable($condition)) {
564: $condition = $this->_createMatcherFilter($condition);
565: }
566:
567: return new StoppableIterator($this, $condition);
568: }
569:
570: 571: 572: 573:
574: public function unfold(callable $transformer = null)
575: {
576: if ($transformer === null) {
577: $transformer = function ($item) {
578: return $item;
579: };
580: }
581:
582: return new Collection(
583: new RecursiveIteratorIterator(
584: new UnfoldIterator($this, $transformer),
585: RecursiveIteratorIterator::LEAVES_ONLY
586: )
587: );
588: }
589:
590: 591: 592: 593:
594: public function through(callable $handler)
595: {
596: $result = $handler($this);
597:
598: return $result instanceof CollectionInterface ? $result : new Collection($result);
599: }
600:
601: 602: 603: 604:
605: public function zip($items)
606: {
607: return new ZipIterator(array_merge([$this], func_get_args()));
608: }
609:
610: 611: 612: 613:
614: public function zipWith($items, $callable)
615: {
616: if (func_num_args() > 2) {
617: $items = func_get_args();
618: $callable = array_pop($items);
619: } else {
620: $items = [$items];
621: }
622:
623: return new ZipIterator(array_merge([$this], $items), $callable);
624: }
625:
626: 627: 628: 629:
630: public function chunk($chunkSize)
631: {
632: return $this->map(function ($v, $k, $iterator) use ($chunkSize) {
633: $values = [$v];
634: for ($i = 1; $i < $chunkSize; $i++) {
635: $iterator->next();
636: if (!$iterator->valid()) {
637: break;
638: }
639: $values[] = $iterator->current();
640: }
641:
642: return $values;
643: });
644: }
645:
646: 647: 648: 649:
650: public function isEmpty()
651: {
652: foreach ($this->unwrap() as $el) {
653: return false;
654: }
655:
656: return true;
657: }
658:
659: 660: 661: 662:
663: public function unwrap()
664: {
665: $iterator = $this;
666: while (get_class($iterator) === 'Cake\Collection\Collection') {
667: $iterator = $iterator->getInnerIterator();
668: }
669:
670: return $iterator;
671: }
672:
673: 674: 675: 676: 677: 678:
679: public function _unwrap()
680: {
681: return $this->unwrap();
682: }
683:
684: 685: 686: 687: 688:
689: public function cartesianProduct(callable $operation = null, callable $filter = null)
690: {
691: if ($this->isEmpty()) {
692: return new Collection([]);
693: }
694:
695: $collectionArrays = [];
696: $collectionArraysKeys = [];
697: $collectionArraysCounts = [];
698:
699: foreach ($this->toList() as $value) {
700: $valueCount = count($value);
701: if ($valueCount !== count($value, COUNT_RECURSIVE)) {
702: throw new LogicException('Cannot find the cartesian product of a multidimensional array');
703: }
704:
705: $collectionArraysKeys[] = array_keys($value);
706: $collectionArraysCounts[] = $valueCount;
707: $collectionArrays[] = $value;
708: }
709:
710: $result = [];
711: $lastIndex = count($collectionArrays) - 1;
712:
713: $currentIndexes = array_fill(0, $lastIndex + 1, 0);
714:
715: $changeIndex = $lastIndex;
716:
717: while (!($changeIndex === 0 && $currentIndexes[0] === $collectionArraysCounts[0])) {
718: $currentCombination = array_map(function ($value, $keys, $index) {
719: return $value[$keys[$index]];
720: }, $collectionArrays, $collectionArraysKeys, $currentIndexes);
721:
722: if ($filter === null || $filter($currentCombination)) {
723: $result[] = ($operation === null) ? $currentCombination : $operation($currentCombination);
724: }
725:
726: $currentIndexes[$lastIndex]++;
727:
728: for ($changeIndex = $lastIndex; $currentIndexes[$changeIndex] === $collectionArraysCounts[$changeIndex] && $changeIndex > 0; $changeIndex--) {
729: $currentIndexes[$changeIndex] = 0;
730: $currentIndexes[$changeIndex - 1]++;
731: }
732: }
733:
734: return new Collection($result);
735: }
736:
737: 738: 739: 740: 741:
742: public function transpose()
743: {
744: $arrayValue = $this->toList();
745: $length = count(current($arrayValue));
746: $result = [];
747: foreach ($arrayValue as $column => $row) {
748: if (count($row) != $length) {
749: throw new LogicException('Child arrays do not have even length');
750: }
751: }
752:
753: for ($column = 0; $column < $length; $column++) {
754: $result[] = array_column($arrayValue, $column);
755: }
756:
757: return new Collection($result);
758: }
759: }
760: