AbstractCollection::__construct()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 1
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php
2
3
/*
4
 * CSVelte: Slender, elegant CSV for PHP
5
 * Inspired by Python's CSV module and Frictionless Data and the W3C's CSV
6
 * standardization efforts, CSVelte was written in an effort to take all the
7
 * suck out of working with CSV.
8
 *
9
 * @version   {version}
10
 * @copyright Copyright (c) 2016 Luke Visinoni <[email protected]>
11
 * @author    Luke Visinoni <[email protected]>
12
 * @license   https://github.com/deni-zen/csvelte/blob/master/LICENSE The MIT License (MIT)
13
 */
14
namespace CSVelte\Collection;
15
16
use ArrayAccess;
17
use ArrayIterator;
18
use Closure;
19
use Countable;
20
use CSVelte\Contract\Collectable;
21
use InvalidArgumentException;
22
use Iterator;
23
use OutOfBoundsException;
24
25
use function CSVelte\is_traversable;
26
27
/**
28
 * Class AbstractCollection.
29
 *
30
 * This is the abstract class that all other collection classes are based on.
31
 * Although it's possible to use a completely custom Collection class by simply
32
 * implementing the "Collectable" interface, extending this class gives you a
33
 * whole slew of convenient methods for free.
34
 *
35
 * @package CSVelte\Collection
36
 *
37
 * @since v0.2.2
38
 *
39
 * @author Luke Visinoni <[email protected]>
40
 * @copyright Copyright (c) 2016 Luke Visinoni <[email protected]>
41
 *
42
 * @todo Implement Serializable, other Interfaces
43
 * @todo Implement __toString() in such a way that by deault it
44
 *     will return a CSV-formatted string but you can configure
45
 *     it to return other formats if you want
46
 */
47
abstract class AbstractCollection implements
48
    ArrayAccess,
49
    Countable,
50
    Iterator
51
    /*Collectable*/
