Issues (3)

Security Analysis    no request data  

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

src/Collection/AbstractCollection.php (2 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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