1 <?php
2
3 namespace Docolight\Support;
4
5 use Countable;
6 use ArrayAccess;
7 use ArrayIterator;
8 use CachingIterator;
9 use JsonSerializable;
10 use IteratorAggregate;
11 use InvalidArgumentException;
12 use Docolight\Http\Contracts\Arrayable;
13
14 /**
15 * Collection contains a lot of handy methods that will make your work so much easier.
16 *
17 * ```php
18 * $results = json_decode($github->request('users/krisanalfa/repos'));
19 *
20 * // Wrap them in a collection.
21 * $collection = new \Docolight\Support\Collection($results);
22 *
23 * // Sort descending by stars.
24 * $collection->sortByDesc('stargazers_count');
25 *
26 * // Get top 5 repositories.
27 * $topFiveRepo = $leaderboard->take(5);
28 *
29 * // This method will return every value of a given key. The following example returns every user's email address, indexed by their user id.
30 * $collection->lists('email', 'id');
31 *
32 * // This will run a filter function over each of the items. If the callback returns true, it will be present in the resulting collection.
33 * $collection->filter(function($user) { if ($user->isNearby($me)) return true; });
34 *
35 * // Another great thing about collections is that they can easily be converted to json.
36 * echo $collection->toJson();
37 *
38 * // When you cast a collection to a string, it will actually call the toJson method
39 * echo $collection;
40 *
41 * // Something that's quite obvious is the count method, which just returns you how many items there are in the collection.
42 * $collection->count();
43 *
44 * // Works just like the query, takes the first or last number of items.
45 * $fiveFirst = $collection->take(5);
46 * $fiveLast = $collection->take(-5);
47 *
48 * // The sum method will return the sum based on the key or a callback function:
49 * $collection->sum('points');
50 *
51 * // You can use sortBy or sortByDesc to sort the collection based on a key or a callback function:
52 * $collection->sortBy('name');
53 *
54 * // Sort descending by rating.
55 * $collection->sortByDesc(function($item) { return $item->rating; });
56 *
57 * // You can also filter your items
58 * $collection->filter(function ($item) { $item->foo === true; }); // will return the collection with foo === true
59 *
60 * // You can paginate the collection too!
61 * $collection->forPage(1, 20); // For page 1, each page has 20 items in it
62 * ```
63 *
64 * @author Krisan Alfa Timur <krisanalfa@docotel.co.id>
65 */
66 class Collection implements ArrayAccess, Countable, IteratorAggregate, JsonSerializable, Arrayable
67 {
68 /**
69 * The items contained in the collection.
70 *
71 * @var array
72 */
73 protected $items = [];
74
75 /**
76 * The items contained in the collection.
77 *
78 * @var array
79 */
80 protected $filters = [];
81
82 /**
83 * Create a new collection.
84 *
85 * @param mixed $items
86 */
87 public function __construct($items = [])
88 {
89 $this->items = is_array($items) ? $items : $this->getArrayableItems($items);
90 }
91
92 /**
93 * Create a new collection instance if the value isn't one already.
94 *
95 * @param mixed $items
96 *
97 * @return static
98 */
99 public static function make($items = null)
100 {
101 return new static($items);
102 }
103
104 /**
105 * {@inheritdoc}
106 */
107 public function fill($attributes)
108 {
109 foreach ((array) $attributes as $index => $value) {
110 $this->offsetSet($index, $value);
111 }
112 }
113
114 /**
115 * Get all of the items in the collection.
116 *
117 * @return array
118 */
119 public function all()
120 {
121 return $this->items;
122 }
123
124 /**
125 * Collapse the collection of items into a single array.
126 *
127 * @return static
128 */
129 public function collapse()
130 {
131 return new static(Arr::collapse($this->items));
132 }
133
134 /**
135 * Determine if an item exists in the collection.
136 *
137 * @param mixed $key
138 * @param mixed $value
139 *
140 * @return bool
141 */
142 public function contains($key, $value = null)
143 {
144 if (func_num_args() == 2) {
145 return $this->contains(function ($k, $item) use ($key, $value) {
146 return data_get($item, $key) == $value;
147 });
148 }
149
150 if ($this->useAsCallable($key)) {
151 return !is_null($this->first($key));
152 }
153
154 return in_array($key, $this->items);
155 }
156
157 /**
158 * Diff the collection with the given items.
159 *
160 * @param mixed $items
161 *
162 * @return static
163 */
164 public function diff($items)
165 {
166 return new static(array_diff($this->items, $this->getArrayableItems($items)));
167 }
168
169 /**
170 * Execute a callback over each item.
171 *
172 * @param callable $callback
173 *
174 * @return $this
175 */
176 public function each(callable $callback)
177 {
178 foreach ($this->items as $key => $item) {
179 if ($callback($item, $key) === false) {
180 break;
181 }
182 }
183
184 return $this;
185 }
186
187 /**
188 * Fetch a nested element of the collection.
189 *
190 * @param string $key
191 *
192 * @return static
193 *
194 * @deprecated since version 5.1. Use pluck instead.
195 */
196 public function fetch($key)
197 {
198 return new static(Arr::fetch($this->items, $key));
199 }
200
201 /**
202 * Run a filter over each of the items.
203 *
204 * @param callable|null $callback
205 *
206 * @return static
207 */
208 public function filter(callable $callback = null)
209 {
210 if ($callback) {
211 return new static(array_filter($this->items, $callback));
212 }
213
214 return new static(array_filter($this->items));
215 }
216
217 /**
218 * Filter items by the given key value pair.
219 *
220 * @param string $key
221 * @param mixed $value
222 * @param bool $strict
223 *
224 * @return static
225 */
226 public function where($key, $value, $strict = true)
227 {
228 return $this->filter(function ($item) use ($key, $value, $strict) {
229 return $strict ? data_get($item, $key) === $value
230 : data_get($item, $key) == $value;
231 });
232 }
233
234 /**
235 * Filter items by the given key value pair using loose comparison.
236 *
237 * @param string $key
238 * @param mixed $value
239 *
240 * @return static
241 */
242 public function whereLoose($key, $value)
243 {
244 return $this->where($key, $value, false);
245 }
246
247 /**
248 * Get the first item from the collection.
249 *
250 * @param callable|null $callback
251 * @param mixed $default
252 *
253 * @return mixed
254 */
255 public function first(callable $callback = null, $default = null)
256 {
257 if (is_null($callback)) {
258 return count($this->items) > 0 ? reset($this->items) : null;
259 }
260
261 return array_first($this->items, $callback, $default);
262 }
263
264 /**
265 * Get a flattened array of the items in the collection.
266 *
267 * @return static
268 */
269 public function flatten()
270 {
271 return new static(array_flatten($this->items));
272 }
273
274 /**
275 * Flip the items in the collection.
276 *
277 * @return static
278 */
279 public function flip()
280 {
281 return new static(array_flip($this->items));
282 }
283
284 /**
285 * Remove an item from the collection by key.
286 *
287 * @param mixed $key
288 *
289 * @return $this
290 */
291 public function forget($key)
292 {
293 $this->offsetUnset($key);
294
295 return $this;
296 }
297
298 /**
299 * Get an item from the collection by key.
300 *
301 * @param mixed $key
302 * @param mixed $default
303 *
304 * @return mixed
305 */
306 public function get($key, $default = null)
307 {
308 if ($this->offsetExists($key)) {
309 return $this->items[$key];
310 }
311
312 return value($default);
313 }
314
315 /**
316 * Group an associative array by a field or using a callback.
317 *
318 * @param callable|string $groupBy
319 * @param bool $preserveKeys
320 *
321 * @return static
322 */
323 public function groupBy($groupBy, $preserveKeys = false)
324 {
325 $groupBy = $this->valueRetriever($groupBy);
326
327 $results = [];
328
329 foreach ($this->items as $key => $value) {
330 $groupKey = $groupBy($value, $key);
331
332 if (!array_key_exists($groupKey, $results)) {
333 $results[$groupKey] = new static();
334 }
335
336 $results[$groupKey]->offsetSet($preserveKeys ? $key : null, $value);
337 }
338
339 return new static($results);
340 }
341
342 /**
343 * Key an associative array by a field or using a callback.
344 *
345 * @param callable|string $keyBy
346 *
347 * @return static
348 */
349 public function keyBy($keyBy)
350 {
351 $keyBy = $this->valueRetriever($keyBy);
352
353 $results = [];
354
355 foreach ($this->items as $item) {
356 $results[$keyBy($item)] = $item;
357 }
358
359 return new static($results);
360 }
361
362 /**
363 * Determine if an item exists in the collection by key.
364 *
365 * @param mixed $key
366 *
367 * @return bool
368 */
369 public function has($key)
370 {
371 return $this->offsetExists($key);
372 }
373
374 /**
375 * Concatenate values of a given key as a string.
376 *
377 * @param string $value
378 * @param string $glue
379 *
380 * @return string
381 */
382 public function implode($value, $glue = null)
383 {
384 $first = $this->first();
385
386 if (is_array($first) || is_object($first)) {
387 return implode($glue, $this->pluck($value)->all());
388 }
389
390 return implode($value, $this->items);
391 }
392
393 /**
394 * Intersect the collection with the given items.
395 *
396 * @param mixed $items
397 *
398 * @return static
399 */
400 public function intersect($items)
401 {
402 return new static(array_intersect($this->items, $this->getArrayableItems($items)));
403 }
404
405 /**
406 * Determine if the collection is empty or not.
407 *
408 * @return bool
409 */
410 public function isEmpty()
411 {
412 return empty($this->items);
413 }
414
415 /**
416 * Determine if the given value is callable, but not a string.
417 *
418 * @param mixed $value
419 *
420 * @return bool
421 */
422 protected function useAsCallable($value)
423 {
424 return !is_string($value) && is_callable($value);
425 }
426
427 /**
428 * Get the keys of the collection items.
429 *
430 * @return static
431 */
432 public function keys()
433 {
434 return new static(array_keys($this->items));
435 }
436
437 /**
438 * Get the last item from the collection.
439 *
440 * @param callable|null $callback
441 * @param mixed $default
442 *
443 * @return mixed
444 */
445 public function last(callable $callback = null, $default = null)
446 {
447 if (is_null($callback)) {
448 return count($this->items) > 0 ? end($this->items) : value($default);
449 }
450
451 return Arr::last($this->items, $callback, $default);
452 }
453
454 /**
455 * Get an array with the values of a given key.
456 *
457 * @param string $value
458 * @param string $key
459 *
460 * @return static
461 */
462 public function pluck($value, $key = null)
463 {
464 return new static(Arr::pluck($this->items, $value, $key));
465 }
466
467 /**
468 * Alias for the "pluck" method.
469 *
470 * @param string $value
471 * @param string $key
472 *
473 * @return static
474 */
475 public function lists($value, $key = null)
476 {
477 return $this->pluck($value, $key);
478 }
479
480 /**
481 * Run a map over each of the items.
482 *
483 * @param callable $callback
484 *
485 * @return static
486 */
487 public function map(callable $callback)
488 {
489 $keys = array_keys($this->items);
490
491 $items = array_map($callback, $this->items, $keys);
492
493 return new static(array_combine($keys, $items));
494 }
495
496 /**
497 * Merge the collection with the given items.
498 *
499 * @param mixed $items
500 *
501 * @return static
502 */
503 public function merge($items)
504 {
505 return new static(array_merge($this->items, $this->getArrayableItems($items)));
506 }
507
508 /**
509 * "Paginate" the collection by slicing it into a smaller collection.
510 *
511 * @param int $page
512 * @param int $perPage
513 *
514 * @return static
515 */
516 public function forPage($page, $perPage)
517 {
518 return $this->slice(($page - 1) * $perPage, $perPage);
519 }
520
521 /**
522 * Get and remove the last item from the collection.
523 *
524 * @return mixed
525 */
526 public function pop()
527 {
528 return array_pop($this->items);
529 }
530
531 /**
532 * Push an item onto the beginning of the collection.
533 *
534 * @param mixed $value
535 *
536 * @return $this
537 */
538 public function prepend($value)
539 {
540 array_unshift($this->items, $value);
541
542 return $this;
543 }
544
545 /**
546 * Push an item onto the end of the collection.
547 *
548 * @param mixed $value
549 *
550 * @return $this
551 */
552 public function push($value)
553 {
554 $this->offsetSet(null, $value);
555
556 return $this;
557 }
558
559 /**
560 * Pulls an item from the collection.
561 *
562 * @param mixed $key
563 * @param mixed $default
564 *
565 * @return mixed
566 */
567 public function pull($key, $default = null)
568 {
569 return Arr::pull($this->items, $key, $default);
570 }
571
572 /**
573 * Put an item in the collection by key.
574 *
575 * @param mixed $key
576 * @param mixed $value
577 *
578 * @return $this
579 */
580 public function put($key, $value)
581 {
582 $this->offsetSet($key, $value);
583
584 return $this;
585 }
586
587 /**
588 * Get one or more items randomly from the collection.
589 *
590 * @param int $amount
591 *
592 * @return mixed
593 *
594 * @throws \InvalidArgumentException
595 */
596 public function random($amount = 1)
597 {
598 if ($amount > ($count = $this->count())) {
599 throw new InvalidArgumentException("You requested {$amount} items, but there are only {$count} items in the collection");
600 }
601
602 $keys = array_rand($this->items, $amount);
603
604 if ($amount == 1) {
605 return $this->items[$keys];
606 }
607
608 return new static(array_intersect_key($this->items, array_flip($keys)));
609 }
610
611 /**
612 * Reduce the collection to a single value.
613 *
614 * @param callable $callback
615 * @param mixed $initial
616 *
617 * @return mixed
618 */
619 public function reduce(callable $callback, $initial = null)
620 {
621 return array_reduce($this->items, $callback, $initial);
622 }
623
624 /**
625 * Create a collection of all elements that do not pass a given truth test.
626 *
627 * @param callable|mixed $callback
628 *
629 * @return static
630 */
631 public function reject($callback)
632 {
633 if ($this->useAsCallable($callback)) {
634 return $this->filter(function ($item) use ($callback) {
635 return !$callback($item);
636 });
637 }
638
639 return $this->filter(function ($item) use ($callback) {
640 return $item != $callback;
641 });
642 }
643
644 /**
645 * Reverse items order.
646 *
647 * @return static
648 */
649 public function reverse()
650 {
651 return new static(array_reverse($this->items));
652 }
653
654 /**
655 * Search the collection for a given value and return the corresponding key if successful.
656 *
657 * @param mixed $value
658 * @param bool $strict
659 *
660 * @return mixed
661 */
662 public function search($value, $strict = false)
663 {
664 if (!$this->useAsCallable($value)) {
665 return array_search($value, $this->items, $strict);
666 }
667
668 foreach ($this->items as $key => $item) {
669 if (call_user_func($value, $item, $key)) {
670 return $key;
671 }
672 }
673
674 return false;
675 }
676
677 /**
678 * Get and remove the first item from the collection.
679 *
680 * @return mixed
681 */
682 public function shift()
683 {
684 return array_shift($this->items);
685 }
686
687 /**
688 * Shuffle the items in the collection.
689 *
690 * @return static
691 */
692 public function shuffle()
693 {
694 $items = $this->items;
695
696 shuffle($items);
697
698 return new static($items);
699 }
700
701 /**
702 * Slice the underlying collection array.
703 *
704 * @param int $offset
705 * @param int $length
706 * @param bool $preserveKeys
707 *
708 * @return static
709 */
710 public function slice($offset, $length = null, $preserveKeys = false)
711 {
712 return new static(array_slice($this->items, $offset, $length, $preserveKeys));
713 }
714
715 /**
716 * Chunk the underlying collection array.
717 *
718 * @param int $size
719 * @param bool $preserveKeys
720 *
721 * @return static
722 */
723 public function chunk($size, $preserveKeys = false)
724 {
725 $chunks = [];
726
727 foreach (array_chunk($this->items, $size, $preserveKeys) as $chunk) {
728 $chunks[] = new static($chunk);
729 }
730
731 return new static($chunks);
732 }
733
734 /**
735 * Sort through each item with a callback.
736 *
737 * @param callable|null $callback
738 *
739 * @return static
740 */
741 public function sort(callable $callback = null)
742 {
743 $items = $this->items;
744
745 $callback ? uasort($items, $callback) : natcasesort($items);
746
747 return new static($items);
748 }
749
750 /**
751 * Sort the collection using the given callback.
752 *
753 * @param callable|string $callback
754 * @param int $options
755 * @param bool $descending
756 *
757 * @return static
758 */
759 public function sortBy($callback, $options = SORT_REGULAR, $descending = false)
760 {
761 $results = [];
762
763 $callback = $this->valueRetriever($callback);
764
765 // First we will loop through the items and get the comparator from a callback
766 // function which we were given. Then, we will sort the returned values and
767 // and grab the corresponding values for the sorted keys from this array.
768 foreach ($this->items as $key => $value) {
769 $results[$key] = $callback($value, $key);
770 }
771
772 $descending ? arsort($results, $options)
773 : asort($results, $options);
774
775 // Once we have sorted all of the keys in the array, we will loop through them
776 // and grab the corresponding model so we can set the underlying items list
777 // to the sorted version. Then we'll just return the collection instance.
778 foreach (array_keys($results) as $key) {
779 $results[$key] = $this->items[$key];
780 }
781
782 return new static($results);
783 }
784
785 /**
786 * Sort the collection in descending order using the given callback.
787 *
788 * @param callable|string $callback
789 * @param int $options
790 *
791 * @return static
792 */
793 public function sortByDesc($callback, $options = SORT_REGULAR)
794 {
795 return $this->sortBy($callback, $options, true);
796 }
797
798 /**
799 * Splice a portion of the underlying collection array.
800 *
801 * @param int $offset
802 * @param int|null $length
803 * @param mixed $replacement
804 *
805 * @return static
806 */
807 public function splice($offset, $length = null, $replacement = [])
808 {
809 if (func_num_args() == 1) {
810 return new static(array_splice($this->items, $offset));
811 }
812
813 return new static(array_splice($this->items, $offset, $length, $replacement));
814 }
815
816 /**
817 * Get the sum of the given values.
818 *
819 * @param callable|string|null $callback
820 *
821 * @return mixed
822 */
823 public function sum($callback = null)
824 {
825 if (is_null($callback)) {
826 return array_sum($this->items);
827 }
828
829 $callback = $this->valueRetriever($callback);
830
831 return $this->reduce(function ($result, $item) use ($callback) {
832 return $result += $callback($item);
833 }, 0);
834 }
835
836 /**
837 * Take the first or last {$limit} items.
838 *
839 * @param int $limit
840 *
841 * @return static
842 */
843 public function take($limit)
844 {
845 if ($limit < 0) {
846 return $this->slice($limit, abs($limit));
847 }
848
849 return $this->slice(0, $limit);
850 }
851
852 /**
853 * Transform each item in the collection using a callback.
854 *
855 * @param callable $callback
856 *
857 * @return $this
858 */
859 public function transform(callable $callback)
860 {
861 $this->items = $this->map($callback)->all();
862
863 return $this;
864 }
865
866 /**
867 * Return only unique items from the collection array.
868 *
869 * @param string|callable|null $key
870 *
871 * @return static
872 */
873 public function unique($key = null)
874 {
875 if (is_null($key)) {
876 return new static(array_unique($this->items, SORT_REGULAR));
877 }
878
879 $key = $this->valueRetriever($key);
880
881 $exists = [];
882
883 return $this->reject(function ($item) use ($key, &$exists) {
884 if (in_array($id = $key($item), $exists)) {
885 return true;
886 }
887
888 $exists[] = $id;
889 });
890 }
891
892 /**
893 * Reset the keys on the underlying array.
894 *
895 * @return static
896 */
897 public function values()
898 {
899 return new static(array_values($this->items));
900 }
901
902 /**
903 * Get a value retrieving callback.
904 *
905 * @param string $value
906 *
907 * @return callable
908 */
909 protected function valueRetriever($value)
910 {
911 if ($this->useAsCallable($value)) {
912 return $value;
913 }
914
915 return function ($item) use ($value) {
916 return data_get($item, $value);
917 };
918 }
919
920 /**
921 * Zip the collection together with one or more arrays.
922 *
923 * e.g. new Collection([1, 2, 3])->zip([4, 5, 6]);
924 * => [[1, 4], [2, 5], [3, 6]]
925 *
926 * @param mixed ...$items
927 *
928 * @return static
929 */
930 public function zip($items)
931 {
932 $arrayableItems = array_map(function ($items) {
933 return $this->getArrayableItems($items);
934 }, func_get_args());
935
936 $params = array_merge([function () {
937 return new static(func_get_args());
938 }, $this->items], $arrayableItems);
939
940 return new static(call_user_func_array('array_map', $params));
941 }
942
943 /**
944 * Get the collection of items as a plain array.
945 *
946 * @return array
947 */
948 public function toArray()
949 {
950 return array_map(function ($value) {
951 return $value instanceof Arrayable ? $value->castToArray() : $value;
952 }, $this->items);
953 }
954
955 /**
956 * {@inheritdoc}
957 */
958 public function castToArray()
959 {
960 return $this->toArray();
961 }
962
963 /**
964 * Convert the object into something JSON serializable.
965 *
966 * @return array
967 */
968 public function jsonSerialize()
969 {
970 return $this->toArray();
971 }
972
973 /**
974 * Get the collection of items as JSON.
975 *
976 * @param int $options
977 *
978 * @return string
979 */
980 public function toJson($options = 0)
981 {
982 return json_encode($this->toArray(), $options);
983 }
984
985 /**
986 * Get an iterator for the items.
987 *
988 * @return \ArrayIterator
989 */
990 public function getIterator()
991 {
992 return new ArrayIterator($this->items);
993 }
994
995 /**
996 * Get a CachingIterator instance.
997 *
998 * @param int $flags
999 *
1000 * @return \CachingIterator
1001 */
1002 public function getCachingIterator($flags = CachingIterator::CALL_TOSTRING)
1003 {
1004 return new CachingIterator($this->getIterator(), $flags);
1005 }
1006
1007 /**
1008 * Count the number of items in the collection.
1009 *
1010 * @return int
1011 */
1012 public function count()
1013 {
1014 return count($this->items);
1015 }
1016
1017 /**
1018 * Determine if an item exists at an offset.
1019 *
1020 * @param mixed $key
1021 *
1022 * @return bool
1023 */
1024 public function offsetExists($key)
1025 {
1026 return array_key_exists($key, $this->items);
1027 }
1028
1029 /**
1030 * Get an item at a given offset.
1031 *
1032 * @param mixed $key
1033 *
1034 * @return mixed
1035 */
1036 public function offsetGet($key)
1037 {
1038 return $this->items[$key];
1039 }
1040
1041 /**
1042 * Set the item at a given offset.
1043 *
1044 * @param mixed $key
1045 * @param mixed $value
1046 */
1047 public function offsetSet($key, $value)
1048 {
1049 if (is_null($key)) {
1050 $this->items[] = $value;
1051 } else {
1052 $this->items[$key] = $value;
1053 }
1054 }
1055
1056 /**
1057 * Unset the item at a given offset.
1058 *
1059 * @param string $key
1060 */
1061 public function offsetUnset($key)
1062 {
1063 unset($this->items[$key]);
1064 }
1065
1066 /**
1067 * Convert the collection to its string representation.
1068 *
1069 * @return string
1070 */
1071 public function __toString()
1072 {
1073 return $this->toJson();
1074 }
1075
1076 /**
1077 * Hack method to render CGridView.
1078 *
1079 * @return boolean
1080 */
1081 public function hasErrors()
1082 {
1083 return false;
1084 }
1085
1086 /**
1087 * Hack method to render CGridView.
1088 *
1089 * @param string $attribute
1090 *
1091 * @return array
1092 */
1093 public function getValidators($attribute)
1094 {
1095 return array();
1096 }
1097
1098 /**
1099 * Set current filter to this collection.
1100 *
1101 * @param array $filters
1102 */
1103 public function setFilters(array $filters)
1104 {
1105 $this->filters = new Fluent($filters);
1106 }
1107
1108 /**
1109 * Results array of items from Collection or Arrayable.
1110 *
1111 * @param mixed $items
1112 *
1113 * @return array
1114 */
1115 protected function getArrayableItems($items)
1116 {
1117 if ($items instanceof self) {
1118 return $items->all();
1119 } elseif ($items instanceof Arrayable) {
1120 return $items->castToArray();
1121 }
1122
1123 return (array) $items;
1124 }
1125
1126 /**
1127 * Method overloading to get current filter attribute. Useful if we want to render CGridView.
1128 *
1129 * @param string $name Filter field name
1130 *
1131 * @return mixed
1132 */
1133 public function __get($name)
1134 {
1135 if ($this->filters) {
1136 return $this->filters->{$name};
1137 }
1138 }
1139 }
1140