Passed
Push — less-original ( 3c06b4...67fe41 )
by Sam
07:41 queued 24s
created

ArrayList::shift()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\ORM;
4
5
use SilverStripe\Dev\Debug;
6
use SilverStripe\View\ArrayData;
7
use SilverStripe\View\ViewableData;
8
use ArrayIterator;
9
use InvalidArgumentException;
10
use LogicException;
11
use SilverStripe\Dev\Deprecation;
12
13
/**
14
 * A list object that wraps around an array of objects or arrays.
15
 *
16
 * Note that (like DataLists), the implementations of the methods from SS_Filterable, SS_Sortable and
17
 * SS_Limitable return a new instance of ArrayList, rather than modifying the existing instance.
18
 *
19
 * For easy reference, methods that operate in this way are:
20
 *
21
 *   - limit
22
 *   - reverse
23
 *   - sort
24
 *   - filter
25
 *   - exclude
26
 */
27
class ArrayList extends ViewableData implements SS_List, Filterable, Sortable, Limitable
28
{
29
30
    /**
31
     * Holds the items in the list
32
     *
33
     * @var array
34
     */
35
    protected $items = [];
36
37
    /**
38
     *
39
     * @param array $items - an initial array to fill this object with
40
     */
41
    public function __construct(array $items = [])
42
    {
43
        $this->items = array_values($items);
44
        parent::__construct();
45
    }
46
47
    /**
48
     * Return the class of items in this list, by looking at the first item inside it.
49
     *
50
     * @return string
51
     */
52
    public function dataClass()
53
    {
54
        if (count($this->items) > 0) {
55
            return get_class($this->items[0]);
56
        }
57
        return null;
58
    }
59
60
    /**
61
     * Return the number of items in this list
62
     *
63
     * @return int
64
     */
65
    public function count()
66
    {
67
        return count($this->items);
68
    }
69
70
    /**
71
     * Returns true if this list has items
72
     *
73
     * @return bool
74
     */
75
    public function exists()
76
    {
77
        return !empty($this->items);
78
    }
79
80
    /**
81
     * Returns an Iterator for this ArrayList.
82
     * This function allows you to use ArrayList in foreach loops
83
     *
84
     * @return ArrayIterator
85
     */
86
    public function getIterator()
87
    {
88
        $items = array_map(
89
            function ($item) {
90
                return is_array($item) ? new ArrayData($item) : $item;
91
            },
92
            $this->items
93
        );
94
        return new ArrayIterator($items);
95
    }
96
97
    /**
98
     * Return an array of the actual items that this ArrayList contains.
99
     *
100
     * @return array
101
     */
102
    public function toArray()
103
    {
104
        return $this->items;
105
    }
106
107
    /**
108
     * Walks the list using the specified callback
109
     *
110
     * @param callable $callback
111
     * @return $this
112
     */
113
    public function each($callback)
114
    {
115
        foreach ($this as $item) {
116
            $callback($item);
117
        }
118
        return $this;
119
    }
120
121
    public function debug()
122
    {
123
        $val = "<h2>" . static::class . "</h2><ul>";
124
        foreach ($this->toNestedArray() as $item) {
125
            $val .= "<li style=\"list-style-type: disc; margin-left: 20px\">" . Debug::text($item) . "</li>";
126
        }
127
        $val .= "</ul>";
128
        return $val;
129
    }
130
131
    /**
132
     * Return this list as an array and every object it as an sub array as well
133
     *
134
     * @return array
135
     */
136
    public function toNestedArray()
137
    {
138
        $result = [];
139
140
        foreach ($this->items as $item) {
141
            if (is_object($item)) {
142
                if (method_exists($item, 'toMap')) {
143
                    $result[] = $item->toMap();
144
                } else {
145
                    $result[] = (array) $item;
146
                }
147
            } else {
148
                $result[] = $item;
149
            }
150
        }
151
152
        return $result;
153
    }
154
155
    /**
156
     * Get a sub-range of this dataobjectset as an array
157
     *
158
     * @param int $length
159
     * @param int $offset
160
     * @return static
161
     */
162
    public function limit($length, $offset = 0)
163
    {
164
        // Type checking: designed for consistency with DataList::limit()
165
        if (!is_numeric($length) || !is_numeric($offset)) {
0 ignored issues
show
introduced by
The condition is_numeric($length) is always true.
Loading history...
introduced by
The condition is_numeric($offset) is always true.
Loading history...
166
            Deprecation::notice(
167
                '4.3',
168
                'Arguments to ArrayList::limit() should be numeric'
169
            );
170
        }
171
172
        if ($length < 0 || $offset < 0) {
173
            Deprecation::notice(
174
                '4.3',
175
                'Arguments to ArrayList::limit() should be positive'
176
            );
177
        }
178
179
        if (!$length) {
180
            if ($length === 0) {
181
                Deprecation::notice(
182
                    '4.3',
183
                    "limit(0) is deprecated in SS4. In SS5 a limit of 0 will instead return no records."
184
                );
185
            }
186
187
            $length = count($this->items);
188
        }
189
190
        $list = clone $this;
191
        $list->items = array_slice($this->items, $offset, $length);
192
193
        return $list;
194
    }
195
196
    /**
197
     * Add this $item into this list
198
     *
199
     * @param mixed $item
200
     */
201
    public function add($item)
202
    {
203
        $this->push($item);
204
    }
205
206
    /**
207
     * Remove this item from this list
208
     *
209
     * @param mixed $item
210
     */
211
    public function remove($item)
212
    {
213
        $renumberKeys = false;
214
        foreach ($this->items as $key => $value) {
215
            if ($item === $value) {
216
                $renumberKeys = true;
217
                unset($this->items[$key]);
218
            }
219
        }
220
        if ($renumberKeys) {
221
            $this->items = array_values($this->items);
222
        }
223
    }
224
225
    /**
226
     * Replaces an item in this list with another item.
227
     *
228
     * @param array|object $item
229
     * @param array|object $with
230
     * @return void;
231
     */
232
    public function replace($item, $with)
233
    {
234
        foreach ($this->items as $key => $candidate) {
235
            if ($candidate === $item) {
236
                $this->items[$key] = $with;
237
                return;
238
            }
239
        }
240
    }
241
242
    /**
243
     * Merges with another array or list by pushing all the items in it onto the
244
     * end of this list.
245
     *
246
     * @param array|object $with
247
     */
248
    public function merge($with)
249
    {
250
        foreach ($with as $item) {
251
            $this->push($item);
252
        }
253
    }
254
255
    /**
256
     * Removes items from this list which have a duplicate value for a certain
257
     * field. This is especially useful when combining lists.
258
     *
259
     * @param string $field
260
     */
261
    public function removeDuplicates($field = 'ID')
262
    {
263
        $seen = [];
264
        $renumberKeys = false;
265
266
        foreach ($this->items as $key => $item) {
267
            $value = $this->extractValue($item, $field);
268
269
            if (array_key_exists($value, $seen)) {
270
                $renumberKeys = true;
271
                unset($this->items[$key]);
272
            }
273
274
            $seen[$value] = true;
275
        }
276
277
        if ($renumberKeys) {
278
            $this->items = array_values($this->items);
279
        }
280
281
        return $this;
282
    }
283
284
    /**
285
     * Pushes an item onto the end of this list.
286
     *
287
     * @param array|object $item
288
     */
289
    public function push($item)
290
    {
291
        $this->items[] = $item;
292
    }
293
294
    /**
295
     * Pops the last element off the end of the list and returns it.
296
     *
297
     * @return array|object
298
     */
299
    public function pop()
300
    {
301
        return array_pop($this->items);
302
    }
303
304
    /**
305
     * Add an item onto the beginning of the list.
306
     *
307
     * @param array|object $item
308
     */
309
    public function unshift($item)
310
    {
311
        array_unshift($this->items, $item);
312
    }
313
314
    /**
315
     * Shifts the item off the beginning of the list and returns it.
316
     *
317
     * @return array|object
318
     */
319
    public function shift()
320
    {
321
        return array_shift($this->items);
322
    }
323
324
    /**
325
     * Returns the first item in the list
326
     *
327
     * @return mixed
328
     */
329
    public function first()
330
    {
331
        if (empty($this->items)) {
332
            return null;
333
        }
334
335
        return reset($this->items);
336
    }
337
338
    /**
339
     * Returns the last item in the list
340
     *
341
     * @return mixed
342
     */
343
    public function last()
344
    {
345
        if (empty($this->items)) {
346
            return null;
347
        }
348
349
        return end($this->items);
350
    }
351
352
    /**
353
     * Returns a map of this list
354
     *
355
     * @param string $keyfield The 'key' field of the result array
356
     * @param string $titlefield The value field of the result array
357
     * @return Map
358
     */
359
    public function map($keyfield = 'ID', $titlefield = 'Title')
360
    {
361
        $list = clone $this;
362
        return new Map($list, $keyfield, $titlefield);
363
    }
364
365
    /**
366
     * Find the first item of this list where the given key = value
367
     *
368
     * @param string $key
369
     * @param string $value
370
     * @return mixed
371
     */
372
    public function find($key, $value)
373
    {
374
        foreach ($this->items as $item) {
375
            if ($this->extractValue($item, $key) == $value) {
376
                return $item;
377
            }
378
        }
379
        return null;
380
    }
381
382
    /**
383
     * Returns an array of a single field value for all items in the list.
384
     *
385
     * @param string $colName
386
     * @return array
387
     */
388
    public function column($colName = 'ID')
389
    {
390
        $result = [];
391
392
        foreach ($this->items as $item) {
393
            $result[] = $this->extractValue($item, $colName);
394
        }
395
396
        return $result;
397
    }
398
399
    /**
400
     * Returns a unique array of a single field value for all the items in the list
401
     *
402
     * @param string $colName
403
     * @return array
404
     */
405
    public function columnUnique($colName = 'ID')
406
    {
407
        return array_unique($this->column($colName));
408
    }
409
410
    /**
411
     * You can always sort a ArrayList
412
     *
413
     * @param string $by
414
     * @return bool
415
     */
416
    public function canSortBy($by)
417
    {
418
        return true;
419
    }
420
421
    /**
422
     * Reverses an {@link ArrayList}
423
     *
424
     * @return ArrayList
425
     */
426
    public function reverse()
427
    {
428
        $list = clone $this;
429
        $list->items = array_reverse($this->items);
430
431
        return $list;
432
    }
433
434
    /**
435
     * Parses a specified column into a sort field and direction
436
     *
437
     * @param string $column String to parse containing the column name
438
     * @param mixed $direction Optional Additional argument which may contain the direction
439
     * @return array Sort specification in the form array("Column", SORT_ASC).
440
     */
441
    protected function parseSortColumn($column, $direction = null)
442
    {
443
        // Substitute the direction for the column if column is a numeric index
444
        if ($direction && (empty($column) || is_numeric($column))) {
445
            $column = $direction;
446
            $direction = null;
447
        }
448
449
        // Parse column specification, considering possible ansi sql quoting
450
        // Note that table prefix is allowed, but discarded
451
        if (preg_match('/^("?(?<table>[^"\s]+)"?\\.)?"?(?<column>[^"\s]+)"?(\s+(?<direction>((asc)|(desc))(ending)?))?$/i', $column, $match)) {
452
            $column = $match['column'];
453
            if (empty($direction) && !empty($match['direction'])) {
454
                $direction = $match['direction'];
455
            }
456
        } else {
457
            throw new InvalidArgumentException("Invalid sort() column");
458
        }
459
460
        // Parse sort direction specification
461
        if (empty($direction) || preg_match('/^asc(ending)?$/i', $direction)) {
462
            $direction = SORT_ASC;
463
        } elseif (preg_match('/^desc(ending)?$/i', $direction)) {
464
            $direction = SORT_DESC;
465
        } else {
466
            throw new InvalidArgumentException("Invalid sort() direction");
467
        }
468
469
        return array($column, $direction);
470
    }
471
472
    /**
473
     * Sorts this list by one or more fields. You can either pass in a single
474
     * field name and direction, or a map of field names to sort directions.
475
     *
476
     * Note that columns may be double quoted as per ANSI sql standard
477
     *
478
     * @return static
479
     * @see SS_List::sort()
480
     * @example $list->sort('Name'); // default ASC sorting
481
     * @example $list->sort('Name DESC'); // DESC sorting
482
     * @example $list->sort('Name', 'ASC');
483
     * @example $list->sort(array('Name'=>'ASC,'Age'=>'DESC'));
484
     */
485
    public function sort()
486
    {
487
        $args = func_get_args();
488
489
        if (count($args)==0) {
490
            return $this;
491
        }
492
        if (count($args)>2) {
493
            throw new InvalidArgumentException('This method takes zero, one or two arguments');
494
        }
495
        $columnsToSort = [];
496
497
        // One argument and it's a string
498
        if (count($args)==1 && is_string($args[0])) {
499
            list($column, $direction) = $this->parseSortColumn($args[0]);
500
            $columnsToSort[$column] = $direction;
501
        } elseif (count($args)==2) {
502
            list($column, $direction) = $this->parseSortColumn($args[0], $args[1]);
503
            $columnsToSort[$column] = $direction;
504
        } elseif (is_array($args[0])) {
505
            foreach ($args[0] as $key => $value) {
506
                list($column, $direction) = $this->parseSortColumn($key, $value);
507
                $columnsToSort[$column] = $direction;
508
            }
509
        } else {
510
            throw new InvalidArgumentException("Bad arguments passed to sort()");
511
        }
512
513
        // Store the original keys of the items as a sort fallback, so we can preserve the original order in the event
514
        // that array_multisort is unable to work out a sort order for them. This also prevents array_multisort trying
515
        // to inspect object properties which can result in errors with circular dependencies
516
        $originalKeys = array_keys($this->items);
517
518
        // This the main sorting algorithm that supports infinite sorting params
519
        $multisortArgs = [];
520
        $values = [];
521
        $firstRun = true;
522
        foreach ($columnsToSort as $column => $direction) {
523
            // The reason these are added to columns is of the references, otherwise when the foreach
524
            // is done, all $values and $direction look the same
525
            $values[$column] = [];
526
            $sortDirection[$column] = $direction;
527
            // We need to subtract every value into a temporary array for sorting
528
            foreach ($this->items as $index => $item) {
529
                $values[$column][] = strtolower($this->extractValue($item, $column));
530
            }
531
            // PHP 5.3 requires below arguments to be reference when using array_multisort together
532
            // with call_user_func_array
533
            // First argument is the 'value' array to be sorted
534
            $multisortArgs[] = &$values[$column];
535
            // First argument is the direction to be sorted,
536
            $multisortArgs[] = &$sortDirection[$column];
537
            if ($firstRun) {
538
                $multisortArgs[] = SORT_REGULAR;
539
            }
540
            $firstRun = false;
541
        }
542
543
        $multisortArgs[] = &$originalKeys;
544
545
        $list = clone $this;
546
        // As the last argument we pass in a reference to the items that all the sorting will be applied upon
547
        $multisortArgs[] = &$list->items;
548
        call_user_func_array('array_multisort', $multisortArgs);
549
        return $list;
550
    }
551
552
    /**
553
     * Returns true if the given column can be used to filter the records.
554
     *
555
     * It works by checking the fields available in the first record of the list.
556
     *
557
     * @param string $by
558
     * @return bool
559
     */
560
    public function canFilterBy($by)
561
    {
562
        if (empty($this->items)) {
563
            return false;
564
        }
565
566
        $firstRecord = $this->first();
567
568
        return array_key_exists($by, $firstRecord);
0 ignored issues
show
Bug introduced by
It seems like $firstRecord can also be of type null; however, parameter $search of array_key_exists() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

568
        return array_key_exists($by, /** @scrutinizer ignore-type */ $firstRecord);
Loading history...
569
    }
570
571
    /**
572
     * Filter the list to include items with these charactaristics
573
     *
574
     * @return ArrayList
575
     * @see SS_List::filter()
576
     * @example $list->filter('Name', 'bob'); // only bob in the list
577
     * @example $list->filter('Name', array('aziz', 'bob'); // aziz and bob in list
578
     * @example $list->filter(array('Name'=>'bob, 'Age'=>21)); // bob with the Age 21 in list
579
     * @example $list->filter(array('Name'=>'bob, 'Age'=>array(21, 43))); // bob with the Age 21 or 43
580
     * @example $list->filter(array('Name'=>array('aziz','bob'), 'Age'=>array(21, 43)));
581
     *          // aziz with the age 21 or 43 and bob with the Age 21 or 43
582
     */
583
    public function filter()
584
    {
585
586
        $keepUs = call_user_func_array([$this, 'normaliseFilterArgs'], func_get_args());
587
588
        $itemsToKeep = [];
589
        foreach ($this->items as $item) {
590
            $keepItem = true;
591
            foreach ($keepUs as $column => $value) {
592
                if ((is_array($value) && !in_array($this->extractValue($item, $column), $value))
593
                    || (!is_array($value) && $this->extractValue($item, $column) != $value)
594
                ) {
595
                    $keepItem = false;
596
                    break;
597
                }
598
            }
599
            if ($keepItem) {
600
                $itemsToKeep[] = $item;
601
            }
602
        }
603
604
        $list = clone $this;
605
        $list->items = $itemsToKeep;
606
        return $list;
607
    }
608
609
    /**
610
     * Return a copy of this list which contains items matching any of these charactaristics.
611
     *
612
     * @example // only bob in the list
613
     *          $list = $list->filterAny('Name', 'bob');
614
     * @example // azis or bob in the list
615
     *          $list = $list->filterAny('Name', array('aziz', 'bob');
616
     * @example // bob or anyone aged 21 in the list
617
     *          $list = $list->filterAny(array('Name'=>'bob, 'Age'=>21));
618
     * @example // bob or anyone aged 21 or 43 in the list
619
     *          $list = $list->filterAny(array('Name'=>'bob, 'Age'=>array(21, 43)));
620
     * @example // all bobs, phils or anyone aged 21 or 43 in the list
621
     *          $list = $list->filterAny(array('Name'=>array('bob','phil'), 'Age'=>array(21, 43)));
622
     *
623
     * @param string|array See {@link filter()}
0 ignored issues
show
Bug introduced by
The type SilverStripe\ORM\See was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
624
     * @return static
625
     */
626
    public function filterAny()
627
    {
628
        $keepUs = $this->normaliseFilterArgs(...func_get_args());
629
630
        $itemsToKeep = [];
631
632
        foreach ($this->items as $item) {
633
            foreach ($keepUs as $column => $value) {
634
                $extractedValue = $this->extractValue($item, $column);
635
                $matches = is_array($value) ? in_array($extractedValue, $value) : $extractedValue == $value;
636
                if ($matches) {
637
                    $itemsToKeep[] = $item;
638
                    break;
639
                }
640
            }
641
        }
642
643
        $list = clone $this;
644
        $list->items = array_unique($itemsToKeep, SORT_REGULAR);
645
        return $list;
646
    }
647
648
    /**
649
     * Take the "standard" arguments that the filter/exclude functions take and return a single array with
650
     * 'colum' => 'value'
651
     *
652
     * @param $column array|string The column name to filter OR an assosicative array of column => value
653
     * @param $value array|string|null The values to filter the $column against
654
     *
655
     * @return array The normalised keyed array
656
     */
657
    protected function normaliseFilterArgs($column, $value = null)
658
    {
659
        $args = func_get_args();
660
        if (count($args) > 2) {
661
            throw new InvalidArgumentException('filter takes one array or two arguments');
662
        }
663
664
        if (count($args) === 1 && !is_array($args[0])) {
665
            throw new InvalidArgumentException('filter takes one array or two arguments');
666
        }
667
668
        $keepUs = [];
669
        if (count($args) === 2) {
670
            $keepUs[$args[0]] = $args[1];
671
        }
672
673
        if (count($args) === 1 && is_array($args[0])) {
674
            foreach ($args[0] as $key => $val) {
675
                $keepUs[$key] = $val;
676
            }
677
        }
678
679
        return $keepUs;
680
    }
681
682
    /**
683
     * Filter this list to only contain the given Primary IDs
684
     *
685
     * @param array $ids Array of integers, will be automatically cast/escaped.
686
     * @return ArrayList
687
     */
688
    public function byIDs($ids)
689
    {
690
        $ids = array_map('intval', $ids); // sanitize
691
        return $this->filter('ID', $ids);
692
    }
693
694
    public function byID($id)
695
    {
696
        $firstElement = $this->filter("ID", $id)->first();
697
698
        if ($firstElement === false) {
699
            return null;
700
        }
701
702
        return $firstElement;
703
    }
704
705
    /**
706
     * @see Filterable::filterByCallback()
707
     *
708
     * @example $list = $list->filterByCallback(function($item, $list) { return $item->Age == 9; })
709
     * @param callable $callback
710
     * @return ArrayList
711
     */
712
    public function filterByCallback($callback)
713
    {
714
        if (!is_callable($callback)) {
715
            throw new LogicException(sprintf(
716
                "SS_Filterable::filterByCallback() passed callback must be callable, '%s' given",
717
                gettype($callback)
718
            ));
719
        }
720
721
        $output = static::create();
722
723
        foreach ($this as $item) {
724
            if (call_user_func($callback, $item, $this)) {
725
                $output->push($item);
726
            }
727
        }
728
729
        return $output;
730
    }
731
732
    /**
733
     * Exclude the list to not contain items with these charactaristics
734
     *
735
     * @return ArrayList
736
     * @see SS_List::exclude()
737
     * @example $list->exclude('Name', 'bob'); // exclude bob from list
738
     * @example $list->exclude('Name', array('aziz', 'bob'); // exclude aziz and bob from list
739
     * @example $list->exclude(array('Name'=>'bob, 'Age'=>21)); // exclude bob that has Age 21
740
     * @example $list->exclude(array('Name'=>'bob, 'Age'=>array(21, 43))); // exclude bob with Age 21 or 43
741
     * @example $list->exclude(array('Name'=>array('bob','phil'), 'Age'=>array(21, 43)));
742
     *          // bob age 21 or 43, phil age 21 or 43 would be excluded
743
     */
744
    public function exclude()
745
    {
746
        $removeUs = $this->normaliseFilterArgs(...func_get_args());
747
748
        $hitsRequiredToRemove = count($removeUs);
749
        $matches = [];
750
        foreach ($removeUs as $column => $excludeValue) {
751
            foreach ($this->items as $key => $item) {
752
                if (!is_array($excludeValue) && $this->extractValue($item, $column) == $excludeValue) {
753
                    $matches[$key] = isset($matches[$key]) ? $matches[$key] + 1 : 1;
754
                } elseif (is_array($excludeValue) && in_array($this->extractValue($item, $column), $excludeValue)) {
755
                    $matches[$key] = isset($matches[$key]) ? $matches[$key] + 1 : 1;
756
                }
757
            }
758
        }
759
760
        $keysToRemove = array_keys($matches, $hitsRequiredToRemove);
761
762
        $itemsToKeep = [];
763
        foreach ($this->items as $key => $value) {
764
            if (!in_array($key, $keysToRemove)) {
765
                $itemsToKeep[] = $value;
766
            }
767
        }
768
769
        $list = clone $this;
770
        $list->items = $itemsToKeep;
771
        return $list;
772
    }
773
774
    protected function shouldExclude($item, $args)
775
    {
776
    }
777
778
779
    /**
780
     * Returns whether an item with $key exists
781
     *
782
     * @param mixed $offset
783
     * @return bool
784
     */
785
    public function offsetExists($offset)
786
    {
787
        return array_key_exists($offset, $this->items);
788
    }
789
790
    /**
791
     * Returns item stored in list with index $key
792
     *
793
     * @param mixed $offset
794
     * @return DataObject
795
     */
796
    public function offsetGet($offset)
797
    {
798
        if ($this->offsetExists($offset)) {
799
            return $this->items[$offset];
800
        }
801
        return null;
802
    }
803
804
    /**
805
     * Set an item with the key in $key
806
     *
807
     * @param mixed $offset
808
     * @param mixed $value
809
     */
810
    public function offsetSet($offset, $value)
811
    {
812
        if ($offset == null) {
813
            $this->items[] = $value;
814
        } else {
815
            $this->items[$offset] = $value;
816
        }
817
    }
818
819
    /**
820
     * Unset an item with the key in $key
821
     *
822
     * @param mixed $offset
823
     */
824
    public function offsetUnset($offset)
825
    {
826
        unset($this->items[$offset]);
827
    }
828
829
    /**
830
     * Extracts a value from an item in the list, where the item is either an
831
     * object or array.
832
     *
833
     * @param array|object $item
834
     * @param string $key
835
     * @return mixed
836
     */
837
    protected function extractValue($item, $key)
838
    {
839
        if (is_object($item)) {
840
            if (method_exists($item, 'hasMethod') && $item->hasMethod($key)) {
841
                return $item->{$key}();
842
            }
843
            return $item->{$key};
844
        }
845
846
        if (array_key_exists($key, $item)) {
847
            return $item[$key];
848
        }
849
850
        return null;
851
    }
852
}
853