Completed
Branch refactor/142 (8a1d2c)
by Luke
02:46
created

Collection::each()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 4

Importance

Changes 0
Metric Value
cc 4
eloc 5
nc 4
nop 1
dl 0
loc 9
ccs 7
cts 7
cp 1
crap 4
rs 9.2
c 0
b 0
f 0
1
<?php
2
/**
3
 * CSVelte: Slender, elegant CSV for PHP
4
 * Inspired by Python's CSV module and Frictionless Data and the W3C's CSV
5
 * standardization efforts, CSVelte was written in an effort to take all the
6
 * suck out of working with CSV.
7
 *
8
 * @version   v0.2.1
9
 * @copyright Copyright (c) 2016 Luke Visinoni <[email protected]>
10
 * @author    Luke Visinoni <[email protected]>
11
 * @license   https://github.com/deni-zen/csvelte/blob/master/LICENSE The MIT License (MIT)
12
 */
13
namespace CSVelte;
14
15
use \Iterator;
16
use \Countable;
17
use \ArrayAccess;
18
use \OutOfBoundsException;
19
use \InvalidArgumentException;
20
use \RuntimeException;
21
use CSVelte\Collection;
22
23
use function CSVelte\collect;
24
25
/**
26
 * Collection class.
27
 *
28
 * Represents a collection of data. This is a one-dimensional structure that is
29
 * represented internally with a simple array. It provides several very
30
 * convenient operations to be performed on its data.
31
 *
32
 * @package   CSVelte
33
 * @copyright (c) 2016, Luke Visinoni <[email protected]>
34
 * @author    Luke Visinoni <[email protected]>
35
 * @since     v0.2.1
36
 * @todo      Most of this class's methods will return a new Collection class
37
 *     rather than modify the existing class. There needs to be a clear distinction
38
 *     as to which ones don't and why. Also, some methods return a single value.
39
 *     These also need to be clear.
40
 * @todo      Need to make sure method naming, args, return values, concepts, etc.
41
 *     are consistent. This is a very large class with a LOT of methods. It will
42
 *     be very difficult to not let it blow up and get extremely messy. Go through
43
 *     and refactor each method. Make sure there is nothing superfluous and that
44
 *     everything makes sense and is intuitive to use. Also, because this class
45
 *     is so enourmous it is going to be a bitch to test. Good test coverage is
46
 *     going to require a LOT of tests. So put that on the list as well...
47
 * @todo      Implement whichever SPL classes/interfaces you can (that make sense).
48
 *     Probably a good idea to implement/extend some of these:
49
 *         Interfaces - RecursiveIterator, SeekableIterator, OuterIterator, IteratorAggregate
50
 *         Classes - FilterIterator, CallbackFilterIterator, CachingIterator, IteratorIterator, etc.
51
 * @replaces  \CSVelte\Utils
52
 */
