Completed
Push — master ( e4da97...0d3405 )
by Luke
06:48 queued 01:32
created

Collection::last()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 4
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
namespace Noz\Collection;
3
4
use Countable;
5
use JsonSerializable;
6
use Iterator;
7
use ArrayAccess;
8
use function Noz\to_array;
9
use RuntimeException;
10
use Traversable;
11
12
use function Noz\is_traversable;
13
14
/**
15
 * Nozavroni Collection
16
 *
17
 * Basically an array wrapper with a bunch of super useful methods for working with its items and/or create new collections from its items.
18
 *
19
 * @note None of the methods in this class have a $preserveKeys param. That is by design. I don't think it's necessary.
20
 *       Instead, keys are ALWAYS preserved and if you want to NOT preserve keys, simply call Collection::values().
21
 */
22
class Collection implements ArrayAccess, Iterator, Countable, JsonSerializable
23
{
24
    /** @var array */
25
    protected $items;
26
27
    /**
28
     * Collection constructor.
29
     *
30
     * @param array $items
31
     */
32 70
    public function __construct(array $items = [])
33
    {
34 70
        $this->items = $items;
35 70
        $this->rewind();
36 70
    }
37
38
    /**
39
     * Generate a collection from an array of items.
40
     * I created this method so that it's possible to extend a collection more easily.
41
     *
42
     * @param array $items
43
     *
44
     * @todo call to_array on $items and don't require input to be an array.
45
     *
46
     * @return Collection
47
     */
48 21
    public static function factory(array $items = [])
49
    {
50 21
        return new Collection($items);
51
    }
52
53
    /**
54
     * Get collection as an array
55
     *
56
     * @return array
57
     */
58 30
    public function toArray()
59
    {
60 30
        return $this->items;
61
    }
62
63
    /**
64
     * Determine if collection has a given key
65
     *
66
     * @param mixed $key The key to look for
67
     *
68
     * @return bool
69
     */
70 38
    public function has($key)
71
    {
72 38
        return isset($this->items[$key]) || array_key_exists($key, $this->items);
73
    }
74
75
    /**
76
     * Does collection have item at position?
77
     *
78
     * Determine if collection has an item at a particular position (indexed from one).
79
     * Position can be positive and start from the beginning or it can be negative and
80
     * start from the end.
81
     *
82
     * @param int $position
83
     *
84
     * @return bool
85
     */
86 2
    public function hasValueAt($position)
87
    {
88
        try {
89 2
            $this->getKeyAt($position);
90 2
            return true;
91 2
        } catch (RuntimeException $e) {
92 2
            return false;
93
        }
94
    }
95
96
    /**
97
     * Get key at given position
98
     *
99
     * Returns the key at the given position, starting from one. Position can be positive (start from beginning) or
100
     * negative (start from the end).
101
     *
102
     * If the position does not exist, a RuntimeException is thrown.
103
     *
104
     * @param int $position
105
     *
106
     * @return string
107
     *
108
     * @throws RuntimeException
109
     */
110 6
    public function getKeyAt($position)
111
    {
112 6
        $collection = $this;
113 6
        if ($position < 0) {
114 3
            $collection = $this->reverse();
115 3
        }
116 6
        $i = 1;
117 6
        foreach ($collection as $key => $val) {
118 6
            if (abs($position) == $i++) {
119 5
                return $key;
120
            }
121 6
        }
122 3
        throw new RuntimeException("No key at position {$position}");
123
    }
124
125
    /**
126
     * Get value at given position
127
     *
128
     * Returns the value at the given position, starting from one. Position can be positive (start from beginning) or
129
     * negative (start from the end).
130
     *
131
     * If the position does not exist, a RuntimeException is thrown.
132
     *
133
     * @param int $position
134
     *
135
     * @return mixed
136
     *
137
     * @throws RuntimeException
138
     */
139 2
    public function getValueAt($position)
140
    {
141 2
        return $this->get($this->getKeyAt($position));
142
    }
143
144
    /**
145
     * Get the key of the first item found matching $item
146
     *
147
     * @todo Perhaps this should allow a callback in place of $item?
148
     *
149
     * @param mixed $item
150
     *
151
     * @return mixed|null
152
     */
153 1
    public function keyOf($item)
154
    {
155 1
        foreach ($this as $key => $val) {
156 1
            if ($item === $val) {
157 1
                return $key;
158
            }
159 1
        }
160
161 1
        return null;
162
    }
163
164
    /**
165
     * Get the offset (index) of the first item found that matches $item
166
     *
167
     * @todo Perhaps this should allow a callback in place of $item?
168
     *
169
     * @param mixed $item
170
     *
171
     * @return int|null
172
     */
173 1
    public function indexOf($item)
174
    {
175 1
        $i = 0;
176 1
        foreach ($this as $key => $val) {
177 1
            if ($item === $val) {
178 1
                return $i;
179
            }
180 1
            $i++;
181 1
        }
182
183 1
        return null;
184
    }
185
186
    /**
187
     * Get item by key, with an optional default return value
188
     *
189
     * @param mixed $key
190
     * @param mixed $default
191
     *
192
     * @return mixed
193
     */
194 8
    public function get($key, $default = null)
195
    {
196 8
        if ($this->has($key)) {
197 8
            return $this->items[$key];
198
        }
199
200 3
        return $default;
201
    }
202
203
    /**
204
     * Add an item with no regard to key
205
     *
206
     * @param mixed $value
207
     *
208
     * @return $this
209
     */
210 4
    public function add($value)
211
    {
212 4
        $this->items[] = $value;
213
214 4
        return $this;
215
    }
216
217
    /**
218
     * Set an item at a given key
219
     *
220
     * @param mixed $key
221
     * @param mixed $value
222
     * @param bool  $overwrite If false, do not overwrite existing key
223
     *
224
     * @return $this
225
     */
226 12
    public function set($key, $value, $overwrite = true)
227
    {
228 12
        if ($overwrite || !$this->has($key)) {
229 12
            $this->items[$key] = $value;
230 12
        }
231
232 12
        return $this;
233
    }
234
235
    /**
236
     * Delete an item by key
237
     *
238
     * @param mixed $key
239
     *
240
     * @return $this
241
     */
242 3
    public function delete($key)
243
    {
244 3
        unset($this->items[$key]);
245
246 3
        return $this;
247
    }
248
249
    /**
250
     * Clear the collection of all its items.
251
     *
252
     * @return $this
253
     */
254 2
    public function clear()
255
    {
256 2
        $this->items = [];
257
258 2
        return $this;
259
    }
260
261
    /**
262
     * Determine if collection contains given value
263
     *
264
     * @param mixed $val
265
     *
266
     * @return bool
267
     */
268 2
    public function contains($val)
269
    {
270 2
        return in_array($val, $this->items, true);
271
    }
272
273
    /**
274
     * Fetch item from collection by key and remove it from collection
275
     *
276
     * @param mixed $key
277
     *
278
     * @return mixed
279
     */
280 1
    public function pull($key)
281
    {
282 1
        if ($this->has($key)) {
283 1
            $value = $this->get($key);
284 1
            $this->delete($key);
285 1
            return $value;
286
        }
287 1
    }
288
289
    /**
290
     * Join collection items using a delimiter
291
     *
292
     * @param string $delim
293
     *
294
     * @return string
295
     */
296 1
    public function join($delim = '')
297
    {
298 1
        return implode($delim, $this->items);
299
    }
300
301
    /**
302
     * Determine if collection has any items
303
     *
304
     * @return bool
305
     */
306 4
    public function isEmpty()
307
    {
308 4
        return $this->count() == 0;
309
    }
310
311
    /**
312
     * Get a collection of only this collection's values (without its keys)
313
     *
314
     * @return Collection
315
     */
316 1
    public function values()
317
    {
318 1
        return static::factory(array_values($this->items));
319
    }
320
321
    /**
322
     * Get a collection of only this collection's keys
323
     *
324
     * @return Collection
325
     */
326 1
    public function keys()
327
    {
328 1
        return static::factory(array_keys($this->items));
329
    }
330
331
    /**
332
     * Get a collection with order reversed
333
     *
334
     * @return Collection
335
     */
336 6
    public function reverse()
337
    {
338 6
        return static::factory(array_reverse($this->items));
339
    }
340
341
    /**
342
     * Get a collection with keys and values flipped
343
     *
344
     * @return Collection
345
     */
346 1
    public function flip()
347
    {
348 1
        $collection = static::factory();
349 1
        foreach ($this as $key => $val) {
350 1
            $collection->set($val, $key);
351 1
        }
352 1
        return $collection;
353
    }
354
355
    /**
356
     * Shuffle the order of this collection's values
357
     *
358
     * @return Collection
359
     */
360 1
    public function shuffle()
361
    {
362 1
        shuffle($this->items);
363 1
        return $this;
364
    }
365
366
    /**
367
     * Get a random value from the collection
368
     *
369
     * @return mixed
370
     */
371 1
    public function random()
372
    {
373 1
        return $this->getValueAt(rand(1, $this->count()));
374
    }
375
376
    /**
377
     * Sort the collection (using values)
378
     *
379
     * @param callable $alg
380
     *
381
     * @return $this
382
     */
383 2
    public function sort(callable $alg = null)
384
    {
385 2
        if (is_null($alg)) {
386
            // case-sensitive string comparison is the default sorting mechanism
387 1
            $alg = 'strcmp';
388 1
        }
389 2
        uasort($this->items, $alg);
390
391 2
        return $this;
392
    }
393
394
    /**
395
     * Sort the collection (using keys)
396
     *
397
     * @param callable $alg
398
     *
399
     * @return $this
400
     */
401 2
    public function ksort(callable $alg = null)
402
    {
403 2
        if (is_null($alg)) {
404
            // case-sensitive string comparison is the default sorting mechanism
405 1
            $alg = 'strcmp';
406 1
        }
407 2
        uksort($this->items, $alg);
408
409 2
        return $this;
410
    }
411
412
    /**
413
     * Append items to collection without regard to keys
414
     *
415
     * @param array|Traversable $items
416
     *
417
     * @return $this
418
     */
419 2
    public function append($items)
420
    {
421 2
        if (!is_traversable($items)) {
422 1
            throw new RuntimeException("Invalid input type for " . __METHOD__ . ", must be array or Traversable");
423
        }
424
425 1
        foreach ($items as $val) {
426 1
            $this->add($val);
427 1
        }
428
429 1
        return $this;
430
    }
431
432
    /**
433
     * Return first item or first item where callback returns true
434
     *
435
     * @param callable|null $callback
436
     *
437
     * @return mixed|null
438
     */
439 7
    public function first(callable $callback = null)
440
    {
441 7
        $index = 0;
442 7
        foreach ($this as $key => $val) {
443 7
            if (is_null($callback) || $callback($val, $key, $index++)) {
444 7
                return $val;
445
            }
446 6
        }
447
448
        return null;
449
    }
450
451
    /**
452
     * Return last item or last item where callback returns true
453
     *
454
     * @param callable|null $callback
455
     *
456
     * @return mixed|null
457
     */
458 3
    public function last(callable $callback = null)
459
    {
460 3
        return $this->reverse()->first($callback);
461
    }
462
463
    /**
464
     * Map collection
465
     *
466
     * Create a new collection using the results of a callback function on each item in this collection.
467
     *
468
     * @param callable $callback
469
     *
470
     * @return Collection
471
     */
472 3
    public function map(callable $callback)
473
    {
474 3
        $collection = static::factory();
475
476 3
        $index = 0;
477 3
        foreach ($this as $key => $val) {
478 3
            $collection->set($key, $callback($val, $key, $index++));
479 3
        }
480
481 3
        return $collection;
482
    }
483
484
    /**
485
     * Combine collection with another traversable/collection
486
     *
487
     * Using this collection's keys, and the incoming collection's values, a new collection is created and returned.
488
     *
489
     * @param array|Traversable $items
490
     *
491
     * @return Collection
492
     */
493 5
    public function combine($items)
494
    {
495 5
        if (!is_traversable($items)) {
496 1
            throw new RuntimeException("Invalid input type for " . __METHOD__ . ", must be array or Traversable");
497
        }
498
499 4
        $items = to_array($items);
500 4
        if (count($items) != count($this->items)) {
501 1
            throw new RuntimeException("Invalid input for " . __METHOD__ . ", number of items does not match");
502
        }
503
504 3
        return static::factory(array_combine($this->items, $items));
505
    }
506
507
    /**
508
     * Get a new collection with only distinct values
509
     *
510
     * @return Collection
511
     */
512 1
    public function distinct()
513
    {
514 1
        $collection = static::factory();
515 1
        foreach ($this as $key => $val) {
516 1
            if (!$collection->contains($val)) {
517 1
                $collection->set($key, $val);
518 1
            }
519 1
        }
520
521 1
        return $collection;
522
    }
523
524
    /**
525
     * Remove all duplicate values from collection in-place
526
     *
527
     * @return Collection
528
     */
529 1
    public function deduplicate()
530
    {
531 1
        $this->items = array_unique($this->items);
532
533 1
        return $this;
534
    }
535
536
    /**
537
     * Return a new collection with only filtered keys/values
538
     *
539
     * The callback accepts value, key, index and should return true if the item should be added to the returned
540
     * collection
541
     *
542
     * @param callable $callback
543
     *
544
     * @return Collection
545
     */
546 1
    public function filter(callable $callback)
547
    {
548 1
        $collection = static::factory();
549 1
        $index = 0;
550 1
        foreach ($this as $key => $value) {
551 1
            if ($callback($value, $key, $index++)) {
552 1
                $collection->set($key, $value);
553 1
            }
554 1
        }
555
556 1
        return $collection;
557
    }
558
559
    /**
560
     * Fold collection into a single value
561
     *
562
     * Loop through collection calling a callback function and passing the result to the next iteration, eventually
563
     * returning a single value.
564
     *
565
     * @param callable $callback
566
     * @param mixed $initial
567
     *
568
     * @return null
569
     */
570 1
    public function fold(callable $callback, $initial = null)
571
    {
572 1
        $index = 0;
573 1
        $folded = $initial;
574 1
        foreach ($this as $key => $val) {
575 1
            $folded = $callback($folded, $val, $key, $index++);
576 1
        }
577
578 1
        return $folded;
579
    }
580
581
    /**
582
     * Return a merge of this collection and $items
583
     *
584
     * @param array|Traversable $items
585
     *
586
     * @return Collection
587
     */
588 3
    public function merge($items)
589
    {
590 3
        if (!is_traversable($items)) {
591 1
            throw new RuntimeException("Invalid input type for " . __METHOD__ . ", must be array or Traversable");
592
        }
593
594 2
        $collection = clone $this;
595 2
        foreach ($items as $key => $val) {
596 2
            $collection->set($key, $val);
597 2
        }
598
599 2
        return $collection;
600
    }
601
602
    /**
603
     * Create a new collection with a union of this collection and $items
604
     *
605
     * This method is similar to merge, except that existing items will not be overwritten.
606
     *
607
     * @param $items
608
     */
609 2
    public function union($items)
610
    {
611 2
        if (!is_traversable($items)) {
612 1
            throw new RuntimeException("Invalid input type for " . __METHOD__ . ", must be array or Traversable");
613
        }
614
615 1
        $collection = clone $this;
616 1
        foreach ($items as $key => $val) {
617 1
            $collection->set($key, $val, false);
618 1
        }
619
620 1
        return $collection;
621
    }
622
623
    /**
624
     * Call callback for each item in collection, passively
625
     *
626
     * @param callable $callback
627
     *
628
     * @return $this
629
     */
630 1
    public function each(callable $callback)
631
    {
632 1
        $index = 0;
633 1
        foreach ($this as $key => $val) {
634 1
            $callback($val, $key, $index++);
635 1
        }
636
637 1
        return $this;
638
    }
639
640
    /**
641
     * Assert callback returns $expected value for each item in collection.
642
     *
643
     * @todo This can be used to easily make methods like all($callback) and none($callback).
644
     *
645
     * @param callable $callback
646
     * @param bool $expected
647
     *
648
     * @return bool
649
     */
650 1
    public function assert(callable $callback, $expected = true)
651
    {
652 1
        $index = 0;
653 1
        foreach ($this as $key => $val) {
654 1
            if ($callback($val, $key, $index++) !== $expected) {
655 1
                return false;
656
            }
657 1
        }
658
659 1
        return true;
660
    }
661
662
    /**
663
     * Pipe collection through a callback
664
     *
665
     * @param callable $callback
666
     *
667
     * @return mixed
668
     */
669 1
    public function pipe(callable $callback)
670
    {
671 1
        return $callback($this);
672
    }
673
674
    /**
675
     * Get new collection in chunks of $size
676
     *
677
     * Creates a new collection of arrays of $size length. The remainder items will be placed at the end.
678
     *
679
     * @param int $size
680
     *
681
     * @return Collection
682
     */
683 2
    public function chunk($size)
684
    {
685 2
        return static::factory(array_chunk($this->items, $size, true));
686
    }
687
688
    /**
689
     * Get a new collection of $count chunks
690
     *
691
     * @todo It might be useful to have a method that spreads remainder items more evenly so you don't end up with the
692
     *       last item containing only one or two items.
693
     *
694
     * @param int $count
695
     *
696
     * @return Collection
697
     */
698 1
    public function slice($count = 1)
699
    {
700 1
        return $this->chunk(ceil($this->count() / $count));
701
    }
702
703
    /** ++++                  ++++ **/
704
    /** ++ Interface Compliance ++ **/
705
    /** ++++                  ++++ **/
706
707
    /**
708
     * @return array
709
     */
710 1
    public function jsonSerialize()
711
    {
712 1
        return $this->toArray();
713
    }
714
715
    /** ++++                  ++++ **/
716
    /** ++ Array Access Methods ++ **/
717
    /** ++++                  ++++ **/
718
719
    /**
720
     * {@inheritDoc}
721
     */
722 1
    public function offsetExists($offset)
723
    {
724 1
        return $this->has($offset);
725
    }
726
727
    /**
728
     * {@inheritDoc}
729
     */
730 2
    public function offsetGet($offset)
731
    {
732 2
        if (!$this->has($offset)) {
733 1
            throw new RuntimeException("Unknown offset: {$offset}");
734
        }
735
736 1
        return $this->get($offset);
737
    }
738
739
    /**
740
     * {@inheritDoc}
741
     */
742 1
    public function offsetUnset($offset)
743
    {
744 1
        $this->delete($offset);
745 1
    }
746
747
    /**
748
     * {@inheritDoc}
749
     */
750 1
    public function offsetSet($offset, $value)
751
    {
752 1
        if (!isset($offset)) {
753 1
            $this->add($value);
754 1
        }
755
756 1
        $this->set($offset, $value);
757 1
    }
758
759
    /** ++++                  ++++ **/
760
    /** ++   Iterator Methods   ++ **/
761
    /** ++++                  ++++ **/
762
763
    /**
764
     * {@inheritDoc}
765
     */
766 27
    public function current()
767
    {
768 27
        return current($this->items);
769
    }
770
771
    /**
772
     * {@inheritDoc}
773
     */
774 27
    public function key()
775
    {
776 27
        return key($this->items);
777
    }
778
779
    /**
780
     * {@inheritDoc}
781
     */
782 26
    public function next()
783
    {
784 26
        return next($this->items);
785
    }
786
787
    /**
788
     * {@inheritDoc}
789
     *
790
     * @todo Should this return $this?
791
     */
792 70
    public function rewind()
793
    {
794 70
        reset($this->items);
795 70
    }
796
797
    /**
798
     * {@inheritDoc}
799
     */
800 27
    public function valid()
801
    {
802 27
        return $this->has(key($this->items));
803
    }
804
805
    /** ++++                  ++++ **/
806
    /** ++   Countable Method   ++ **/
807
    /** ++++                  ++++ **/
808
809
    /**
810
     * {@inheritDoc}
811
     */
812 7
    public function count()
813
    {
814 7
        return count($this->items);
815
    }
816
}