Completed
Push — master ( 694a43...f37ac0 )
by Ivan
02:36
created

Collection::__clone()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 6
ccs 0
cts 5
cp 0
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 4
nc 1
nop 0
crap 2
1
<?php
2
3
namespace vakata\collection;
4
5
class Collection implements \Iterator, \ArrayAccess, \Serializable, \Countable
6
{
7
    protected $array = null;
8
    protected $stack = [];
9
    protected $iterator = null;
10
    protected $key = null;
11
    protected $val = null;
12
13 4
    protected static function rangeGenerator($low, $high, $step = 1)
14
    {
15 4
        $k = -1;
16 4
        for ($i = $low; $i <= $high; $i += $step) {
17 4
            yield ++$k => $i;
18
        }
19 4
    }
20
    /**
21
     * Create a collection based on a range generator
22
     * @param  int|float  $low  start value
23
     * @param  int|float  $high end value
24
     * @param  int|float  $step increment
25
     * @return Collection
26
     */
27 4
    public static function range($low, $high, $step = 1) : Collection
28
    {
29 4
        return new static(static::rangeGenerator($low, $high, $step));
30
    }
31
    /**
32
     * A static alias of the __constructor
33
     * @param  mixed  $input  Anything iterable
34
     * @return Collection
35
     */
36 51
    public static function from($input) : Collection
37
    {
38 51
        return new static($input);
39
    }
40
    /**
41
     * Create an instance
42
     * @param  mixed  $input  Anything iterable
43
     */
44 55
    public function __construct($input = [])
45
    {
46 55
        if (is_object($input)) {
47 28
            if ($input instanceof \Iterator) {
48 5
                $this->array = $input;
49 5
                $this->iterator = $input;
50 23
            } else if ($input instanceof self) {
51
                $this->array = $input->toArray();
52
            } else {
53 23
                $input = get_object_vars($input);
54
            }
55
        }
56 55
        if (is_array($input)) {
57 51
            $this->array = new \ArrayObject($input);
58 51
            $this->iterator = $this->array->getIterator();
59
        }
60 55
    }
61
    public function __clone()
62
    {
63
        $this->array = new \ArrayObject(iterator_to_array($this));
64
        $this->stack = [];
65
        $this->iterator = $this->array->getIterator();
66
    }
67 1
    public function __toString()
68
    {
69 1
        return implode(', ', $this->toArray());
70
    }
71 1
    public function serialize() {
72 1
        return serialize($this->toArray());
73
    }
74 1
    public function unserialize($array) {
75 1
        $this->array = new \ArrayObject(unserialize($array));
76 1
        $this->stack = [];
77 1
        $this->iterator = $this->array->getIterator();
78 1
    }
79
80
    /**
81
     * Applies all pending operations
82
     * @return $this
83
     */
84 45
    public function squash() : Collection
85
    {
86 45
        $this->array = new \ArrayObject(iterator_to_array($this));
87 45
        $this->stack = [];
88 45
        $this->iterator = $this->array->getIterator();
89 45
        return $this;
90
    }
91
    /**
92
     * Get an actual array from the collection
93
     * @return array
94
     */
95 44
    public function toArray() : array
96
    {
97 44
        $this->squash();
98 44
        return $this->array->getArrayCopy();
99
    }
100
    /**
101
     * Gets the first value in the collection or null if empty
102
     * @return mixed
103
     */
104 3
    public function value()
105
    {
106 3
        foreach ($this as $v) {
107 3
            return $v;
108
        }
109 1
        return null;
110
    }
111
112
    // iterator
113 51
    public function key()
114
    {
115 51
        return $this->key;
116
    }
117 54
    public function current()
118
    {
119 54
        return $this->val;
120
    }
121 54
    public function rewind()
122
    {
123 54
        return $this->iterator->rewind();
124
    }
125 53
    public function next()
126
    {
127 53
        return $this->iterator->next();
128
    }
129 54
    public function valid()
130
    {
131 54
        while ($this->iterator->valid()) {
132 54
            $this->val = $this->iterator->current();
133 54
            $this->key = $this->iterator->key();
134 54
            $con = false;
135 54
            foreach ($this->stack as $action) {
136 12
                if ($action[0] === 'filter') {
137 8
                    if (!call_user_func($action[1], $this->val, $this->key, $this)) {
138 8
                        $con = true;
139 8
                        break;
140
                    }
141
                }
142 12
                if ($action[0] === 'map') {
143 5
                    $this->val = call_user_func($action[1], $this->val, $this->key, $this);
144
                }
145 12
                if ($action[0] === 'mapKey') {
146 12
                    $this->key = call_user_func($action[1], $this->val, $this->key, $this);
147
                }
148
            }
149 54
            if ($con) {
150 8
                $this->iterator->next();
151 8
                continue;
152
            }
153 54
            return true;
154
        }
155 54
        return false;
156
    }
157
158
    // array access
159 1
    public function offsetGet($offset)
160
    {
161 1
        if (count($this->stack) || !($this->array instanceof \ArrayAccess)) {
162
            $this->squash();
163
        }
164 1
        return $this->squash()->iterator->offsetGet($offset);
165
    }
166 1
    public function offsetExists($offset)
167
    {
168 1
        if (count($this->stack) || !($this->array instanceof \ArrayAccess)) {
169
            $this->squash();
170
        }
171 1
        return $this->squash()->iterator->offsetExists($offset);
172
    }
173 1
    public function offsetUnset($offset)
174
    {
175 1
        return $this->squash()->iterator->offsetUnset($offset);
176
    }
177 1
    public function offsetSet($offset, $value)
178
    {
179 1
        return $this->squash()->iterator->offsetSet($offset, $value);
180
    }
181
    /**
182
     * Get the collection length
183
     * @return int
184
     */
185 1
    public function count()
186
    {
187 1
        if (count($this->stack) || !($this->array instanceof \Countable)) {
188
            $this->squash();
189
        }
190 1
        return $this->array->count();
191
    }
192
193
    // mutators
194
    /**
195
     * Filter values from the collection based on a predicate. The callback will receive the value, key and collection
196
     * @param  callable $iterator the predicate
197
     * @return $this
198
     */
199 8
    public function filter(callable $iterator) : Collection
200
    {
201 8
        $this->stack[] = [ 'filter', $iterator ];
202 8
        return $this;
203
    }
204
    /**
205
     * Pass all values of the collection through a mutator callable, which will receive the value, key and collection
206
     * @param  callable $iterator the mutator
207
     * @return $this
208
     */
209 5
    public function map(callable $iterator) : Collection
210
    {
211 5
        $this->stack[] = [ 'map', $iterator ];
212 5
        return $this;
213
    }
214
    /**
215
     * Pass all values of the collection through a key mutator callable, which will receive the value, key and collection
216
     * @param  callable $iterator the mutator
217
     * @return $this
218
     */
219 1
    public function mapKey(callable $iterator) : Collection
220
    {
221 1
        $this->stack[] = [ 'mapKey', $iterator ];
222 1
        return $this;
223
    }
224
    /**
225
     * Clone the current collection and return it.
226
     * @return Collection
227
     */
228
    public function clone() : Collection
229
    {
230 1
        return new static($this->toArray());
231
    }
232
    /**
233
     * Remove all falsy values from the collection (uses filter internally).
234
     * @return $this
235
     */
236 1
    public function compact() : Collection
237
    {
238
        return $this->filter(function ($v) {
239 1
            return !!$v;
240 1
        });
241
    }
242
    /**
243
     * Exclude all listed values from the collection (uses filter internally).
244
     * @param  iterable $values the values to exclude
245
     * @return $this
246
     */
247 3
    public function difference($values) : Collection
248
    {
249 3
        if (!is_array($values)) {
250 1
            $values = iterator_to_array($values);
251
        }
252 3
        $keys = array_keys($values);
253 3
        $isAssoc = $keys !== array_keys($keys);
254
        return $this->filter(function ($v, $k) use ($values, $isAssoc) {
255 3
            return $isAssoc ? 
256 1
                ($index = array_search($v, $values)) === false || $index !== $k :
257 3
                !in_array($v, $values, true);
258 3
        });
259
    }
260
    /**
261
     * Append more values to the collection
262
     * @param  iterable $source the values to add
263
     * @return Collection
264
     */
265 2
    public function extend($source) : Collection
266
    {
267 2
        if (!is_array($source)) {
268 1
            $source = iterator_to_array($source);
269
        }
270 2
        return new static(array_merge($this->toArray(), $source));
271
    }
272
    /**
273
     * Append more values to the collection
274
     * @param  iterable $source the values to add
275
     * @return Collection
276
     */
277 1
    public function merge($source) : Collection
278
    {
279 1
        return $this->extend($source);
280
    }
281
    /**
282
     * Perform a shallow flatten of the collection
283
     * @return Collection
284
     */
285 1
    public function flatten() : Collection
286
    {
287 1
        $rslt = [];
288 1
        $temp = $this->toArray();
289 1
        foreach ($temp as $v) {
290 1
            $rslt = array_merge($rslt, is_array($v) ? $v : [$v]);
291
        }
292 1
        return new static($rslt);
293
    }
294
    /**
295
     * Group by a key (if a callable is used - return the value to group by)
296
     * @param  string|callable $iterator the key to group by
297
     * @return Collection
298
     */
299 1
    public function groupBy($iterator) : Collection
300
    {
301 1
        $rslt = [];
302 1
        $temp = $this->toArray();
303 1
        foreach ($temp as $k => $v) {
304 1
            $rslt[is_string($iterator) ? (is_object($v) ? $v->{$iterator} : $v[$iterator]) : call_user_func($iterator, $v, $k)][] = $v;
305
        }
306 1
        return new static($rslt);
307
    }
308
    /**
309
     * Get the first X items from the collection
310
     * @param  int $count the number of items to include (defaults to 1)
311
     * @return Collection
312
     */
313 1
    public function first(int $count = 1) : Collection
314
    {
315 1
        $i = 0;
316 1
        $new = [];
317 1
        foreach ($this as $k => $v) {
318 1
            if (++$i > $count) {
319 1
                break;
320
            }
321 1
            $new[$k] = $v;
322
        }
323 1
        return new static($new);
324
    }
325
    /**
326
     * Get the first X items from the collection
327
     * @param  int $count the number of items to include (defaults to 1)
328
     * @return Collection
329
     */
330 1
    public function head(int $count = 1) : Collection
331
    {
332 1
        return $this->first($count);
333
    }
334
    /**
335
     * Get the last X items from the collection
336
     * @param  int $count the number of items to include (defaults to 1)
337
     * @return Collection
338
     */
339 2
    public function last(int $count = 1) : Collection
340
    {
341 2
        $new = $this->toArray();
342 2
        return new static(array_slice($new, $count * -1));
343
    }
344
    /**
345
     * Get the first X items from the collection
346
     * @param  int $count the number of items to include (defaults to 1)
347
     * @return Collection
348
     */
349 1
    public function tail(int $count = 1) : Collection
350
    {
351 1
        return $this->last($count);
352
    }
353
    /**
354
     * Get all but the last X items from the collection
355
     * @param  int $count the number of items to exclude (defaults to 1)
356
     * @return Collection
357
     */
358 1
    public function initial(int $count = 1) : Collection
359
    {
360 1
        $new = $this->toArray();
361 1
        return new static(array_slice($new, 0, $count * -1));
362
    }
363
    /**
364
     * Get all but the first X items from the collection
365
     * @param  int $count the number of items to exclude (defaults to 1)
366
     * @return Collection
367
     */
368 1
    public function rest(int $count = 1) : Collection
369
    {
370 1
        $new = $this->toArray();
371 1
        return new static(array_slice($new, $count));
372
    }
373
    /**
374
     * Execute a callable for each item in the collection (does not modify the collection)
375
     * @param  callable $iterator the callable to execute
376
     * @return $this
377
     */
378 1
    public function each(callable $iterator) : Collection
379
    {
380 1
        foreach ($this as $k => $v) {
381 1
            call_user_func($iterator, $v, $k, $this);
382
        }
383 1
        return $this;
384
    }
385
    /**
386
     * Execute a callable for each item in the collection (does not modify the collection)
387
     * @param  callable $iterator the callable to execute
388
     * @return $this
389
     */
390 1
    public function invoke(callable $iterator) : Collection
391
    {
392 1
        return $this->each($iterator);
393
    }
394
    /**
395
     * Get all the collection keys
396
     * @return $this
397
     */
398
    public function keys() : Collection
399
    {
400
        return $this->map(function ($v, $k) { return $k; })->values();
401
    }
402
    /**
403
     * Pluck a value from each object (uses map internally)
404
     * @param  string|int $key the key to extract
405
     * @return $this
406
     */
407 2
    public function pluck($key) : Collection
408
    {
409
        return $this->map(function ($v) use ($key) {
410 2
            return is_object($v) ?
411 1
                (isset($v->{$key}) ? $v->{$key} : null) :
412 2
                (isset($v[$key]) ? $v[$key] : null);
413 2
        });
414
    }
415
    /**
416
     * Intersect the collection with another iterable (uses filter internally)
417
     * @param  interable $values the data to intersect with
418
     * @return $this
419
     */
420 1
    public function intersection($values) : Collection
421
    {
422 1
        if (!is_array($values)) {
423 1
            $values = iterator_to_array($values);
424
        }
425 1
        $keys = array_keys($values);
426 1
        $isAssoc = $keys !== array_keys($keys);
427
        return $this->filter(function ($v, $k) use ($values, $isAssoc) {
428 1
            return $isAssoc ? 
429 1
                array_search($v, $values) === $k :
430 1
                in_array($v, $values, true);
431 1
        });
432
    }
433
    /**
434
     * Reject values on a given predicate (opposite of filter)
435
     * @param  callable $iterator the predicate
436
     * @return $this
437
     */
438 1
    public function reject(callable $iterator) : Collection
439
    {
440
        return $this->filter(function ($v, $k, $array) use ($iterator) {
441 1
            return !call_user_func($iterator, $v, $k, $array);
442 1
        });
443
    }
444
    /**
445
     * Shuffle the values in the collection
446
     * @return Collection
447
     */
448 1
    public function shuffle() : Collection
449
    {
450 1
        $temp = $this->toArray();
451 1
        $keys = array_keys($temp);
452 1
        shuffle($keys);
453 1
        $rslt = [];
454 1
        foreach ($keys as $key) {
455 1
            $rslt[$key] = $temp[$key];
456
        }
457 1
        return new static($rslt);
458
    }
459
    /**
460
     * Sort the collection using a standard sorting function
461
     * @param  callable $iterator the sort function (must return -1, 0 or 1)
462
     * @return Collection
463
     */
464 1
    public function sortBy(callable $iterator) : Collection
465
    {
466 1
        $this->squash();
467 1
        $this->array->uasort($iterator);
468 1
        return $this;
469
    }
470
    /**
471
     * Inspect the whole collection (as an array) mid-chain
472
     * @param  callable $iterator the callable to execute
473
     * @return $this
474
     */
475 1
    public function tap(callable $iterator) : Collection
476
    {
477 1
        call_user_func($iterator, $this->toArray());
478 1
        return $this;
479
    }
480
    /**
481
     * Modify the whole collection (as an array) mid-chain
482
     * @param  callable $iterator the callable to execute
483
     * @return Collection
484
     */
485 1
    public function thru(callable $iterator) : Collection
486
    {
487 1
        $temp = $this->toArray();
488 1
        $rslt = call_user_func($iterator, $temp);
489 1
        return new static($rslt);
490
    }
491
    /**
492
     * Leave only unique items in the collection
493
     * @return Collection
494
     */
495 3
    public function unique() : Collection
496
    {
497 3
        $temp = $this->toArray();
498 3
        $rslt = [];
499 3
        foreach ($temp as $k => $v) {
500 3
            if (!in_array($v, $rslt, true)) {
501 3
                $rslt[$k] = $v;
502
            }
503
        }
504 3
        return new static($rslt);
505
    }
506
    /**
507
     * Get only the values of the collection
508
     * @return Collection
509
     */
510 3
    public function values() : Collection
511
    {
512 3
        return new static(array_values($this->toArray()));
513
    }
514
    /**
515
     * Filter items from the collection using key => value pairs
516
     * @param  array   $properties the key => value to check for in each item
517
     * @param  boolean $strict     should the comparison be strict
518
     * @return $this
519
     */
520
    public function where(array $properties, $strict = true) : Collection
521
    {
522 1
        return $this->filter(function ($v) use ($properties, $strict) {
523 1
            foreach ($properties as $key => $value) {
524 1
                $vv = is_object($v) ? (isset($v->{$key}) ? $v->{$key} : null) : (isset($v[$key]) ? $v[$key] : null);
525 1
                if (!$vv || ($strict && $vv !== $value) || (!$strict && $vv != $value)) {
526 1
                    return false;
527
                }
528
            }
529 1
            return true;
530 1
        });
531
    }
532
    /**
533
     * Exclude all listed values from the collection (uses filter internally).
534
     * @param  iterable $values the values to exclude
535
     * @return $this
536
     */
537 2
    public function without($values) : Collection
538
    {
539 2
        return $this->difference($values);
540
    }
541
    /**
542
     * Combine all the values from the collection with a key
543
     * @param  iterable $keys the keys to use
544
     * @return Collection
545
     */
546 2
    public function zip($keys) : Collection
547
    {
548 2
        if (!is_array($keys)) {
549 1
            $keys = iterator_to_array($keys);
550
        }
551 2
        return new static(array_combine($keys, $this->toArray()));
552
    }
553
    /**
554
     * Reverse the collection order
555
     * @return Collection
556
     */
557 1
    public function reverse() : Collection
558
    {
559 1
        return new static(array_reverse($this->toArray()));
560
    }
561
562
    // accessors
563
    /**
564
     * Do all of the items in the collection match a given criteria
565
     * @param  callable $iterator the criteria - should return true / false
566
     * @return bool
567
     */
568 1
    public function all(callable $iterator) : bool
569
    {
570 1
        foreach ($this as $k => $v) {
571 1
            if (!call_user_func($iterator, $v, $k, $this)) {
572 1
                return false;
573
            }
574
        }
575 1
        return true;
576
    }
577
    /**
578
     * Do any of the items in the collection match a given criteria
579
     * @param  callable $iterator the criteria - should return true / false
580
     * @return bool
581
     */
582 1
    public function any(callable $iterator) : bool
583
    {
584 1
        foreach ($this as $k => $v) {
585 1
            if (call_user_func($iterator, $v, $k, $this)) {
586 1
                return true;
587
            }
588
        }
589 1
        return false;
590
    }
591
    /**
592
     * Does the collection contain a given value
593
     * @param  mixed $needle the value to check for
594
     * @return bool
595
     */
596 1
    public function contains($needle) : bool
597
    {
598 1
        foreach ($this as $k => $v) {
599 1
            if ($v === $needle) {
600 1
                return true;
601
            }
602
        }
603 1
        return false;
604
    }
605
    /**
606
     * Get the first element matching a given criteria (or null)
607
     * @param  callable $iterator the filter criteria
608
     * @return mixed
609
     */
610 1
    public function find(callable $iterator)
611
    {
612 1
        foreach ($this as $k => $v) {
613 1
            if (call_user_func($iterator, $v, $k, $this)) {
614 1
                return $v;
615
            }
616
        }
617 1
        return null;
618
    }
619
    /**
620
     * Get all the elements matching a given criteria (with the option to limit the number of results)
621
     * @param  callable $iterator the search criteria
622
     * @param  int|null $limit    optional limit to the number of results (default to null - no limit)
623
     * @return Collection
624
     */
625 1
    public function findAll(callable $iterator, int $limit = null) : Collection
626
    {
627 1
        $res = [];
628 1
        foreach ($this as $k => $v) {
629 1
            if (call_user_func($iterator, $v, $k, $this)) {
630 1
                $res[] = $v;
631
            }
632 1
            if ((int)$limit > 0 && count($res) >= $limit) {
633 1
                break;
634
            }
635
        }
636 1
        return new static($res);
637
    }
638
    /**
639
     * Get the key corresponding to a value (or false)
640
     * @param  mixed  $needle the value to search for
641
     * @return mixed
642
     */
643 1
    public function indexOf($needle)
644
    {
645 1
        return array_search($needle, $this->toArray(), true);
646
    }
647
    /**
648
     * Get the last key corresponding to a value (or false)
649
     * @param  mixed  $needle the value to search for
650
     * @return mixed
651
     */
652 1
    public function lastIndexOf($needle)
653
    {
654 1
        $res = null;
655 1
        foreach ($this as $k => $v) {
656 1
            if ($v === $needle) {
657 1
                $res = $k;
658
            }
659
        }
660 1
        return $res;
661
    }
662
    /**
663
     * Get the number of elements in the collection
664
     * @return int
665
     */
666 1
    public function size() : int
667
    {
668 1
        return $this->count();
669
    }
670
    /**
671
     * Get the minimal item in the collection
672
     * @return mixed
673
     */
674 1
    public function min()
675
    {
676 1
        $min = null;
677 1
        $first = false;
678 1
        foreach ($this as $v) {
679 1
            if (!$first || $v < $min) {
680 1
                $min = $v;
681 1
                $first = true;
682
            }
683
        }
684 1
        return $min;
685
    }
686
    /**
687
     * Get the maximum item in the collection
688
     * @return mixed
689
     */
690 1
    public function max()
691
    {
692 1
        $max = null;
693 1
        $first = false;
694 1
        foreach ($this as $v) {
695 1
            if (!$first || $v > $max) {
696 1
                $max = $v;
697 1
                $first = true;
698
            }
699
        }
700 1
        return $max;
701
    }
702
    /**
703
     * Does the collection contain a given key
704
     * @param  string|int  $key the key to check
705
     * @return bool
706
     */
707 1
    public function has($key) : bool
708
    {
709 1
        return $this->offsetExists($key);
710
    }
711
    /**
712
     * Reduce the collection to a single value
713
     * @param  callable $iterator the reducer
714
     * @param  mixed    $initial  the initial value
715
     * @return mixed the final value
716
     */
717 1
    public function reduce(callable $iterator, $initial = null)
718
    {
719 1
        return array_reduce($this->toArray(), $iterator, $initial);
720
    }
721
    /**
722
     * Reduce the collection to a single value, starting from the last element
723
     * @param  callable $iterator the reducer
724
     * @param  mixed    $initial  the initial value
725
     * @return mixed the final value
726
     */
727 1
    public function reduceRight(callable $iterator, $initial = null)
728
    {
729 1
        return array_reduce(array_reverse($this->toArray()), $iterator, $initial);
730
    }
731
}
732