53
class Collection implements Countable, ArrayAccess
54
{
55
    /**
56
     * Constants used as comparison operators in where() method
57
     */
58
59
    /** @var const Use this operator constant to test for identity (exact same) **/
60
    const WHERE_ID = '===';
61
62
    /** @var const Use this operator constant to test for non-identity **/
63
    const WHERE_NID = '!==';
64
65
    /** @var const Use this operator constant to test for equality **/
66
    const WHERE_EQ = '==';
67
68
    /** @var const Use this operator constant to test for non-equality **/
69
    const WHERE_NEQ = '!=';
70
71
    /** @var const Use this operator constant to test for less-than **/
72
    const WHERE_LT = '<';
73
74
    /** @var const Use this operator constant to test for greater-than or equal-to **/
75
    const WHERE_LTE = '<=';
76
77
    /** @var const Use this operator constant to test for greater-than **/
78
    const WHERE_GT = '>';
79
80
    /** @var const Use this operator constant to test for greater-than or equal-to **/
81
    const WHERE_GTE = '>=';
82
83
    /** @var const Use this operator constant to test for case insensitive equality **/
84
    const WHERE_LIKE = 'like';
85
86
    /** @var const Use this operator constant to test for case instensitiv inequality **/
87
    const WHERE_NLIKE = '!like';
88
89
    /** @var const Use this operator constant to test for descendants or instances of a class **/
90
    const WHERE_ISA = 'instanceof';
91
92
    /** @var const Use this operator constant to test for values that aren't descendants or instances of a class  **/
93
    const WHERE_NISA = '!instanceof';
94
95
    /** @var const Use this operator constant to test for internal PHP types **/
96
    const WHERE_TOF = 'typeof';
97
98
    /** @var const Use this operator constant to test for internal PHP type (negated) **/
99
    const WHERE_NTOF = '!typeof';
100
101
    /** @var const Use this operator constant to test against a regex pattern **/
102
    const WHERE_MATCH = 'match';
103
104
    /** @var const Use this operator constant to test against a regex pattern (negated) **/
105
    const WHERE_NMATCH = '!match';
106
107
    /**
108
     * Underlying array
109
     * @var array The array of data for this collection
110
     */
111
    protected $data = [];
112
113
    /**
114
     * Collection constructor.
115
     *
116
     * Set the data for this collection using $data
117
     *
118
     * @param array|ArrayAccess|null $data Either an array or an object that can be accessed
119
     *     as if it were an array.
120
     */
121 127
    public function __construct($data = null)
122
    {
123 127
        $this->assertArrayOrIterator($data);
124 126
        if (!is_null($data)) {
125 125
            $this->setData($data);
0 ignored issues
show
Bug introduced by
It seems like $data defined by parameter $data on line 121 can also be of type object<ArrayAccess>; however, CSVelte\Collection::setData() does only seem to accept array|object<Iterator>, 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...
126 125
        }
127 126
    }
128
129
    /**
130
     * Invokes the object as a function.
131
     *
132
     * If called with no arguments, it will return underlying data array
133
     * If called with array as first argument, array will be merged into data array
134
     * If called with second param, it will call $this->set($key, $val)
135
     * If called with null as first param and key as second param, it will call $this->offsetUnset($key)
136
     *
137
     * @param null|array $val If an array, it will be merged into the collection
138
     *     If both this arg and second arg are null, underlying data array will be returned
139
     * @param null|any $key If null and first arg is callable, this method will call map with callable
140
     *     If this value is not null but first arg is, it will call $this->offsetUnset($key)
141
     *     If this value is not null and first arg is anything other than callable, it will return $this->set($key, $val)
142
     * @see the description for various possible method signatures
143
     * @return mixed The return value depends entirely upon the arguments passed
144
     *     to it. See description for various possible arguments/return value combinations
145
     */
146 1
    public function __invoke($val = null, $key = null)
147
    {
148 1
        if (is_null($val)) {
149
            if (is_null($key)) {
150
                return $this->data;
151
            } else {
152
                return $this->offsetUnset($key);
153
            }
154
        } else {
155 1
            if (is_null($key)) {
156 1
                if (is_array($val)) return $this->merge($val);
157
                else {
158 1
                    if (is_callable($val)) {
159 1
                        return $this->map($val);
160
                    } /*else {
161
                        return $this->set($key, $val);
162
                    }*/
163
                }
164
            } else {
165
                $this->offsetSet($key, $val);
166
            }
167
        }
168
        return $this;
169
    }
170
171
    /**
172
     * Set internal collection data.
173
     *
174
     * Use an array or iterator to set this collection's data.
175
     *
176
     * @param array|Iterator $data The data to set for this collection
177
     * @return $this
178
     * @throws InvalidArgumentException If invalid data type
179
     */
180 125
    protected function setData($data)
181
    {
182 125
        $this->assertArrayOrIterator($data);
183 125
        foreach ($data as $key => $val) {
184 124
            $this->data[$key] = $val;
185 125
        }
186 125
        return $this;
187
    }
188
189
    /**
190
     * Get data as an array.
191
     *
192
     * @return array Collection data as an array
193
     */
194 63
    public function toArray()
195
    {
196 63
        $data = [];
197 63
        foreach($this->data as $key => $val) {
198 63
            $data[$key] = (is_object($val) && method_exists($val, 'toArray')) ? $val->toArray() : $val;
199 63
        }
200 63
        return $data;
201
    }
202
203
    /**
204
     * Get array keys
205
     *
206
     * @return \CSVelte\Collection The collection's keys (as a collection)
207
     */
208 3
    public function keys()
209
    {
210 3
        return new self(array_keys($this->data));
211
    }
212
213
    /**
214
     * Merge data (array or iterator)
215
     *
216
     * Pass an array to this method to have it merged into the collection. A new
217
     * collection will be created with the merged data and returned.
218
     *
219
     * @param array|iterator $data Data to merge into the collection
220
     * @param boolean $overwrite Whether existing values should be overwritten
221
     * @return \CSVelte\Collection A new collection with $data merged into it
222
     */
223 1
    public function merge($data = null, $overwrite = true)
224
    {
225 1
        $this->assertArrayOrIterator($data);
226 1
        $coll = new self($this->data);
227 1
        foreach ($data as $key => $val) {
0 ignored issues
show
Bug introduced by
The expression $data of type array|object<CSVelte\iterator>|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
228 1
            $coll->set($key, $val, $overwrite);
0 ignored issues
show
Documentation introduced by
$key is of type integer|string, but the function expects a object<CSVelte\any>.

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...
229 1
        }
230 1
        return $coll;
231
    }
232
233
    /**
234
     * Test whether this collection contains the given value, optionally at a
235
     * specific key.
236
     *
237
     * This will return true if the collection contains a value equivalent to $val.
238
     * If $val is a callable (function/method), than the callable will be called
239
     * with $val, $key as its arguments (in that order). If the callable returns
240
     * any truthy value, than this method will return true.
241
     *
242
     * @param any|callable $val Either the value to check for or a callable that
243
     *     accepts $key,$val and returns true if collection contains $val
244
     * @param any $key If not null, the only the value for this key will be checked
245
     * @return boolean True if this collection contains $val, $key
246
     */
247 48
    public function contains($val, $key = null)
248
    {
249 48
        if (is_callable($callback = $val)) {
250 47
            foreach ($this->data as $key => $val) {
251 47
                if ($callback($val, $key)) return true;
252 40
            }
253 42
        } elseif (in_array($val, $this->data)) {
254 8
            return (is_null($key) || (isset($this->data[$key]) && $this->data[$key] == $val));
255
        }
256 42
        return false;
257
    }
258
259
    /**
260
     * Tabular Where Search.
261
     *
262
     * Search for values of a certain key that meet a particular search criteria
263
     * using either one of the "Collection::WHERE_" class constants, or its string
264
     * counterpart.
265
     *
266
     * Warning: Only works for tabular collections (2-dimensional data array)
267
     *
268
     * @param string $key The key to compare to $val
269
     * @param mixed|Callable $val Either a value to test against or a callable to
270
     *     run your own custom "where comparison logic"
271
     * @param string $comp The type of comparison operation ot use (such as "=="
272
     *     or "instanceof"). Must be one of the self::WHERE_* constants' values
273
     *     listed at the top of this class.
274
     * @return \CSVelte\Collection A collection of rows that meet the criteria
275
     *     specified by $key, $val, and $comp
276
     */
277 4
    public function where($key, $val, $comp = null)
278
    {
279 4
        $this->assertIsTabular();
280 3
        $data = [];
281 3
        if ($this->has($key, true)) {
0 ignored issues
show
Documentation introduced by
$key is of type string, but the function expects a object<CSVelte\any>.

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...
282 3
            if (is_callable($val)) {
283 1
                foreach ($this->data as $ln => $row) {
284 1
                    if ($val($row[$key], $key)) {
285 1
                        $data[$ln] = $row;
286 1
                    }
287 1
                }
288 1
            } else {
289 3
                foreach ($this->data as $ln => $row) {
290 3
                    $fieldval = $row[$key];
291 3
                    switch (strtolower($comp)) {
292 3
                        case self::WHERE_ID:
293 1
                            $comparison = $fieldval === $val;
294 1
                            break;
295 3
                        case self::WHERE_NID:
296 1
                            $comparison = $fieldval !== $val;
297 1
                            break;
298 3
                        case self::WHERE_LT:
299
                            $comparison = $fieldval < $val;
300
                            break;
301 3
                        case self::WHERE_LTE:
302 1
                            $comparison = $fieldval <= $val;
303 1
                            break;
304 3
                        case self::WHERE_GT:
305 1
                            $comparison = $fieldval > $val;
306 1
                            break;
307 3
                        case self::WHERE_GTE:
308
                            $comparison = $fieldval >= $val;
309
                            break;
310 3
                        case self::WHERE_LIKE:
311 1
                            $comparison = strtolower($fieldval) == strtolower($val);
312 1
                            break;
313 3
                        case self::WHERE_NLIKE:
314 1
                            $comparison = strtolower($fieldval) != strtolower($val);
315 1
                            break;
316 3
                        case self::WHERE_ISA:
317 1
                            $comparison = (is_object($fieldval) && $fieldval instanceof $val);
318 1
                            break;
319 3
                        case self::WHERE_NISA:
320 1
                            $comparison = (!is_object($fieldval) || !($fieldval instanceof $val));
321 1
                            break;
322 3
                        case self::WHERE_TOF:
323 1
                            $comparison = (strtolower(gettype($fieldval)) == strtolower($val));
324 1
                            break;
325 3
                        case self::WHERE_NTOF:
326 1
                            $comparison = (strtolower(gettype($fieldval)) != strtolower($val));
327 1
                            break;
328 3
                        case self::WHERE_NEQ:
329 1
                            $comparison = $fieldval != $val;
330 1
                            break;
331 2
                        case self::WHERE_MATCH:
332 1
                            $match = preg_match($val, $fieldval);
333 1
                            $comparison = $match === 1;
334 1
                            break;
335 2
                        case self::WHERE_NMATCH:
336 1
                            $match = preg_match($val, $fieldval);
337 1
                            $comparison = $match === 0;
338 1
                            break;
339 1
                        case self::WHERE_EQ:
340 1
                        default:
341 1
                            $comparison = $fieldval == $val;
342 1
                            break;
343 3
                    }
344 3
                    if ($comparison) {
345 3
                        $data[$ln] = $row;
346 3
                    }
347 3
                }
348
            }
349 3
        }
350 3
        return new self($data);
351
    }
352
353
    /**
354
     * Get the key at a given numerical position.
355
     *
356
     * This method will give you the key at the specified numerical offset,
357
     * regardless of how it's indexed (associatively, unordered numerical, etc.).
358
     * This allows you to find out what the first key is. Or the second. etc.
359
     *
360
     * @param int $pos Numerical position
361
     * @return mixed The key at numerical position
362
     * @throws \OutOfBoundsException If you request a position that doesn't exist
363
     * @todo Allow negative $pos to start counting from end
364
     */
365 71
    public function getKeyAtPosition($pos)
366
    {
367 71
        $i = 0;
368 71
        foreach ($this->data as $key => $val) {
369 70
            if ($i === $pos) return $key;
370 15
            $i++;
371 20
        }
372 17
        throw new OutOfBoundsException("Collection data does not contain a key at given position: " . $pos);
373
    }
374
375
    /**
376
     * Get the value at a given numerical position.
377
     *
378
     * This method will give you the value at the specified numerical offset,
379
     * regardless of how it's indexed (associatively, unordered numerical, etc.).
380
     * This allows you to find out what the first value is. Or the second. etc.
381
     *
382
     * @param int $pos Numerical position
383
     * @return mixed The value at numerical position
384
     * @throws \OutOfBoundsException If you request a position that doesn't exist
385
     * @todo Allow negative $pos to start counting from end
386
     */
387 69
    public function getValueAtPosition($pos)
388
    {
389 69
        return $this->data[$this->getKeyAtPosition($pos)];
390
    }
391
392
    /**
393
     * Determine if this collection has a value at the specified numerical position.
394
     *
395
     * @param int $pos Numerical position
396
     * @return boolean Whether there exists a value at specified position
397
     */
398 63
    public function hasPosition($pos)
399
    {
400
        try {
401 63
            $this->getKeyAtPosition($pos);
402 63
        } catch (OutOfBoundsException $e) {
403 10
            return false;
404
        }
405 62
        return true;
406
    }
407
408
    /**
409
     * "Pop" an item from the end of a collection.
410
     *
411
     * Removes an item from the bottom of the collection's underlying array and
412
     * returns it. This will actually remove the item from the collection.
413
     *
414
     * @param boolean $discard Whether to discard the popped item and return
415
     *     $this instead of the default behavior
416
     * @return mixed Whatever the last item in the collection is
417
     */
418 1
    public function pop($discard = false)
419
    {
420 1
        $popped = array_pop($this->data);
421 1
        return ($discard) ? $this : $popped;
422
    }
423
424
    /**
425
     * "Shift" an item from the top of a collection.
426
     *
427
     * Removes an item from the top of the collection's underlying array and
428
     * returns it. This will actually remove the item from the collection.
429
     *
430
     * @param boolean $discard Whether to discard the shifted item and return
431
     *     $this instead of the default behavior
432
     * @return mixed Whatever the first item in the collection is
433
     */
434 22
    public function shift($discard = false)
435
    {
436 22
        $shifted = array_shift($this->data);
437 22
        return ($discard) ? $this : $shifted;
438
    }
439
440
    /**
441
     * "Push" an item onto the end of the collection.
442
     *
443
     * Adds item(s) to the end of the collection's underlying array.
444
     *
445
     * @param mixed ... The item(s) to push onto the end of the collection. You may
446
     *     also add additional arguments to push multiple items onto the end
447
     * @return $this
448
     */
449 22
    public function push()
450
    {
451 22
        foreach (func_get_args() as $arg) {
452 22
            array_push($this->data, $arg);
453 22
        }
454 22
        return $this;
455
    }
456
457
    /**
458
     * "Unshift" an item onto the beginning of the collection.
459
     *
460
     * Adds item(s) to the beginning of the collection's underlying array.
461
     *
462
     * @param mixed ... The item(s) to push onto the top of the collection. You may
463
     *     also add additional arguments to add multiple items
464
     * @return $this
465
     */
466 1
    public function unshift()
467
    {
468 1
        foreach (array_reverse(func_get_args()) as $arg) {
469 1
            array_unshift($this->data, $arg);
470 1
        }
471 1
        return $this;
472
    }
473
474
    /**
475
     * "Insert" an item at a given numerical position.
476
     *
477
     * Regardless of how the collection is keyed (numerically or otherwise), this
478
     * method will insert an item at a given numerical position. If the given
479
     * position is more than there are items in the collection, the given item
480
     * will simply be added to the end. Nothing is overwritten with this method.
481
     * All elements that come after $offset will simply be shifted a space.
482
     *
483
     * Note: This method is one of the few that will modify the collection in
484
     *       place rather than returning a new one.
485
     *
486
     * @param mixed ... The item(s) to push onto the top of the collection. You may
487
     *     also add additional arguments to add multiple items
488
     * @return $this
489
     */
490 1
    public function insert($offset, $item)
491
    {
492 1
        $top = array_slice($this->data, 0, $offset);
493 1
        $bottom = array_slice($this->data, $offset);
494 1
        $this->data = array_merge($top, [$item], $bottom);
495 1
        return $this;
496
    }
497
498
    /**
499
     * Pad collection to specified length.
500
     *
501
     * Pad the collection to a specific length, filling it with a given value. A
502
     * new collection with padded values is returned.
503
     *
504
     * @param  int $size The number of values you want this collection to have
505
     * @param  any $with The value you want to pad the collection with
506
     * @return \CSVelte\Collection A new collection, padded to specified size
507
     */
508 1
    public function pad($size, $with = null)
509
    {
510 1
        return new self(array_pad($this->data, (int) $size, $with));
511
    }
512
513
    /**
514
     * Check if this collection has a value at the given key.
515
     *
516
     * If this is a tabular data collection, this will check if the table has the
517
     * given key by default. You can change this behavior by passing false as the
518
     * second argument (this will change the behavior to check for a given key
519
     * at the row-level so it will likely only ever be numerical).
520
     *
521
     * @param any $key The key you want to check
522
     * @return boolean Whether there's a value at $key
523
     */
524 26
    public function has($key, $column = true)
525
    {
526
        // we only need to check one row for the existance of $key because the
527
        // isTabular() method ensures every row has the same keys
528 26
        if ($column && $this->isTabular() && $first = reset($this->data)) {
529 3
            return array_key_exists($key, $first);
530
        }
531
        // if we don't have tabular data or we don't want to check for a column...
532 23
        return array_key_exists($key, $this->data);
533
    }
534
535
    /**
536
     * Get the value at the given key.
537
     *
538
     * If there is a value at the given key, it will be returned. If there isn't,
539
     * a default may be specified. If you would like for this method to throw an
540
     * exception when there is no value at $key, pass true as the third argument
541
     *
542
     * @param  any  $key      The key you want to test for
543
     * @param  any  $default  The default to return if there is no value at $key
544
     * @param  boolean $throwExc Whether to throw an exception on failure to find
545
     *     a value at the given key.
546
     * @return mixed            Either the value at $key or the specified default
547
     *     value
548
     * @throws \OutOfBoundsException If value can't be found at $key and $throwExc
549
     *     is set to true
550
     */
551 34
    public function get($key, $default = null, $throwExc = false)
552
    {
553 34
        if (array_key_exists($key, $this->data)) {
554 32
            return $this->data[$key];
555
        } else {
556 7
            if ($throwExc) {
557 6
                throw new OutOfBoundsException("Collection data does not contain value for given key: " . $key);
558
            }
559
        }
560 2
        return $default;
561
    }
562
563
    /**
564
     * Set value for the given key.
565
     *
566
     * Given $key, this will set $this->data[$key] to the value of $val. If that
567
     * index already has a value, it will be overwritten unless $overwrite is set
568
     * to false. In that case nothing happens.
569
     *
570
     * @param any $key The key you want to set a value for
571
     * @param any $value The value you want to set key to
572
     * @param boolean $overwrite Whether to overwrite existing value
573
     * @return $this
574
     */
575 24
    public function set($key, $value = null, $overwrite = true)
576
    {
577 24
        if (!array_key_exists($key, $this->data) || $overwrite) {
578 24
            $this->data[$key] = $value;
579 24
        }
580 24
        return $this;
581
    }
582
583
    /**
584
     * Unset value at the given offset.
585
     *
586
     * This method is used when the end-user uses a colleciton as an array and
587
     * calls unset($collection[5]).
588
     *
589
     * @param mixed $offset The offset at which to unset
590
     * @return $this
591
     * @todo create an alias for this... maybe delete() or remove()
592
     */
593 1
    public function offsetUnset($offset)
594
    {
595 1
        if ($this->has($offset)) {
596 1
            unset($this->data[$offset]);
597 1
        }
598 1
        return $this;
599
    }
600
601
    /**
602
     * Alias of self::has
603
     *
604
     * @param int|mixed The offset to test for
605
     * @return boolean Whether a value exists at $offset
606
     */
607
    public function offsetExists($offset)
608
    {
609
        return $this->has($offset);
610
    }
611
612
    /**
613
     * Alias of self::set
614
     *
615
     * @param int|mixed The offset to set
616
     * @param any The value to set it to
617
     * @return boolean
618
     */
619
    public function offsetSet($offset, $value)
620
    {
621
        $this->set($offset, $value);
622
        return $this;
623
    }
624
625
    /**
626
     * Alias of self::get
627
     *
628
     * @param int|mixed The offset to get
629
     * @return mixed The value at $offset
630
     */
631
    public function offsetGet($offset)
632
    {
633
        return $this->get($offset);
634
    }
635
636
    /**
637
     * Increment an item.
638
     *
639
     * Increment the item specified by $key by one value. Intended for integers
640
     * but also works (using this term loosely) for letters. Any other data type
641
     * it may modify is unintended behavior at best.
642
     *
643
     * This method modifies its internal data array rather than returning a new
644
     * collection.
645
     *
646
     * @param  mixed $key The key of the item you want to increment.
647
     * @return $this
648
     */
649 3
    public function increment($key)
650
    {
651 3
        $val = $this->get($key, null, true);
652 3
        $this->set($key, ++$val);
0 ignored issues
show
Documentation introduced by
++$val is of type integer|double, but the function expects a object<CSVelte\any>|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...
653 3
        return $this;
654
    }
655
656
    /**
657
     * Decrement an item.
658
     *
659
     * Frcrement the item specified by $key by one value. Intended for integers.
660
     * Does not work for letters and if it does anything to anything else, it's
661
     * unintended at best.
662
     *
663
     * This method modifies its internal data array rather than returning a new
664
     * collection.
665
     *
666
     * @param  mixed $key The key of the item you want to decrement.
667
     * @return $this
668
     */
669 1
    public function decrement($key)
670
    {
671 1
        $val = $this->get($key, null, true);
672 1
        $this->set($key, --$val);
0 ignored issues
show
Documentation introduced by
--$val is of type integer|double, but the function expects a object<CSVelte\any>|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...
673 1
        return $this;
674
    }
675
676
    /**
677
     * Count the items in this collection.
678
     *
679
     * Returns either the number of items in the collection or, if this is a
680
     * collection of tabular data, and you pass true as the first argument, you
681
     * will get back a collection containing the count of each row (which will
682
     * always be the same so maybe I should still just return an integer).
683
     *
684
     * @param boolean $multi Whether to count just the items in the collection or
685
     *     to count the items in each tabular data row.
686
     * @return int|\CSVelte\Collection Either an integer count or a collection of counts
687
     */
688 44
    public function count($multi = false)
689
    {
690 44
        if ($multi) {
691
            // if every value is an array...
692 1
            if (false !== ($condRet = $this->if2DMapInternalMethod(__METHOD__))) {
693 1
                return $condRet;
1 ignored issue
show
Bug Best Practice introduced by
The return type of return $condRet; (CSVelte\Collection) is incompatible with the return type declared by the interface Countable::count of type integer.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
694
            }
695
        }
696
        // just count main array
697 44
        return count($this->data);
698
    }
699
700
    /**
701
     * Collection map.
702
     *
703
     * Apply a callback to each element in the collection and return the
704
     * resulting collection. The resulting collection will contain the return
705
     * values of each call to $callback.
706
     *
707
     * @param Callable $callback A callback to apply to each item in the collection
708
     * @return \CSVelte\Collection A collection of callback return values
709
     */
710 33
    public function map(Callable $callback)
711
    {
712 33
        return new self(array_map($callback, $this->data));
713
    }
714
715
    /**
716
     * Walk the collection.
717
     *
718
     * Walk through each item in the collection, calling a function for each
719
     * item in the collection. This is one of the few methods that doesn't return
720
     * a new collection. All changes will be to the existing collection object.
721
     *
722
     * Note: return false from the collback to stop walking.
723
     *
724
     * @param Callable $callback A callback function to call for each item in the collection
725
     * @param any $userdata Any extra data you'd like passed to your callback
726
     * @return $this
727
     */
728 34
    public function walk(Callable $callback, $userdata = null)
729
    {
730 34
        array_walk($this->data, $callback, $userdata);
731 34
        return $this;
732
    }
733
734
    /**
735
     * Call a user function for each item in the collection. If function returns
736
     * false, loop is terminated.
737
     *
738
     * @return $this
739
     * @todo I'm not entirely sure what this method should do... return new
740
     *     collection? modify this one?
741
     * @todo This method appears to be a duplicate of walk(). Is it even necessary?
742
     */
743 2
    public function each(Callable $callback)
744
    {
745 2
        foreach ($this->data as $key => $val) {
746 2
            if (!$ret = $callback($val, $key)) {
747 2
                if ($ret === false) break;
748 2
            }
749 2
        }
750 2
        return $this;
751
    }
752
753
    /**
754
     * Reduce collection to single value.
755
     *
756
     * Reduces the collection to a single value by calling a callback for each
757
     * item in the collection, carrying along an accumulative value as it does so.
758
     * The final value is then returned.
759
     *
760
     * @param Callable $callback The function to reduce the collection
761
     * @param any $initial The initial value to set the accumulative value to
762
     * @return mixed Whatever the final value from the callback is
763
     */
764
    public function reduce(Callable $callback, $initial = null)
765
    {
766
        return array_reduce($this->data, $callback, $initial);
767
    }
768
769
    /**
770
     * Filter out unwanted items using a callback function.
771
     *
772
     * @param Callable $callback
773
     * @return CSVelte\Collection A new collection with filtered items removed
774
     */
775 23
    public function filter(Callable $callback)
776
    {
777 23
        $keys = [];
778 23
        foreach ($this->data as $key => $val) {
779 23
            if (false === $callback($val, $key)) $keys[$key] = true;
780 23
        }
781 23
        return new self(array_diff_key($this->data, $keys));
782
    }
783
784
    /**
785
     * Get first match.
786
     *
787
     * Get first value that meets criteria specified with $callback function.
788
     *
789
     * @param Callable $callback A callback with arguments ($val, $key). If it
790
     *     returns true, that $val will be returned.
791
     * @return mixed The first $val that meets criteria specified with $callback
792
     */
793 1
    public function first(Callable $callback)
794
    {
795 1
        foreach ($this->data as $key => $val) {
796 1
            if ($callback($val, $key)) return $val;
797 1
        }
798
        return null;
799
    }
800
801
    /**
802
     * Get last match.
803
     *
804
     * Get last value that meets criteria specified with $callback function.
805
     *
806
     * @param Callable $callback A callback with arguments ($val, $key). If it
807
     *     returns true, that $val will be returned.
808
     * @return mixed The last $val that meets criteria specified with $callback
809
     */
810 1
    public function last(Callable $callback)
811
    {
812 1
        $elem = null;
813 1
        foreach ($this->data as $key => $val) {
814 1
            if ($callback($val, $key)) $elem = $val;
815 1
        }
816 1
        return $elem;
817
    }
818
819
    /**
820
     * Collection value frequency.
821
     *
822
     * Returns an array where the key is a value in the collection and the value
823
     * is the number of times that value appears in the collection.
824
     *
825
     * @return CSVelte\Collection A collection of value frequencies (see description)
826
     */
827 23
    public function frequency()
828
    {
829 23
        if (false !== ($condRet = $this->if2DMapInternalMethod(__METHOD__))) {
830 22
            return $condRet;
831
        }
832 23
        $freq = [];
833 23
        foreach ($this->data as $val) {
834 23
            $key = is_numeric($val) ? $val : (string) $val;
835 23
            if (!isset($freq[$key])) {
836 23
                $freq[$key] = 0;
837 23
            }
838 23
            $freq[$key]++;
839 23
        }
840 23
        return new self($freq);
841
    }
842
843
    /**
844
     * Unique collection.
845
     *
846
     * Returns a collection with duplicate values removed. If two-dimensional,
847
     * then each array within the collection will have its duplicates removed.
848
     *
849
     * @return CSVelte\Collection A new collection with duplicate values removed.
850
     */
851 23
    public function unique()
852
    {
853 23
        if (false !== ($condRet = $this->if2DMapInternalMethod(__METHOD__))) {
854 5
            return $condRet;
855
        }
856 21
        return new self(array_unique($this->data));
857
    }
858
859
    /**
860
     * Get duplicate values.
861
     *
862
     * Returns a collection of arrays where the key is the duplicate value
863
     * and the value is an array of keys from the original collection.
864
     *
865
     * @return CSVelte\Collection A new collection with duplicate values.
866
     */
867 6
    public function duplicates()
868
    {
869 6
        $dups = [];
870
        $this->walk(function($val, $key) use (&$dups) {
871 6
            $dups[$val][] = $key;
872 6
        });
873
        return (new self($dups))->filter(function($val, $key) {
0 ignored issues
show
Unused Code introduced by
The parameter $key is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
874 6
            return (count($val) > 1);
875 6
        });
876
    }
877
878
    /**
879
     * Reverse keys/values.
880
     *
881
     * Get a new collection where the keys and values have been swapped.
882
     *
883
     * @return CSVelte\Collection A new collection where keys/values have been swapped
884
     */
885 1
    public function flip()
886
    {
887 1
        return new self(array_flip($this->data));
888
    }
889
890
    /**
891
     * Return an array of key/value pairs.
892
     *
893
     * Return array can either be in [key,value] or [key => value] format. The
894
     * first is the default.
895
     *
896
     * @param boolean Whether you want pairs in [k => v] rather than [k, v] format
897
     * @return CSVelte\Collection A collection of key/value pairs
898
     */
899 1
    public function pairs($alt = false)
900
    {
901 1
        return new self(array_map(
902
            function ($key, $val) use ($alt) {
903 1
                if ($alt) {
904 1
                    return [$key => $val];
905
                } else {
906 1
                    return [$key, $val];
907
                }
908 1
            },
909 1
            array_keys($this->data),
910 1
            array_values($this->data)
911 1
        ));
912
    }
913
914
    /**
915
     * Get average of data items.
916
     *
917
     * @return mixed The average of all items in collection
918
     */
919 2
    public function sum()
920
    {
921 2
        if (false !== ($condRet = $this->if2DMapInternalMethod(__METHOD__))) {
922 1
            return $condRet;
923
        }
924 2
        $this->assertNumericValues();
925 2
        return array_sum($this->data);
926
    }
927
928
    /**
929
     * Get average of data items.
930
     *
931
     * If two-dimensional it will return a collection of averages.
932
     *
933
     * @return mixed|CSVelte\Collection The average of all items in collection
934
     */
935 8
    public function average()
936
    {
937 8
        if (false !== ($condRet = $this->if2DMapInternalMethod(__METHOD__))) {
938 6
            return $condRet;
939
        }
940 8
        $this->assertNumericValues();
941 8
        $total = array_sum($this->data);
942 8
        $count = count($this->data);
943 8
        return $total / $count;
944
    }
945
946
    /**
947
     * Get largest item in the collection
948
     *
949
     * @return mixed The largest item in the collection
950
     */
951 8
    public function max()
952
    {
953 8
        if (false !== ($condRet = $this->if2DMapInternalMethod(__METHOD__))) {
954 1
            return $condRet;
955
        }
956 8
        $this->assertNumericValues();
957 8
        return max($this->data);
958
    }
959
960
    /**
961
     * Get smallest item in the collection
962
     *
963
     * @return mixed The smallest item in the collection
964
     */
965 5
    public function min()
966
    {
967 5
        if (false !== ($condRet = $this->if2DMapInternalMethod(__METHOD__))) {
968 1
            return $condRet;
969
        }
970 5
        $this->assertNumericValues();
971 5
        return min($this->data);
972
    }
973
974
    /**
975
     * Get mode of data items.
976
     *
977
     * @return mixed The mode of all items in collection
978
     */
979 8
    public function mode()
980
    {
981 8
        if (false !== ($condRet = $this->if2DMapInternalMethod(__METHOD__))) {
982 6
            return $condRet;
983
        }
984
        $strvals = $this->map(function($val){
985 8
            return (string) $val;
986 8
        });
987 8
        $this->assertNumericValues();
988 7
        $counts = array_count_values($strvals->toArray());
989 7
        arsort($counts);
990 7
        $mode = key($counts);
991 7
        return (strpos($mode, '.')) ? floatval($mode) : intval($mode);
992
    }
993
994
    /**
995
     * Get median of data items.
996
     *
997
     * @return mixed The median of all items in collection
998
     */
999 2
    public function median()
1000
    {
1001 2
        if (false !== ($condRet = $this->if2DMapInternalMethod(__METHOD__))) {
1002 1
            return $condRet;
1003
        }
1004 2
        $this->assertNumericValues();
1005 2
        $count = count($this->data);
1006 2
        natcasesort($this->data);
1007 2
        $middle = ($count / 2);
1008 2
        $values = array_values($this->data);
1009 2
        if ($count % 2 == 0) {
1010
            // even number, use middle
1011 2
            $low = $values[$middle - 1];
1012 2
            $high = $values[$middle];
1013 2
            return ($low + $high) / 2;
1014
        }
1015
        // odd number return median
1016 2
        return $values[$middle];
1017
    }
1018
1019
    /**
1020
     * Join items together into a string
1021
     *
1022
     * @param string $glue The string to join items together with
1023
     * @return string A string with all items in the collection strung together
1024
     * @todo Make this work with 2D collection
1025
     */
1026 18
    public function join($glue)
1027
    {
1028 18
        return implode($glue, $this->data);
1029
    }
1030
1031
    /**
1032
     * Is the collection empty?
1033
     *
1034
     * @return boolean Whether the collection is empty
1035
     */
1036 1
    public function isEmpty()
1037
    {
1038 1
        return empty($this->data);
1039
    }
1040
1041
    /**
1042
     * Immediately invoke a callback.
1043
     *
1044
     * @param Callable $callback A callback to invoke with ($this)
1045
     * @return mixed Whatever the callback returns
1046
     */
1047 1
    public function value(Callable $callback)
1048
    {
1049 1
        return $callback($this);
1050
    }
1051
1052
    /**
1053
     * Sort the collection.
1054
     *
1055
     * This method can sort your collection in any which way you please. By
1056
     * default it uses a case-insensitive natural order algorithm, but you can
1057
     * pass it any sorting algorithm you like.
1058
     *
1059
     * @param Callable $sort_func The sorting function you want to use
0 ignored issues
show
Bug introduced by
There is no parameter named $sort_func. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
1060
     * @param boolean $preserve_keys Whether you want to preserve keys
1061
     * @return CSVelte\Collection A new collection sorted by $callback
1062
     */
1063 23
    public function sort(Callable $callback = null, $preserve_keys = true)
1064
    {
1065 23
        if (is_null($callback)) $callback = 'strcasecmp';
1066 23
        if (!is_callable($callback)) {
1067
            throw new InvalidArgumentException(sprintf(
1068
                'Invalid argument supplied for %s. Expected %s, got: "%s".',
1069
                __METHOD__,
1070
                'Callable',
1071
                gettype($callback)
1072
            ));
1073
        }
1074 23
        $data = $this->data;
1075 23
        if ($preserve_keys) {
1076 23
            uasort($data, $callback);
1077 23
        } else {
1078 2
            usort($data, $callback);
1079
        }
1080 23
        return new self($data);
1081
    }
1082
1083
    /**
1084
     * Order tabular data.
1085
     *
1086
     * Order a tabular dataset by a given key/comparison algorithm
1087
     *
1088
     * @param string $key The key you want to order by
1089
     * @param Callable $cmp The sorting comparison algorithm to use
1090
     * @param boolean $preserve_keys Whether keys should be preserved
1091
     * @return CSVelte\Collection A new collection sorted by $cmp and $key
1092
     */
1093 1
    public function orderBy($key, Callable $cmp = null, $preserve_keys = true)
1094
    {
1095 1
        $this->assertIsTabular();
1096
        return $this->sort(function($a, $b) use ($key, $cmp) {
1097 1
            if (!isset($a[$key]) || !isset($b[$key])) {
1098
                throw new RuntimeException('Cannot order collection by non-existant key: ' . $key);
1099
            }
1100 1
            if (is_null($cmp)) {
1101 1
                return strcasecmp($a[$key], $b[$key]);
1102
            } else {
1103 1
                return $cmp($a[$key], $b[$key]);
1104
            }
1105 1
        }, $preserve_keys);
1106
    }
1107
1108
    /**
1109
     * Reverse collection order.
1110
     *
1111
     * Reverse the order of items in a collection. Sometimes it's easier than
1112
     * trying to write a particular sorting algurithm that sorts forwards and back.
1113
     *
1114
     * @param boolean $preserve_keys Whether keys should be preserved
1115
     * @return CSVelte\Collection A new collection in reverse order
1116
     */
1117 22
    public function reverse($preserve_keys = true)
1118
    {
1119 22
        return new self(array_reverse($this->data, $preserve_keys));
1120
    }
1121
1122 39
    protected function if2DMapInternalMethod($method)
1123
    {
1124 39
        if ($this->is2D()) {
1125 29
            $method = explode('::', $method, 2);
1126 29
            if (count($method) == 2) {
1127 29
                $method = $method[1];
1128
                return $this->map(function($val) use ($method) {
1129 29
                    return (new self($val))->$method();
1130 29
                });
1131
            }
1132
        }
1133 38
        return false;
1134
    }
1135
1136
    /**
1137
     * Is this collection two-dimensional
1138
     *
1139
     * If all items of the collection are arrays this will return true.
1140
     *
1141
     * @return boolean whether this is two-dimensional
1142
     */
1143 46
    public function is2D()
1144
    {
1145
        return !$this->contains(function($val){
1146 46
            return !is_array($val);
1147 46
        });
1148
        return false;
0 ignored issues
show
Unused Code introduced by
return false; does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
1149
    }
1150
1151
    /**
1152
     * Is this a tabular collection?
1153
     *
1154
     * If this is a two-dimensional collection with the same keys in every array,
1155
     * this method will return true.
1156
     *
1157
     * @return boolean Whether this is a tabular collection
1158
     */
1159 35
    public function isTabular()
1160
    {
1161 35
        if ($this->is2D()) {
1162
            // look through each item in the collection and if an array, grab its keys
1163
            // and throw them in an array to be analyzed later...
1164 32
            $test = [];
1165
            $this->walk(function($val, $key) use (&$test) {
1166 11
                if (is_array($val)) {
1167 11
                    $test[$key] = array_keys($val);
1168 11
                    return true;
1169
                }
1170
                return false;
1171 32
            });
1172
1173
            // if the list of array keys is shorter than the total amount of items in
1174
            // the collection, than this is not tabular data
1175 32
            if (count($test) != count($this)) return false;
1176
1177
            // loop through the array of each item's array keys that we just created
1178
            // and compare it to the FIRST item. If any array contains different keys
1179
            // than this is not tabular data.
1180 32
            $first = array_shift($test);
1181 32
            foreach ($test as $key => $keys) {
1182 11
                $diff = array_diff($first, $keys);
1183 11
                if (!empty($diff)) return false;
1184 28
            }
1185 28
            return true;
1186
        }
1187 24
        return false;
1188
    }
1189
1190
    /**
1191
     * Assert this collection is two-dimensional.
1192
     *
1193
     * Although a collection must be two-dimensional to be tabular, the opposite
1194
     * is not necessarily true. This will throw an exception if this collection
1195
     * contains anything but arrays.
1196
     *
1197
     * @throws
1198
     */
1199
    protected function assertIs2D()
1200
    {
1201
        if (!$this->is2D()) {
1202
            throw new RuntimeException('Invalid data type, requires two-dimensional array.');
1203
        }
1204
    }
1205
1206 5
    protected function assertIsTabular()
1207
    {
1208 5
        if (!$this->isTabular()) {
1209 1
            throw new RuntimeException('Invalid data type, requires tabular data (two-dimensional array where each sub-array has the same keys).');
1210
        }
1211 4
    }
1212
1213
    protected function assertNumericValues()
1214
    {
1215 18
        if ($this->contains(function($val){
1216 18
            return !is_numeric($val);
1217 18
        })) {
1218
            // can't average non-numeric data
1219 1
            throw new InvalidArgumentException(sprintf(
1220 1
                "%s expects collection of integers or collection of arrays of integers",
1221
                __METHOD__
1222 1
            ));
1223
        }
1224 17
    }
1225
1226 127
    protected function assertArrayOrIterator($data)
1227
    {
1228 127
        if (is_null($data) || is_array($data) || $data instanceof Iterator) {
1229 126
            return;
1230
        }
1231 1
        throw new InvalidArgumentException("Invalid type for collection data: " . gettype($data));
1232
    }
1233
}
1234