52
{
53
    /**
54
     * @var array The collection of data this object represents
55
     */
56
    protected $data = [];
57
58
    /**
59
     * @var bool True unless we have advanced past the end of the data array
60
     */
61
    protected $isValid = true;
62
63
    /**
64
     * AbstractCollection constructor.
65
     *
66
     * @param mixed $data The data to wrap
67
     */
68
    public function __construct($data = [])
69
    {
70
        $this->setData($data);
71
    }
72
73
    /**
74
     * Invoke object.
75
     *
76
     * Magic "invoke" method. Called when object is invoked as if it were a function.
77
     *
78
     * @param mixed $val   The value (depends on other param value)
79
     * @param mixed $index The index (depends on other param value)
80
     *
81
     * @return mixed (Depends on parameter values)
82
     */
83
    public function __invoke($val = null, $index = null)
84
    {
85
        if (is_null($val)) {
86
            if (is_null($index)) {
87
                return $this->toArray();
88
            }
89
90
            return $this->delete($index);
91
        }
92
        if (is_null($index)) {
93
            // @todo cast $val to array?
94
                return $this->merge($val);
95
        }
96
97
        return $this->set($val, $index);
98
    }
99
100
    /**
101
     * Convert collection to string.
102
     *
103
     * @return string A string representation of this collection
104
     *
105
     * @todo Eventually I would like to add a $delim property so that
106
     *     I can easily join collection items together with a particular
107
     * character (or set of characters). I would then add a few methods
108
     * to change the delim property. It would default to a comma.
109
     */
110
    public function __toString()
111
    {
112
        return $this->join();
113
    }
114
115
    // BEGIN ArrayAccess methods
116
117
    /**
118
     * Whether a offset exists.
119
     *
120
     * @param mixed $offset An offset to check for.
121
     *
122
     * @return bool true on success or false on failure.
123
     *
124
     * @see http://php.net/manual/en/arrayaccess.offsetexists.php
125
     */
126
    public function offsetExists($offset)
127
    {
128
        return $this->has($offset);
129
    }
130
131
    /**
132
     * Offset to retrieve.
133
     *
134
     * @param mixed $offset The offset to retrieve.
135
     *
136
     * @return mixed Can return all value types.
137
     *
138
     * @see http://php.net/manual/en/arrayaccess.offsetget.php
139
     */
140
    public function offsetGet($offset)
141
    {
142
        return $this->get($offset, null, true);
143
    }
144
145
    /**
146
     * Offset to set.
147
     *
148
     * @param mixed $offset The offset to assign the value to.
149
     * @param mixed $value  The value to set.
150
     *
151
     * @see http://php.net/manual/en/arrayaccess.offsetset.php
152
     */
153
    public function offsetSet($offset, $value)
154
    {
155
        $this->set($offset, $value);
156
    }
157
158
    /**
159
     * Offset to unset.
160
     *
161
     * @param mixed $offset The offset to unset.
162
     *
163
     * @see http://php.net/manual/en/arrayaccess.offsetunset.php
164
     */
165
    public function offsetUnset($offset)
166
    {
167
        $this->delete($offset);
168
    }
169
170
    // END ArrayAccess methods
171
172
    // BEGIN Countable methods
173
174
    public function count()
175
    {
176
        return count($this->data);
177
    }
178
179
    // END Countable methods
180
181
    // BEGIN Iterator methods
182
183
    /**
184
     * Return the current element.
185
     *
186
     * Returns the current element in the collection. The internal array pointer
187
     * of the data array wrapped by the collection should not be advanced by this
188
     * method. No side effects. Return current element only.
189
     *
190
     * @return mixed
191
     */
192
    public function current()
193
    {
194
        return current($this->data);
195
    }
196
197
    /**
198
     * Return the current key.
199
     *
200
     * Returns the current key in the collection. No side effects.
201
     *
202
     * @return mixed
203
     */
204
    public function key()
205
    {
206
        return key($this->data);
207
    }
208
209
    /**
210
     * Advance the internal pointer forward.
211
     *
212
     * Although this method will return the current value after advancing the
213
     * pointer, you should not expect it to. The interface does not require it
214
     * to return any value at all.
215
     *
216
     * @return mixed
217
     */
218
    public function next()
219
    {
220
        $next = next($this->data);
221
        $key  = key($this->data);
222
        if (isset($key)) {
223
            return $next;
224
        }
225
        $this->isValid = false;
226
    }
227
228
    /**
229
     * Rewind the internal pointer.
230
     *
231
     * Return the internal pointer to the first element in the collection. Again,
232
     * this method is not required to return anything by its interface, so you
233
     * should not count on a return value.
234
     *
235
     * @return mixed
236
     */
237
    public function rewind()
238
    {
239
        $this->isValid = !empty($this->data);
240
241
        return reset($this->data);
242
    }
243
244
    /**
245
     * Is internal pointer in a valid position?
246
     *
247
     * If the internal pointer is advanced beyond the end of the collection, this method will return false.
248
     *
249
     * @return bool True if internal pointer isn't past the end
250
     */
251
    public function valid()
252
    {
253
        return $this->isValid;
254
    }
255
256
    public function sort($alg = null)
257
    {
258
        if (is_null($alg)) {
259
            $alg = 'natcasesort';
260
        }
261
        $alg($this->data);
262
263
        return static::factory($this->data);
264
    }
265
266
    /**
267
     * Does this collection have a value at given index?
268
     *
269
     * @param mixed $index The index to check
270
     *
271
     * @return bool
272
     */
273
    public function has($index)
274
    {
275
        return array_key_exists($index, $this->data);
276
    }
277
278
    /**
279
     * Get value at a given index.
280
     *
281
     * Accessor for this collection of data. You can optionally provide a default
282
     * value for when the collection doesn't find a value at the given index. It can
283
     * also optionally throw an OutOfBoundsException if no value is found.
284
     *
285
     * @param mixed $index   The index of the data you want to get
286
     * @param mixed $default The default value to return if none available
287
     * @param bool  $throw   True if you want an exception to be thrown if no data found at $index
288
     *
289
     * @throws OutOfBoundsException If $throw is true and $index isn't found
290
     *
291
     * @return mixed The data found at $index or failing that, the $default
292
     *
293
     * @todo Use OffsetGet, OffsetSet, etc. internally here and on set, has, delete, etc.
294
     */
295
    public function get($index, $default = null, $throw = false)
296
    {
297
        if (isset($this->data[$index])) {
298
            return $this->data[$index];
299
        }
300
        if ($throw) {
301
            throw new OutOfBoundsException(__CLASS__ . ' could not find value at index ' . $index);
302
        }
303
304
        return $default;
305
    }
306
307
    /**
308
     * Set a value at a given index.
309
     *
310
     * Setter for this collection. Allows setting a value at a given index.
311
     *
312
     * @param mixed $index The index to set a value at
313
     * @param mixed $val   The value to set $index to
314
     *
315
     * @return $this
316
     */
317
    public function set($index, $val)
318
    {
319
        $this->data[$index] = $val;
320
321
        return $this;
322
    }
323
324
    /**
325
     * Unset a value at a given index.
326
     *
327
     * Unset (delete) value at the given index.
328
     *
329
     * @param mixed $index The index to unset
330
     * @param bool  $throw True if you want an exception to be thrown if no data found at $index
331
     *
332
     * @throws OutOfBoundsException If $throw is true and $index isn't found
333
     *
334
     * @return $this
335
     */
336
    public function delete($index, $throw = false)
337
    {
338
        if (isset($this->data[$index])) {
339
            unset($this->data[$index]);
340
        } else {
341
            if ($throw) {
342
                throw new OutOfBoundsException('No value found at given index: ' . $index);
343
            }
344
        }
345
346
        return $this;
347
    }
348
349
    /**
350
     * Does this collection have a value at specified numerical position?
351
     *
352
     * Returns true if collection contains a value (any value including null)
353
     * at specified numerical position.
354
     *
355
     * @param int $pos The position
356
     *
357
     * @return bool
358
     *
359
     * @todo I feel like it would make more sense  to have this start at position 1 rather than 0
360
     */
361
    public function hasPosition($pos)
362
    {
363
        try {
364
            $this->getKeyAtPosition($pos);
365
366
            return true;
367
        } catch (OutOfBoundsException $e) {
368
            return false;
369
        }
370
    }
371
372
    /**
373
     * Return value at specified numerical position.
374
     *
375
     * @param int $pos The numerical position
376
     *
377
     * @throws OutOfBoundsException if no pair at position
378
     *
379
     * @return mixed
380
     */
381
    public function getValueAtPosition($pos)
382
    {
383
        return $this->data[$this->getKeyAtPosition($pos)];
384
    }
385
386
    /**
387
     * Return key at specified numerical position.
388
     *
389
     * @param int $pos The numerical position
390
     *
391
     * @throws OutOfBoundsException if no pair at position
392
     *
393
     * @return mixed
394
     */
395
    public function getKeyAtPosition($pos)
396
    {
397
        $i = 0;
398
        foreach ($this as $key => $val) {
399
            if ($i === $pos) {
400
                return $key;
401
            }
402
            $i++;
403
        }
404
        throw new OutOfBoundsException("No element at expected position: $pos");
405
    }
406
407
    /**
408
     * @param int $pos The numerical position
409
     *
410
     * @throws OutOfBoundsException if no pair at position
411
     *
412
     * @return array
413
     */
414
    public function getPairAtPosition($pos)
415
    {
416
        $pairs = $this->pairs();
417
418
        return $pairs[$this->getKeyAtPosition($pos)];
419
    }
420
421
    /**
422
     * Get collection as array.
423
     *
424
     * @return array This collection as an array
425
     */
426
    public function toArray()
427
    {
428
        $arr = [];
429
        foreach ($this as $index => $value) {
430
            if (is_object($value) && method_exists($value, 'toArray')) {
431
                $value = $value->toArray();
432
            }
433
            $arr[$index] = $value;
434
        }
435
436
        return $arr;
437
    }
438
439
    /**
440
     * Get this collection's keys as a collection.
441
     *
442
     * @return AbstractCollection Containing this collection's keys
443
     */
444
    public function keys()
445
    {
446
        return static::factory(array_keys($this->data));
447
    }
448
449
    /**
450
     * Get this collection's values as a collection.
451
     *
452
     * This method returns this collection's values but completely re-indexed (numerically).
453
     *
454
     * @return AbstractCollection Containing this collection's values
455
     */
456
    public function values()
457
    {
458
        return static::factory(array_values($this->data));
459
    }
460
461
    /**
462
     * Merge data into collection.
463
     *
464
     * Merges input data into this collection. Input can be an array or another collection. Returns a NEW collection object.
465
     *
466
     * @param Traversable|array $data The data to merge with this collection
467
     *
468
     * @return AbstractCollection A new collection with $data merged in
469
     */
470
    public function merge($data)
471
    {
472
        $this->assertCorrectInputDataType($data);
473
        $coll = static::factory($this->data);
474
        foreach ($data as $index => $value) {
475
            $coll->set($index, $value);
476
        }
477
478
        return $coll;
479
    }
480
481
    /**
482
     * Determine if this collection contains a value.
483
     *
484
     * Allows you to pass in a value or a callback function and optionally an index,
485
     * and tells you whether or not this collection contains that value. If the $index param is specified, only that index will be looked under.
486
     *
487
     * @param mixed|callable $value The value to check for
488
     * @param mixed          $index The (optional) index to look under
489
     *
490
     * @return bool True if this collection contains $value
491
     *
492
     * @todo Maybe add $identical param for identical comparison (===)
493
     * @todo Allow negative offset for second param
494
     */
495
    public function contains($value, $index = null)
496
    {
497
        return (bool) $this->first(function ($val, $key) use ($value, $index) {
498
            if (is_callable($value)) {
499
                $found = $value($val, $key);
500
            } else {
501
                $found = ($value == $val);
502
            }
503
            if ($found) {
504
                if (is_null($index)) {
505
                    return true;
506
                }
507
                if (is_array($index)) {
508
                    return in_array($key, $index);
509
                }
510
511
                return $key == $index;
512
            }
513
514
            return false;
515
        });
516
    }
517
518
    /**
519
     * Get duplicate values.
520
     *
521
     * Returns a collection of arrays where the key is the duplicate value
522
     * and the value is an array of keys from the original collection.
523
     *
524
     * @return AbstractCollection A new collection with duplicate values.
525
     */
526
    public function duplicates()
527
    {
528
        $dups = [];
529
        $this->walk(function ($val, $key) use (&$dups) {
530
            $dups[$val][] = $key;
531
        });
532
533
        return static::factory($dups)->filter(function ($val) {
534
            return count($val) > 1;
535
        });
536
    }
537
538
    /**
539
     * Pop an element off the end of this collection.
540
     *
541
     * @return mixed The last item in this collectio n
542
     */
543
    public function pop()
544
    {
545
        return array_pop($this->data);
546
    }
547
548
    /**
549
     * Shift an element off the beginning of this collection.
550
     *
551
     * @return mixed The first item in this collection
552
     */
553
    public function shift()
554
    {
555
        return array_shift($this->data);
556
    }
557
558
    /**
559
     * Push a item(s) onto the end of this collection.
560
     *
561
     * Returns a new collection with $items added.
562
     *
563
     * @param array $items Any number of arguments will be pushed onto the
564
     *
565
     * @return mixed The first item in this collection
566
     */
567
    public function push(...$items)
568
    {
569
        array_push($this->data, ...$items);
570
571
        return static::factory($this->data);
572
    }
573
574
    /**
575
     * Unshift item(s) onto the beginning of this collection.
576
     *
577
     * Returns a new collection with $items added.
578
     *
579
     * @return mixed The first item in this collection
580
     */
581
    public function unshift(...$items)
582
    {
583
        array_unshift($this->data, ...$items);
584
585
        return static::factory($this->data);
586
    }
587
588
    /**
589
     * Pad this collection to a certain size.
590
     *
591
     * Returns a new collection, padded to the given size, with the given value.
592
     *
593
     * @param int  $size The number of items that should be in the collection
594
     * @param null $with The value to pad the collection with
595
     *
596
     * @return AbstractCollection A new collection padded to specified length
597
     */
598
    public function pad($size, $with = null)
599
    {
600
        return static::factory(array_pad($this->data, $size, $with));
601
    }
602
603
    /**
604
     * Apply a callback to each item in collection.
605
     *
606
     * Applies a callback to each item in collection and returns a new collection
607
     * containing each iteration's return value.
608
     *
609
     * @param callable $callback The callback to apply
610
     *
611
     * @return AbstractCollection A new collection with callback return values
612
     */
613
    public function map(callable $callback)
614
    {
615
        return static::factory(array_map($callback, $this->data));
616
    }
617
618
    /**
619
     * Apply a callback to each item in collection.
620
     *
621
     * Applies a callback to each item in collection. The callback should return
622
     * false to filter any item from the collection.
623
     *
624
     * @param callable $callback     The callback function
625
     * @param null     $extraContext Extra context to pass as third param in callback
626
     *
627
     * @return $this
628
     *
629
     * @see php.net array_walk
630
     */
631
    public function walk(callable $callback, $extraContext = null)
632
    {
633
        array_walk($this->data, $callback, $extraContext);
634
635
        return $this;
636
    }
637
638
    /**
639
     * Iterate over each item that matches criteria in callback.
640
     *
641
     * @param Closure|callable $callback A callback to use
642
     * @param object           $bindTo   The object to bind to
643
     *
644
     * @return AbstractCollection
645
     */
646
    public function each(Closure $callback, $bindTo = null)
647
    {
648
        if (is_null($bindTo)) {
649
            $bindTo = $this;
650
        }
651
        if (!is_object($bindTo)) {
652
            throw new InvalidArgumentException('Second argument must be an object.');
653
        }
654
        $cb     = $callback->bindTo($bindTo);
655
        $return = [];
656
        foreach ($this as $key => $val) {
657
            if ($cb($val, $key)) {
658
                $return[$key] = $val;
659
            }
660
        }
661
662
        return static::factory($return);
663
    }
664
665
    /**
666
     * Get each key/value as an array pair.
667
     *
668
     * Returns a collection of arrays where each item in the collection is [key,value]
669
     *
670
     * @return AbstractCollection
671
     */
672
    public function pairs()
673
    {
674
        return static::factory(array_map(
675
            function ($key, $val) {
676
                return [$key, $val];
677
            },
678
            array_keys($this->data),
679
            array_values($this->data)
680
        ));
681
    }
682
683
    /**
684
     * Reduce the collection to a single value.
685
     *
686
     * Using a callback function, this method will reduce this collection to a
687
     * single value.
688
     *
689
     * @param callable $callback The callback function used to reduce
690
     * @param null     $initial  The initial carry value
691
     *
692
     * @return mixed The single value produced by reduction algorithm
693
     */
694
    public function reduce(callable $callback, $initial = null)
695
    {
696
        return array_reduce($this->data, $callback, $initial);
697
    }
698
699
    /**
700
     * Filter the collection.
701
     *
702
     * Using a callback function, this method will filter out unwanted values, returning
703
     * a new collection containing only the values that weren't filtered.
704
     *
705
     * @param callable $callback The callback function used to filter
706
     * @param int      $flag     array_filter flag(s) (ARRAY_FILTER_USE_KEY or ARRAY_FILTER_USE_BOTH)
707
     *
708
     * @return AbstractCollection A new collection with only values that weren't filtered
709
     *
710
     * @see php.net array_filter
711
     */
712
    public function filter(callable $callback, $flag = ARRAY_FILTER_USE_BOTH)
713
    {
714
        return static::factory(array_filter($this->data, $callback, $flag));
715
    }
716
717
    /**
718
     * Return the first item that meets given criteria.
719
     *
720
     * Using a callback function, this method will return the first item in the collection
721
     * that causes the callback function to return true.
722
     *
723
     * @param callable $callback The callback function
724
     *
725
     * @return null|mixed The first item in the collection that causes callback to return true
726
     */
727
    public function first(callable $callback)
728
    {
729
        foreach ($this->data as $index => $value) {
730
            if ($callback($value, $index)) {
731
                return $value;
732
            }
733
        }
734
735
        return null;
736
    }
737
738
    /**
739
     * Return the last item that meets given criteria.
740
     *
741
     * Using a callback function, this method will return the last item in the collection
742
     * that causes the callback function to return true.
743
     *
744
     * @param callable $callback The callback function
745
     *
746
     * @return null|mixed The last item in the collection that causes callback to return true
747
     */
748
    public function last(callable $callback)
749
    {
750
        $reverse = $this->reverse(true);
751
752
        return $reverse->first($callback);
753
    }
754
755
    /**
756
     * Returns collection in reverse order.
757
     *
758
     * @param null $preserveKeys True if you want to preserve collection's keys
759
     *
760
     * @return AbstractCollection This collection in reverse order.
761
     */
762
    public function reverse($preserveKeys = null)
763
    {
764
        return static::factory(array_reverse($this->data, $preserveKeys));
765
    }
766
767
    /**
768
     * Get unique items.
769
     *
770
     * Returns a collection of all the unique items in this collection.
771
     *
772
     * @return AbstractCollection This collection with duplicate items removed
773
     */
774
    public function unique()
775
    {
776
        return static::factory(array_unique($this->data));
777
    }
778
779
    /**
780
     * Join collection together using a delimiter.
781
     *
782
     * @param string $delimiter The delimiter string/char
783
     *
784
     * @return string
785
     */
786
    public function join($delimiter = '')
787
    {
788
        return implode($delimiter, $this->data);
789
    }
790
791
    /**
792
     * Counts how many times each value occurs in a collection.
793
     *
794
     * Returns a new collection with values as keys and how many times that
795
     * value appears in the collection. Works best with scalar values but will
796
     * attempt to work on collections of objects as well.
797
     *
798
     * @return AbstractCollection
799
     *
800
     * @todo Right now, collections of arrays or objects are supported via the
801
     * __toString() or spl_object_hash()
802
     * @todo NumericCollection::counts() does the same thing...
803
     */
804
    public function frequency()
805
    {
806
        $frequency = [];
807
        foreach ($this as $key => $val) {
808
            if (!is_scalar($val)) {
809
                if (!is_object($val)) {
810
                    $val = new ArrayIterator($val);
811
                }
812
813
                if (method_exists($val, '__toString')) {
814
                    $val = (string) $val;
815
                } else {
816
                    $val = spl_object_hash($val);
817
                }
818
            }
819
            if (!isset($frequency[$val])) {
820
                $frequency[$val] = 0;
821
            }
822
            $frequency[$val]++;
823
        }
824
825
        return static::factory($frequency);
826
    }
827
828
    /**
829
     * Collection factory method.
830
     *
831
     * This method will analyze input data and determine the most appropriate Collection
832
     * class to use. It will then instantiate said Collection class with the given
833
     * data and return it.
834
     *
835
     * @param mixed $data The data to wrap
836
     *
837
     * @return AbstractCollection A collection containing $data
838
     */
839
    public static function factory($data = null)
840
    {
841
        if (static::isTabular($data)) {
842
            $class = TabularCollection::class;
843
        } elseif (static::isMultiDimensional($data)) {
844
            $class = MultiCollection::class;
845
        } elseif (static::isAllNumeric($data)) {
846
            $class = NumericCollection::class;
847
        } elseif (static::isCharacterSet($data)) {
848
            $class = CharCollection::class;
849
        } else {
850
            $class = Collection::class;
851
        }
852
853
        return new $class($data);
854
    }
855
856
    /**
857
     * Is input data tabular?
858
     *
859
     * Returns true if input data is tabular in nature. This means that it is a
860
     * two-dimensional array with the same keys (columns) for each element (row).
861
     *
862
     * @param mixed $data The data structure to check
863
     *
864
     * @return bool True if data structure is tabular
865
     */
866
    public static function isTabular($data)
867
    {
868
        if (!is_traversable($data)) {
869
            return false;
870
        }
871
        foreach ($data as $row) {
872
            if (!is_traversable($row)) {
873
                return false;
874
            }
875
            $columns = array_keys($row);
876
            if (!isset($cmp_columns)) {
877
                $cmp_columns = $columns;
878
            } else {
879
                if ($cmp_columns != $columns) {
880
                    return false;
881
                }
882
            }
883
            // if row contains an array it isn't tabular
884
            if (array_reduce($row, function ($carry, $item) {
885
                return is_array($item) && $carry;
886
            }, true)) {
887
                return false;
888
            }
889
        }
890
891
        return true;
892
    }
893
894
    /**
895
     * Check data for multiple dimensions.
896
     *
897
     * This method is to determine whether a data structure is multi-dimensional.
898
     * That is to say, it is a traversable structure that contains at least one
899
     * traversable structure.
900
     *
901
     * @param mixed $data The input data
902
     *
903
     * @return bool
904
     */
905
    public static function isMultiDimensional($data)
906
    {
907
        if (!is_traversable($data)) {
908
            return false;
909
        }
910
        foreach ($data as $elem) {
911
            if (is_traversable($elem)) {
912
                return true;
913
            }
914
        }
915
916
        return false;
917
    }
918
919
    /**
920
     * Determine if structure contains all numeric values.
921
     *
922
     * @param mixed $data The input data
923
     *
924
     * @return bool
925
     */
926
    public static function isAllNumeric($data)
927
    {
928
        if (!is_traversable($data)) {
929
            return false;
930
        }
931
        foreach ($data as $val) {
932
            if (!is_numeric($val)) {
933
                return false;
934
            }
935
        }
936
937
        return true;
938
    }
939
940
    /**
941
     * Is data a string of characters?
942
     *
943
     * Just checks to see if input is a string of characters or a string
944
     * of digits.
945
     *
946
     * @param mixed $data Data to check
947
     *
948
     * @return bool
949
     */
950
    public static function isCharacterSet($data)
951
    {
952
        return
953
            is_string($data) ||
954
            is_numeric($data);
955
    }
956
957
    // END Iterator methods
958
959
    /**
960
     * Set collection data.
961
     *
962
     * Sets the collection data.
963
     *
964
     * @param array $data The data to wrap
965
     *
966
     * @return $this
967
     */
968
    protected function setData($data)
969
    {
970
        if (is_null($data)) {
971
            $data = [];
972
        }
973
        $this->assertCorrectInputDataType($data);
974
        $data = $this->prepareData($data);
975
        foreach ($data as $index => $value) {
976
            $this->set($index, $value);
977
        }
978
        reset($this->data);
979
980
        return $this;
981
    }
982
983
    /**
984
     * Assert input data is of the correct structure.
985
     *
986
     * @param mixed $data Data to check
987
     *
988
     * @throws InvalidArgumentException If invalid data structure
989
     */
990
    protected function assertCorrectInputDataType($data)
991
    {
992
        if (!$this->isConsistentDataStructure($data)) {
993
            throw new InvalidArgumentException(__CLASS__ . ' expected traversable data, got: ' . gettype($data));
994
        }
995
    }
996
997
    /**
998
     * Convert input data to an array.
999
     *
1000
     * Convert the input data to an array that can be worked with by a collection.
1001
     *
1002
     * @param mixed $data The input data
1003
     *
1004
     * @return array
1005
     */
1006
    protected function prepareData($data)
1007
    {
1008
        return $data;
1009
    }
1010
1011
    /**
1012
     * Determine whether data is consistent with a given collection type.
1013
     *
1014
     * This method is used to determine whether input data is consistent with a
1015
     * given collection type. For instance, CharCollection requires a string.
1016
     * NumericCollection requires an array or traversable set of numeric data.
1017
     * TabularCollection requires a two-dimensional data structure where all the
1018
     * keys are the same in every row.
1019
     *
1020
     * @param mixed $data Data structure to check for consistency
1021
     *
1022
     * @return bool
1023
     */
1024
    abstract protected function isConsistentDataStructure($data);
1025
}
1026