Completed
Pull Request — master (#49)
by Luke
02:09
created

AbstractCollection::exclude()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 7
nc 3
nop 1
dl 0
loc 11
rs 9.4285
c 0
b 0
f 0
1
<?php
2
/*
3
 * Nozavroni/Collections
4
 * Just another collections library for PHP5.6+.
5
 *
6
 * @copyright Copyright (c) 2016 Luke Visinoni <[email protected]>
7
 * @author    Luke Visinoni <[email protected]>
8
 * @license   https://github.com/nozavroni/collections/blob/master/LICENSE The MIT License (MIT)
9
 */
10
namespace Noz\Collection;
11
12
use ArrayAccess;
13
use ArrayIterator;
14
use Countable;
15
use InvalidArgumentException;
16
use Iterator;
17
use Noz\Contracts\ArrayableInterface;
18
use Noz\Contracts\CollectionInterface;
19
use OutOfBoundsException;
20
21
use Noz\Traits\IsArrayable;
22
23
use function
24
    Noz\is_traversable,
25
    Noz\typeof,
26
    Noz\collect;
27
28
/**
29
 * Class AbstractCollection.
30
 *
31
 * This is the abstract class that all other collection classes are based on.
32
 * Although it's possible to use a completely custom Collection class by simply
33
 * implementing the "Collectable" interface, extending this class gives you a
34
 * whole slew of convenient methods for free.
35
 *
36
 * @package Noz\Collection
37
 *
38
 * @author Luke Visinoni <[email protected]>
39
 * @copyright Copyright (c) 2016 Luke Visinoni <[email protected]>
40
 *
41
 * @todo Implement Serializable, other Interfaces
42
 */
43
abstract class AbstractCollection implements
44
    CollectionInterface,
45
    ArrayableInterface,
46
    ArrayAccess,
47
    Countable,
48
    Iterator
49
{
50
    use IsArrayable;
51
52
    /**
53
     * @var array The collection of data this object represents
54
     */
55
    protected $data = [];
56
57
    /**
58
     * @var bool True unless we have advanced past the end of the data array
59
     */
60
    protected $isValid = true;
61
62
    /**
63
     * AbstractCollection constructor.
64
     *
65
     * @param mixed $data The data to wrap
66
     */
67
    public function __construct($data = [])
68
    {
69
        $this->setData($data);
70
    }
71
72
    /**
73
     * Set collection data.
74
     *
75
     * Sets the collection data.
76
     *
77
     * @param array $data The data to wrap
78
     *
79
     * @return $this
80
     */
81
    protected function setData($data)
82
    {
83
        if (is_null($data)) {
84
            $data = [];
85
        }
86
        $this->assertCorrectInputDataType($data);
87
        $data = $this->prepareData($data);
88
        foreach ($data as $index => $value) {
89
            $this->set($index, $value);
90
        }
91
        reset($this->data);
92
93
        return $this;
94
    }
95
96
    /**
97
     * Assert input data is of the correct structure.
98
     *
99
     * @param mixed $data Data to check
100
     *
101
     * @throws InvalidArgumentException If invalid data structure
102
     */
103
    protected function assertCorrectInputDataType($data)
104
    {
105
        // @todo this is thrown whenever a collection is instantiated with the wrong data type
106
        // it is not the right message usually... fix it.
107
        if (!$this->isConsistentDataStructure($data)) {
108
            throw new InvalidArgumentException(__CLASS__ . ' expected traversable data, got: ' . gettype($data));
109
        }
110
    }
111
112
    /**
113
     * Determine whether data is consistent with a given collection type.
114
     *
115
     * This method is used to determine whether input data is consistent with a
116
     * given collection type. For instance,
117
     * NumericCollection requires an array or traversable set of numeric data.
118
     * TabularCollection requires a two-dimensional data structure where all the
119
     * keys are the same in every row.
120
     *
121
     * @param mixed $data Data structure to check for consistency
122
     *
123
     * @return bool
124
     */
125
    abstract protected function isConsistentDataStructure($data);
126
127
    /**
128
     * Convert input data to an array.
129
     *
130
     * Convert the input data to an array that can be worked with by a collection.
131
     *
132
     * @param mixed $data The input data
133
     *
134
     * @return array
135
     */
136
    abstract protected function prepareData($data);
137
138
    /**
139
     * Invoke object.
140
     *
141
     * Magic "invoke" method. Called when object is invoked as if it were a function.
142
     *
143
     * @param mixed $val   The value (depends on other param value)
144
     * @param mixed $index The index (depends on other param value)
145
     *
146
     * @return array|CollectionInterface (Depends on parameter values)
147
     */
148
    public function __invoke($val = null, $index = null)
149
    {
150
        if (is_null($val)) {
151
            if (is_null($index)) {
152
                return $this->toArray();
153
            }
154
155
            return $this->delete($index);
156
        }
157
        if (is_null($index)) {
158
            // @todo cast $val to array?
159
                return $this->merge($val);
160
        }
161
162
        return $this->set($val, $index);
163
    }
164
165
    /**
166
     * Whether a offset exists.
167
     *
168
     * @param mixed $offset An offset to check for.
169
     *
170
     * @return bool true on success or false on failure.
171
     *
172
     * @see http://php.net/manual/en/arrayaccess.offsetexists.php
173
     */
174
    public function offsetExists($offset)
175
    {
176
        return $this->has($offset);
177
    }
178
179
    /**
180
     * Offset to retrieve.
181
     *
182
     * @param mixed $offset The offset to retrieve.
183
     *
184
     * @return mixed Can return all value types.
185
     *
186
     * @see http://php.net/manual/en/arrayaccess.offsetget.php
187
     */
188
    public function offsetGet($offset)
189
    {
190
        return $this->retrieve($offset);
191
    }
192
193
    /**
194
     * Offset to set.
195
     *
196
     * @param mixed $offset The offset to assign the value to.
197
     * @param mixed $value  The value to set.
198
     *
199
     * @see http://php.net/manual/en/arrayaccess.offsetset.php
200
     */
201
    public function offsetSet($offset, $value)
202
    {
203
        $this->set($offset, $value);
204
    }
205
206
    /**
207
     * Offset to unset.
208
     *
209
     * @param mixed $offset The offset to unset.
210
     *
211
     * @see http://php.net/manual/en/arrayaccess.offsetunset.php
212
     */
213
    public function offsetUnset($offset)
214
    {
215
        $this->delete($offset);
216
    }
217
218
    /**
219
     * Get count.
220
     * If a callback is supplied, this method will return the number of items that cause the callback to return true.
221
     * Otherwise, all items in the collection will be counted.
222
     *
223
     * @param callable $callback The (optional) callback function
0 ignored issues
show
Documentation introduced by
Should the type for parameter $callback not be null|callable? Also, consider making the array more specific, something like array<String>, or String[].

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive. In addition it looks for parameters that have the generic type array and suggests a stricter type like array<String>.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
224
     *
225
     * @return int
226
     */
227
    public function count(callable $callback = null)
228
    {
229
        if (!is_null($callback)) {
230
            return $this->filter($callback)->count();
231
        }
232
        return count($this->data);
233
    }
234
235
    /**
236
     * Return the current element.
237
     *
238
     * Returns the current element in the collection. The internal array pointer
239
     * of the data array wrapped by the collection should not be advanced by this
240
     * method. No side effects. Return current element only.
241
     *
242
     * @return mixed
243
     */
244
    public function current()
245
    {
246
        return current($this->data);
247
    }
248
249
    /**
250
     * Return the current key.
251
     *
252
     * Returns the current key in the collection. No side effects.
253
     *
254
     * @return mixed
255
     */
256
    public function key()
257
    {
258
        return key($this->data);
259
    }
260
261
    /**
262
     * Advance the internal pointer forward.
263
     *
264
     * Although this method will return the current value after advancing the
265
     * pointer, you should not expect it to. The interface does not require it
266
     * to return any value at all.
267
     *
268
     * @return mixed
269
     */
270
    public function next()
271
    {
272
        $next = next($this->data);
273
        $key  = key($this->data);
274
        if (isset($key)) {
275
            return $next;
276
        }
277
        $this->isValid = false;
278
    }
279
280
    /**
281
     * Rewind the internal pointer.
282
     *
283
     * Return the internal pointer to the first element in the collection. Again,
284
     * this method is not required to return anything by its interface, so you
285
     * should not count on a return value.
286
     *
287
     * @return mixed
288
     */
289
    public function rewind()
290
    {
291
        $this->isValid = !empty($this->data);
292
293
        return reset($this->data);
294
    }
295
296
    /**
297
     * Is internal pointer in a valid position?
298
     *
299
     * If the internal pointer is advanced beyond the end of the collection, this method will return false.
300
     *
301
     * @return bool True if internal pointer isn't past the end
302
     */
303
    public function valid()
304
    {
305
        return $this->isValid;
306
    }
307
308
    public function sort($alg = null)
309
    {
310
        if (is_null($alg)) {
311
            $alg = 'natcasesort';
312
        }
313
        $alg($this->data);
314
315
        return collect($this->data);
316
    }
317
318
    /**
319
     * Does this collection have a value at given index?
320
     *
321
     * @param mixed $index The index to check
322
     *
323
     * @return bool
324
     */
325
    public function has($index)
326
    {
327
        return array_key_exists($index, $this->data);
328
    }
329
330
    /**
331
     * Set a value at a given index.
332
     *
333
     * Setter for this collection. Allows setting a value at a given index.
334
     *
335
     * @param mixed $index The index to set a value at
336
     * @param mixed $val   The value to set $index to
337
     *
338
     * @return $this
339
     */
340
    public function set($index, $val)
341
    {
342
        $this->data[$index] = $val;
343
344
        return $this;
345
    }
346
347
    /**
348
     * Unset a value at a given index.
349
     *
350
     * Unset (delete) value at the given index.
351
     *
352
     * @param mixed $index The index to unset
353
     * @param bool  $throw True if you want an exception to be thrown if no data found at $index
354
     *
355
     * @throws OutOfBoundsException If $throw is true and $index isn't found
356
     *
357
     * @return $this
358
     */
359
    public function delete($index, $throw = false)
360
    {
361
        if (isset($this->data[$index])) {
362
            unset($this->data[$index]);
363
        } else {
364
            if ($throw) {
365
                throw new OutOfBoundsException('No value found at given index: ' . $index);
366
            }
367
        }
368
369
        return $this;
370
    }
371
372
    /**
373
     * Get index of a value.
374
     *
375
     * Given a value, this method will return the index of the first occurrence of that value.
376
     *
377
     * @param mixed $value Value to get the index of
378
     * @param bool  $throw Whether to throw an exception if value isn't found
379
     *
380
     * @return int|null|string
381
     */
382
    public function indexOf($value, $throw = true)
383
    {
384
        $return = null;
385
        $this->first(function($val, $key) use (&$return, $value) {
386
            if ($val == $value) {
387
                $return = $key;
388
                return true;
389
            }
390
        });
391
        if ($throw && is_null($return)) {
392
            throw new OutOfBoundsException(sprintf(
393
                'Value "%s" not found in collection.',
394
                $value
395
            ));
396
        }
397
        return $return;
398
    }
399
400
    /**
401
     * Get this collection's keys as a collection.
402
     *
403
     * @return CollectionInterface Containing this collection's keys
404
     */
405
    public function keys()
406
    {
407
        return collect(array_keys($this->data));
408
    }
409
410
    /**
411
     * Get this collection's values as a collection.
412
     *
413
     * This method returns this collection's values but completely re-indexed (numerically).
414
     *
415
     * @return CollectionInterface Containing this collection's values
416
     */
417
    public function values()
418
    {
419
        return collect(array_values($this->data));
420
    }
421
422
    /**
423
     * Merge data into collection.
424
     *
425
     * Merges input data into this collection. Input can be an array or another collection.
426
     * Returns a NEW collection object.
427
     *
428
     * @param Traversable|array $data The data to merge with this collection
429
     *
430
     * @return CollectionInterface A new collection with $data merged in
431
     */
432
    public function merge($data)
433
    {
434
        $this->assertCorrectInputDataType($data);
435
        $coll = collect($this->data);
436
        foreach ($data as $index => $value) {
437
            $coll->set($index, $value);
438
        }
439
440
        return $coll;
441
    }
442
443
    /**
444
     * Determine if this collection contains a value.
445
     *
446
     * Allows you to pass in a value or a callback function and optionally an index,
447
     * and tells you whether or not this collection contains that value.
448
     * If the $index param is specified, only that index will be looked under.
449
     *
450
     * @param mixed|callable $value The value to check for
451
     * @param mixed          $index The (optional) index to look under
452
     *
453
     * @return bool True if this collection contains $value
454
     *
455
     * @todo Maybe add $identical param for identical comparison (===)
456
     * @todo Allow negative offset for second param
457
     */
458
    public function contains($value, $index = null)
459
    {
460
        return (bool) $this->first(function ($val, $key) use ($value, $index) {
461
            if (is_callable($value)) {
462
                $found = $value($val, $key);
463
            } else {
464
                $found = ($value == $val);
465
            }
466
            if ($found) {
467
                if (is_null($index)) {
468
                    return true;
469
                }
470
                if (is_array($index)) {
471
                    return in_array($key, $index);
472
                }
473
474
                return $key == $index;
475
            }
476
477
            return false;
478
        });
479
    }
480
481
    /**
482
     * Pop an element off the end of this collection.
483
     *
484
     * @return mixed The last item in this collectio n
485
     */
486
    public function pop()
487
    {
488
        return array_pop($this->data);
489
    }
490
491
    /**
492
     * Shift an element off the beginning of this collection.
493
     *
494
     * @return mixed The first item in this collection
495
     */
496
    public function shift()
497
    {
498
        return array_shift($this->data);
499
    }
500
501
    /**
502
     * Pad this collection to a certain size.
503
     *
504
     * Returns a new collection, padded to the given size, with the given value.
505
     *
506
     * @param int   $size The number of items that should be in the collection
507
     * @param mixed $with The value to pad the collection with
508
     *
509
     * @return CollectionInterface A new collection padded to specified length
510
     */
511
    public function pad($size, $with = null)
512
    {
513
        return collect(array_pad($this->data, $size, $with));
514
    }
515
516
    /**
517
     * Apply a callback to each item in collection.
518
     *
519
     * Applies a callback to each item in collection and returns a new collection
520
     * containing each iteration's return value.
521
     *
522
     * @param callable $callback The callback to apply
523
     *
524
     * @return CollectionInterface A new collection with callback return values
525
     */
526
    public function map(callable $callback)
527
    {
528
        $iter = 0;
529
        $transform = [];
530
        foreach ($this as $key => $val) {
531
            $transform[$key] = $callback($val, $key, $iter++);
532
        }
533
        return collect($transform);
534
    }
535
536
    /**
537
     * Apply a callback to each item in collection.
538
     *
539
     * Applies a callback to each item in collection. The callback should return
540
     * false to filter any item from the collection.
541
     *
542
     * @param callable $callback     The callback function
543
     * @param null     $extraContext Extra context to pass as third param in callback
544
     *
545
     * @return $this
546
     *
547
     * @see php.net array_walk
548
     */
549
    public function walk(callable $callback, $extraContext = null)
550
    {
551
        array_walk($this->data, $callback, $extraContext);
552
553
        return $this;
554
    }
555
556
    /**
557
     * Iterate over each item in the collection, calling $callback on it. Return false to stop iterating.
558
     *
559
     * @param callable    $callback A callback to use
560
     *
561
     * @return $this
562
     */
563
    public function each(callable $callback)
564
    {
565
        foreach ($this as $key => $val) {
566
            if (!$callback($val, $key)) {
567
                break;
568
            }
569
        }
570
571
        return $this;
572
    }
573
574
    /**
575
     * Filter the collection.
576
     *
577
     * Using a callback function, this method will filter out unwanted values, returning
578
     * a new collection containing only the values that weren't filtered.
579
     *
580
     * @param callable $callback The callback function used to filter
581
     *
582
     * @return CollectionInterface A new collection with only values that weren't filtered
583
     */
584
    public function filter(callable $callback)
585
    {
586
        $iter = 0;
587
        $filtered = [];
588
        foreach ($this as $key => $val) {
589
            if ($callback($val, $key, $iter++)) {
590
                $filtered[$key] = $val;
591
            }
592
        }
593
        return collect($filtered);
594
    }
595
596
    /**
597
     * Filter the collection.
598
     *
599
     * Using a callback function, this method will filter out unwanted values, returning
600
     * a new collection containing only the values that weren't filtered.
601
     *
602
     * @param callable $callback The callback function used to filter
603
     *
604
     * @return CollectionInterface A new collection with only values that weren't filtered
605
     */
606
    public function exclude(callable $callback)
607
    {
608
        $iter = 0;
609
        $filtered = [];
610
        foreach ($this as $key => $val) {
611
            if (!$callback($val, $key, $iter++)) {
612
                $filtered[$key] = $val;
613
            }
614
        }
615
        return collect($filtered);
616
    }
617
618
    /**
619
     * Return the first item that meets given criteria.
620
     *
621
     * Using a callback function, this method will return the first item in the collection
622
     * that causes the callback function to return true.
623
     *
624
     * @param callable|null $callback The callback function
625
     * @param mixed|null    $default  The default return value
626
     *
627
     * @return mixed
628
     */
629
    public function first(callable $callback = null, $default = null)
630
    {
631
        if (is_null($callback)) {
632
            return $this->getOffset(0);
633
        }
634
635
        foreach ($this as $index => $value) {
636
            if ($callback($value, $index)) {
637
                return $value;
638
            }
639
        }
640
641
        return $default;
642
    }
643
644
    /**
645
     * Return the last item that meets given criteria.
646
     *
647
     * Using a callback function, this method will return the last item in the collection
648
     * that causes the callback function to return true.
649
     *
650
     * @param callable|null $callback The callback function
651
     * @param mixed|null    $default  The default return value
652
     *
653
     * @return mixed
654
     */
655
    public function last(callable $callback = null, $default = null)
656
    {
657
        $reverse = $this->reverse();
658
        if (is_null($callback)) {
659
            return $reverse->getOffset(0);
660
        }
661
        return $reverse->first($callback);
662
    }
663
664
    /**
665
     * Returns collection in reverse order.
666
     *
667
     * @return CollectionInterface This collection in reverse order.
668
     */
669
    public function reverse()
670
    {
671
        return collect(array_reverse($this->data, true));
672
    }
673
674
    /**
675
     * Get unique items.
676
     *
677
     * Returns a collection of all the unique items in this collection.
678
     *
679
     * @return CollectionInterface This collection with duplicate items removed
680
     */
681
    public function unique()
682
    {
683
        return collect(array_unique($this->data));
684
    }
685
686
    /**
687
     * Collection factory method.
688
     *
689
     * This method will analyze input data and determine the most appropriate Collection
690
     * class to use. It will then instantiate said Collection class with the given
691
     * data and return it.
692
     *
693
     * @param mixed $data The data to wrap
694
     *
695
     * @return CollectionInterface A collection containing $data
696
     */
697
    public static function factory($data = null)
698
    {
699
        if (static::isAllObjects($data)) {
700
            $class = ObjectCollection::class;
701
        } elseif (static::isTabular($data)) {
702
            $class = TabularCollection::class;
703
        } elseif (static::isMultiDimensional($data)) {
704
            $class = MultiCollection::class;
705
        } elseif (static::isAllNumeric($data)) {
706
            $class = NumericCollection::class;
707
        } else {
708
            $class = Collection::class;
709
        }
710
711
        return new $class($data);
712
    }
713
714
    /**
715
     * Is data structure all objects?
716
     *
717
     * Does the data structure passed in contain only objects?
718
     *
719
     * @param mixed $data The data structure to check
720
     *
721
     * @return bool
722
     */
723
    public static function isAllObjects($data)
724
    {
725
        if (!is_traversable($data) || empty($data)) {
726
            return false;
727
        }
728
        foreach ($data as $value) {
729
            if (!is_object($value)) {
730
                return false;
731
            }
732
        }
733
734
        return true;
735
    }
736
737
    /**
738
     * Is input data tabular?
739
     *
740
     * Returns true if input data is tabular in nature. This means that it is a
741
     * two-dimensional array with the same keys (columns) for each element (row).
742
     *
743
     * @param mixed $data The data structure to check
744
     *
745
     * @return bool True if data structure is tabular
746
     */
747
    public static function isTabular($data)
748
    {
749
        if (!is_traversable($data) || empty($data)) {
750
            return false;
751
        }
752
        foreach ($data as $row) {
753
            if (!is_traversable($row)) {
754
                return false;
755
            }
756
            $columns = array_keys($row);
757
            if (!isset($cmp_columns)) {
758
                $cmp_columns = $columns;
759
            } else {
760
                if ($cmp_columns != $columns) {
761
                    return false;
762
                }
763
            }
764
            // if row contains an array it isn't tabular
765
            if (array_reduce($row, function ($carry, $item) {
766
                return is_array($item) && $carry;
767
            }, true)) {
768
                return false;
769
            }
770
        }
771
772
        return true;
773
    }
774
775
    /**
776
     * Check data for multiple dimensions.
777
     *
778
     * This method is to determine whether a data structure is multi-dimensional.
779
     * That is to say, it is a traversable structure that contains at least one
780
     * traversable structure.
781
     *
782
     * @param mixed $data The input data
783
     *
784
     * @return bool
785
     */
786
    public static function isMultiDimensional($data)
787
    {
788
        if (!is_traversable($data) || empty($data)) {
789
            return false;
790
        }
791
        foreach ($data as $elem) {
792
            if (is_traversable($elem)) {
793
                return true;
794
            }
795
        }
796
797
        return false;
798
    }
799
800
    /**
801
     * Determine if structure contains all numeric values.
802
     *
803
     * @param mixed $data The input data
804
     *
805
     * @return bool
806
     */
807
    public static function isAllNumeric($data)
808
    {
809
        if (!is_traversable($data) || empty($data)) {
810
            return false;
811
        }
812
        foreach ($data as $val) {
813
            if (!is_numeric($val)) {
814
                return false;
815
            }
816
        }
817
818
        return true;
819
    }
820
821
    /**
822
     * Is data a string of characters?
823
     *
824
     * Just checks to see if input is a string of characters or a string
825
     * of digits.
826
     *
827
     * @param mixed $data Data to check
828
     *
829
     * @return bool
830
     */
831
    public static function isCharacterSet($data)
832
    {
833
        return
834
            is_string($data) ||
835
            is_numeric($data);
836
    }
837
838
    /**
839
     * @inheritdoc
840
     */
841
    public function hasOffset($offset)
842
    {
843
        try {
844
            $this->getOffsetKey($offset);
845
            return true;
846
        } catch (OutOfBoundsException $e) {
847
            return false;
848
        }
849
    }
850
851
    /**
852
     * @inheritdoc
853
     */
854
    public function getOffsetKey($offset)
855
    {
856
        if (!is_null($key = $this->foldRight(function($val, $carry, $key, $iter) use ($offset) {
857
            return ($iter === $offset) ? $key : $carry;
858
        }))) {
859
            return $key;
860
        }
861
        throw new OutOfBoundsException("Offset does not exist: $offset");
862
    }
863
864
    /**
865
     * @inheritdoc
866
     */
867
    public function getOffset($offset)
868
    {
869
        return $this->retrieve($this->getOffsetKey($offset));
870
    }
871
872
    /**
873
     * @param int $pos The numerical position
874
     *
875
     * @throws OutOfBoundsException if no pair at position
876
     *
877
     * @return array
878
     */
879
    public function getPairAtPosition($pos)
880
    {
881
        $pairs = $this->pairs();
882
883
        return $pairs[$this->getKeyAtPosition($pos)];
0 ignored issues
show
Bug introduced by
The method getKeyAtPosition() does not seem to exist on object<Noz\Collection\AbstractCollection>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
884
    }
885
886
    /**
887
     * Get each key/value as an array pair.
888
     *
889
     * Returns a collection of arrays where each item in the collection is [key,value]
890
     *
891
     * @return CollectionInterface
892
     */
893
    public function pairs()
894
    {
895
        return collect(array_map(
896
            function ($key, $val) {
897
                return [$key, $val];
898
            },
899
            array_keys($this->data),
900
            array_values($this->data)
901
        ));
902
    }
903
904
    /**
905
     * Get duplicate values.
906
     *
907
     * Returns a collection of arrays where the key is the duplicate value
908
     * and the value is an array of keys from the original collection.
909
     *
910
     * @return CollectionInterface A new collection with duplicate values.
911
     */
912
    public function duplicates()
913
    {
914
        $dups = [];
915
        $this->walk(function ($val, $key) use (&$dups) {
916
            $dups[$val][] = $key;
917
        });
918
919
        return collect($dups)->filter(function ($val) {
920
            return count($val) > 1;
921
        });
922
    }
923
924
    // END Iterator methods
925
926
    /**
927
     * Counts how many times each value occurs in a collection.
928
     *
929
     * Returns a new collection with values as keys and how many times that
930
     * value appears in the collection. Works best with scalar values but will
931
     * attempt to work on collections of objects as well.
932
     *
933
     * @return CollectionInterface
934
     *
935
     * @todo Right now, collections of arrays or objects are supported via the
936
     * __toString() or spl_object_hash()
937
     * @todo NumericCollection::counts() does the same thing...
938
     */
939
    public function frequency()
940
    {
941
        $frequency = [];
942
        foreach ($this as $key => $val) {
943
            if (!is_scalar($val)) {
944
                if (!is_object($val)) {
945
                    $val = new ArrayIterator($val);
946
                }
947
948
                if (method_exists($val, '__toString')) {
949
                    $val = (string) $val;
950
                } else {
951
                    $val = spl_object_hash($val);
952
                }
953
            }
954
            if (!isset($frequency[$val])) {
955
                $frequency[$val] = 0;
956
            }
957
            $frequency[$val]++;
958
        }
959
960
        return collect($frequency);
961
    }
962
963
    /**
964
     * @inheritDoc
965
     */
966
    public function add($index, $value)
967
    {
968
        if (!$this->has($index)) {
969
            return $this->set($index, $value);
970
        }
971
        return $this;
972
    }
973
974
    /**
975
     * @inheritdoc
976
     */
977
    public function get($index, $default = null)
978
    {
979
        try {
980
            return $this->retrieve($index);
981
        } catch (OutOfBoundsException $e) {
982
            return $default;
983
        }
984
    }
985
986
    /**
987
     * @inheritdoc
988
     */
989
    public function retrieve($index)
990
    {
991
        if (!$this->has($index)) {
992
            throw new OutOfBoundsException(__CLASS__ . ' could not retrieve value at index ' . $index);
993
        }
994
        return $this->data[$index];
995
    }
996
997
    /**
998
     * @inheritDoc
999
     */
1000
    public function take($index)
1001
    {
1002
        try {
1003
            $item = $this->retrieve($index);
1004
            $this->data = $this->except([$index])->toArray();
1005
        } catch (OutOfBoundsException $e) {
1006
            // do nothing...
1007
            $item = null;
1008
        }
1009
        return $item;
1010
    }
1011
1012
    /**
1013
     * @inheritDoc
1014
     */
1015
    public function prepend($item)
1016
    {
1017
        array_unshift($this->data, $item);
1018
1019
        return $this;
1020
    }
1021
1022
    /**
1023
     * @inheritDoc
1024
     */
1025
    public function append($item)
1026
    {
1027
        array_push($this->data, $item);
1028
1029
        return $this;
1030
    }
1031
1032
    /**
1033
     * @inheritDoc
1034
     */
1035
    public function chunk($size)
1036
    {
1037
        $data = [];
1038
        $group = $iter = 0;
1039
        foreach ($this as $key => $val) {
1040
            $data[$group][$key] = $val;
1041
            if ($iter++ > $size) {
1042
                $group++;
1043
                $iter = 0;
1044
            }
1045
        }
1046
        return collect($data);
1047
    }
1048
1049
    /**
1050
     * @inheritDoc
1051
     */
1052
    public function combine($values)
1053
    {
1054
        if (!is_traversable($values)) {
1055
            throw new InvalidArgumentException(sprintf(
1056
                'Expecting traversable data for %s but got %s.',
1057
                __METHOD__,
1058
                typeof($values)
1059
            ));
1060
        }
1061
        return collect(
1062
            array_combine(
1063
                $this->keys()->toArray(),
1064
                collect($values)->values()->toArray()
0 ignored issues
show
Bug introduced by
It seems like $values defined by parameter $values on line 1052 can also be of type object<Noz\Contracts\CollectionInterface>; however, Noz\collect() does only seem to accept array|object<Iterator>|null, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
1065
            )
1066
        );
1067
    }
1068
1069
    /**
1070
     * @inheritDoc
1071
     */
1072
    public function diff($data)
1073
    {
1074
        return collect(
1075
            array_diff(
1076
                $this->toArray(),
1077
                collect($data)->toArray()
0 ignored issues
show
Bug introduced by
It seems like $data defined by parameter $data on line 1072 can also be of type object<Noz\Contracts\CollectionInterface>; however, Noz\collect() does only seem to accept array|object<Iterator>|null, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
1078
            )
1079
        );
1080
    }
1081
1082
    /**
1083
     * @inheritDoc
1084
     */
1085
    public function diffKeys($data)
1086
    {
1087
        return collect(
1088
            array_diff_key(
1089
                $this->toArray(),
1090
                collect($data)->toArray()
0 ignored issues
show
Bug introduced by
It seems like $data defined by parameter $data on line 1085 can also be of type object<Noz\Contracts\CollectionInterface>; however, Noz\collect() does only seem to accept array|object<Iterator>|null, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
1091
            )
1092
        );
1093
    }
1094
1095
    /**
1096
     * @inheritDoc
1097
     */
1098
    public function every($nth, $offset = null)
1099
    {
1100
        return $this->slice($offset)->filter(function($val, $key, $iter) use ($nth) {
1101
            return $iter % $nth == 0;
1102
        });
1103
    }
1104
1105
    /**
1106
     * @inheritDoc
1107
     */
1108
    public function except($indexes)
1109
    {
1110
        return $this->diffKeys(collect($indexes)->flip());
0 ignored issues
show
Bug introduced by
It seems like $indexes defined by parameter $indexes on line 1108 can also be of type object<Noz\Contracts\CollectionInterface>; however, Noz\collect() does only seem to accept array|object<Iterator>|null, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
1111
    }
1112
1113
    /**
1114
     * @inheritDoc
1115
     */
1116
    public function flip()
1117
    {
1118
        return collect(array_flip($this->data));
1119
    }
1120
1121
    /**
1122
     * @inheritDoc
1123
     */
1124
    public function intersect($data)
1125
    {
1126
        return collect(
1127
            array_intersect(
1128
                $this->toArray(),
1129
                collect($data)->toArray()
0 ignored issues
show
Bug introduced by
It seems like $data defined by parameter $data on line 1124 can also be of type object<Noz\Contracts\CollectionInterface>; however, Noz\collect() does only seem to accept array|object<Iterator>|null, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
1130
            )
1131
        );
1132
    }
1133
1134
    /**
1135
     * @inheritDoc
1136
     */
1137
    public function intersectKeys($data)
1138
    {
1139
        return collect(
1140
            array_intersect_key(
1141
                $this->toArray(),
1142
                collect($data)->toArray()
0 ignored issues
show
Bug introduced by
It seems like $data defined by parameter $data on line 1137 can also be of type object<Noz\Contracts\CollectionInterface>; however, Noz\collect() does only seem to accept array|object<Iterator>|null, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
1143
            )
1144
        );
1145
    }
1146
1147
    /**
1148
     * @inheritDoc
1149
     */
1150
    public function isEmpty(callable $callback = null)
1151
    {
1152
        if (!is_null($callback)) {
1153
            return $this->all($callback);
1154
        }
1155
        return empty($this->data);
1156
    }
1157
1158
    /**
1159
     * @inheritDoc
1160
     */
1161
    public function only($indices)
1162
    {
1163
        return $this->intersectKeys(collect($indices)->flip()->toArray());
0 ignored issues
show
Bug introduced by
It seems like $indices defined by parameter $indices on line 1161 can also be of type object<Noz\Contracts\CollectionInterface>; however, Noz\collect() does only seem to accept array|object<Iterator>|null, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
1164
    }
1165
1166
    /**
1167
     * @inheritDoc
1168
     */
1169
    public function pipe(callable $callback)
1170
    {
1171
        return $callback($this);
1172
    }
1173
1174
    /**
1175
     * @inheritDoc
1176
     */
1177
    public function random($num)
1178
    {
1179
        return $this->shuffle()->slice(0, $num);
1180
    }
1181
1182
    /**
1183
     * @inheritDoc
1184
     */
1185
    public function indicesOf($value)
1186
    {
1187
        return $this->filter(function($val) use ($value) {
1188
            return $val == $value;
1189
        })->map(function($val, $key) {
1190
            return $key;
1191
        });
1192
    }
1193
1194
    /**
1195
     * @inheritDoc
1196
     */
1197
    public function shuffle()
1198
    {
1199
        return collect(shuffle($data = $this->data));
0 ignored issues
show
Bug introduced by
$data = $this->data cannot be passed to shuffle() as the parameter $array expects a reference.
Loading history...
Documentation introduced by
shuffle($data = $this->data) is of type boolean, but the function expects a array|object<Iterator>|null.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1200
    }
1201
1202
    /**
1203
     * @inheritDoc
1204
     */
1205
    public function slice($offset, $length = null)
1206
    {
1207
        return collect(array_slice($this->data, $offset, $length, true));
1208
    }
1209
1210
    /**
1211
     * @inheritDoc
1212
     */
1213
    public function splice($offset, $length = null)
1214
    {
1215
        return $this->intersectKeys($this->slice($offset, $length)->toArray());
1216
    }
1217
1218
    /**
1219
     * @inheritDoc
1220
     */
1221
    public function split($num)
1222
    {
1223
        $data = [];
1224
        $group = $iter = 0;
1225
        $size = (int) ($this->count() / $num);
1226
        foreach ($this as $key => $val) {
1227
            $data[$group][$key] = $val;
1228
            if ($iter++ > $size) {
1229
                $group++;
1230
                $iter = 0;
1231
            }
1232
        }
1233
        return collect($data);
1234
    }
1235
1236
    /**
1237
     * @inheritDoc
1238
     */
1239
    public function transform(callable $callback)
1240
    {
1241
        $this->data = $this->map($callback)->toArray();
1242
        return $this;
1243
    }
1244
1245
    /**
1246
     * @inheritDoc
1247
     */
1248
    public function union($data)
1249
    {
1250
        // @todo Need a merge that doesn't change this collection
1251
        return collect(
1252
            array_merge(
1253
                collect($data)->toArray(),
0 ignored issues
show
Bug introduced by
It seems like $data defined by parameter $data on line 1248 can also be of type object<Noz\Contracts\CollectionInterface>; however, Noz\collect() does only seem to accept array|object<Iterator>|null, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
1254
                $this->toArray()
1255
            )
1256
        );
1257
    }
1258
1259
    /**
1260
     * @inheritDoc
1261
     */
1262
    public function zip(...$data)
1263
    {
1264
        return collect(
1265
            array_map(
1266
                $this->toArray(),
1267
                ...$data
1268
            )
1269
        );
1270
    }
1271
1272
    /**
1273
     * @inheritDoc
1274
     */
1275
    public function foldRight(callable $callback, $initial = null)
1276
    {
1277
        $iter = 0;
1278
        $carry = $initial;
1279
        foreach ($this as $key => $val) {
1280
            $carry = $callback($val, $carry, $key, $iter++);
1281
        }
1282
        return $carry;
1283
    }
1284
1285
    /**
1286
     * @inheritDoc
1287
     */
1288
    public function foldLeft(callable $callback, $initial = null)
1289
    {
1290
        return $this->reverse()->foldRight($callback, $initial);
1291
    }
1292
1293
    /**
1294
     * @inheritDoc
1295
     */
1296
    public function all(callable $callback = null)
1297
    {
1298
        if (is_null($callback)) {
1299
            $callback = function($val) {
1300
                return !((bool) $val);
1301
            };
1302
        }
1303
        return $this->filter($callback)->isEmpty();
1304
    }
1305
1306
    /**
1307
     * @inheritDoc
1308
     */
1309
    public function none(callable $callback = null)
1310
    {
1311
        if (is_null($callback)) {
1312
            $callback = function($val) {
1313
                return (bool) $val;
1314
            };
1315
        }
1316
        return $this->filter($callback)->isEmpty();
1317
    }
1318
1319
}
1320