Passed
Push — consistent-limit ( 4911ac...2d3f1d )
by Sam
04:45
created

ArrayList::count()   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
        foreach ($this->items as $i => $item) {
89
            if (is_array($item)) {
90
                $this->items[$i] = new ArrayData($item);
91
            }
92
        }
93
        return new ArrayIterator($this->items);
94
    }
95
96
    /**
97
     * Return an array of the actual items that this ArrayList contains.
98
     *
99
     * @return array
100
     */
101
    public function toArray()
102
    {
103
        return $this->items;
104
    }
105
106
    /**
107
     * Walks the list using the specified callback
108
     *
109
     * @param callable $callback
110
     * @return $this
111
     */
112
    public function each($callback)
113
    {
114
        foreach ($this as $item) {
115
            $callback($item);
116
        }
117
        return $this;
118
    }
119
120
    public function debug()
121
    {
122
        $val = "<h2>" . static::class . "</h2><ul>";
123
        foreach ($this->toNestedArray() as $item) {
124
            $val .= "<li style=\"list-style-type: disc; margin-left: 20px\">" . Debug::text($item) . "</li>";
125
        }
126
        $val .= "</ul>";
127
        return $val;
128
    }
129
130
    /**
131
     * Return this list as an array and every object it as an sub array as well
132
     *
133
     * @return array
134
     */
135
    public function toNestedArray()
136
    {
137
        $result = [];
138
139
        foreach ($this->items as $item) {
140
            if (is_object($item)) {
141
                if (method_exists($item, 'toMap')) {
142
                    $result[] = $item->toMap();
143
                } else {
144
                    $result[] = (array) $item;
145
                }
146
            } else {
147
                $result[] = $item;
148
            }
149
        }
150
151
        return $result;
152
    }
153
154
    /**
155
     * Get a sub-range of this dataobjectset as an array
156
     *
157
     * @param int $length
158
     * @param int $offset
159
     * @return static
160
     */
161
    public function limit($length, $offset = 0)
162
    {
163
        // Type checking: designed for consistency with DataList::limit()
164
        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...
165
            Deprecation::notice(
166
                '4.3',
167
                'Arguments to ArrayList::limit() should be numeric'
168
            );
169
        }
170
171
        if ($length < 0 || $offset < 0) {
172
            Deprecation::notice(
173
                '4.3',
174
                'Arguments to ArrayList::limit() should be positive'
175
            );
176
        }
177
178
        if (!$length) {
179
            if ($limit === 0) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $limit seems to be never defined.
Loading history...
180
                Deprecation::notice(
181
                    '4.3',
182
                    "limit(0) is deprecated in SS4. In SS5 a limit of 0 will instead return no records."
183
                );
184
            }
185
186
            $length = count($this->items);
187
        }
188
189
        $list = clone $this;
190
        $list->items = array_slice($this->items, $offset, $length);
191
192
        return $list;
193
    }
194
195
    /**
196
     * Add this $item into this list
197
     *
198
     * @param mixed $item
199
     */
200
    public function add($item)
201
    {
202
        $this->push($item);
203
    }
204
205
    /**
206
     * Remove this item from this list
207
     *
208
     * @param mixed $item
209
     */
210
    public function remove($item)
211
    {
212
        $renumberKeys = false;
213
        foreach ($this->items as $key => $value) {
214
            if ($item === $value) {
215
                $renumberKeys = true;
216
                unset($this->items[$key]);
217
            }
218
        }
219
        if ($renumberKeys) {
220
            $this->items = array_values($this->items);
221
        }
222
    }
223
224
    /**
225
     * Replaces an item in this list with another item.
226
     *
227
     * @param array|object $item
228
     * @param array|object $with
229
     * @return void;
230
     */
231
    public function replace($item, $with)
232
    {
233
        foreach ($this->items as $key => $candidate) {
234
            if ($candidate === $item) {
235
                $this->items[$key] = $with;
236
                return;
237
            }
238
        }
239
    }
240
241
    /**
242
     * Merges with another array or list by pushing all the items in it onto the
243
     * end of this list.
244
     *
245
     * @param array|object $with
246
     */
247
    public function merge($with)
248
    {
249
        foreach ($with as $item) {
250
            $this->push($item);
251
        }
252
    }
253
254
    /**
255
     * Removes items from this list which have a duplicate value for a certain
256
     * field. This is especially useful when combining lists.
257
     *
258
     * @param string $field
259
     */
260
    public function removeDuplicates($field = 'ID')
261
    {
262
        $seen = [];
263
        $renumberKeys = false;
264
265
        foreach ($this->items as $key => $item) {
266
            $value = $this->extractValue($item, $field);
267
268
            if (array_key_exists($value, $seen)) {
269
                $renumberKeys = true;
270
                unset($this->items[$key]);
271
            }
272
273
            $seen[$value] = true;
274
        }
275
276
        if ($renumberKeys) {
277
            $this->items = array_values($this->items);
278
        }
279
280
        return $this;
281
    }
282
283
    /**
284
     * Pushes an item onto the end of this list.
285
     *
286
     * @param array|object $item
287
     */
288
    public function push($item)
289
    {
290
        $this->items[] = $item;
291
    }
292
293
    /**
294
     * Pops the last element off the end of the list and returns it.
295
     *
296
     * @return array|object
297
     */
298
    public function pop()
299
    {
300
        return array_pop($this->items);
301
    }
302
303
    /**
304
     * Add an item onto the beginning of the list.
305
     *
306
     * @param array|object $item
307
     */
308
    public function unshift($item)
309
    {
310
        array_unshift($this->items, $item);
311
    }
312
313
    /**
314
     * Shifts the item off the beginning of the list and returns it.
315
     *
316
     * @return array|object
317
     */
318
    public function shift()
319
    {
320
        return array_shift($this->items);
321
    }
322
323
    /**
324
     * Returns the first item in the list
325
     *
326
     * @return mixed
327
     */
328
    public function first()
329
    {
330
        if (empty($this->items)) {
331
            return null;
332
        }
333
334
        return reset($this->items);
335
    }
336
337
    /**
338
     * Returns the last item in the list
339
     *
340
     * @return mixed
341
     */
342
    public function last()
343
    {
344
        if (empty($this->items)) {
345
            return null;
346
        }
347
348
        return end($this->items);
349
    }
350
351
    /**
352
     * Returns a map of this list
353
     *
354
     * @param string $keyfield The 'key' field of the result array
355
     * @param string $titlefield The value field of the result array
356
     * @return Map
357
     */
358
    public function map($keyfield = 'ID', $titlefield = 'Title')
359
    {
360
        $list = clone $this;
361
        return new Map($list, $keyfield, $titlefield);
362
    }
363
364
    /**
365
     * Find the first item of this list where the given key = value
366
     *
367
     * @param string $key
368
     * @param string $value
369
     * @return mixed
370
     */
371
    public function find($key, $value)
372
    {
373
        foreach ($this->items as $item) {
374
            if ($this->extractValue($item, $key) == $value) {
375
                return $item;
376
            }
377
        }
378
        return null;
379
    }
380
381
    /**
382
     * Returns an array of a single field value for all items in the list.
383
     *
384
     * @param string $colName
385
     * @return array
386
     */
387
    public function column($colName = 'ID')
388
    {
389
        $result = [];
390
391
        foreach ($this->items as $item) {
392
            $result[] = $this->extractValue($item, $colName);
393
        }
394
395
        return $result;
396
    }
397
398
    /**
399
     * Returns a unique array of a single field value for all the items in the list
400
     *
401
     * @param string $colName
402
     * @return array
403
     */
404
    public function columnUnique($colName = 'ID')
405
    {
406
        return array_unique($this->column($colName));
407
    }
408
409
    /**
410
     * You can always sort a ArrayList
411
     *
412
     * @param string $by
413
     * @return bool
414
     */
415
    public function canSortBy($by)
416
    {
417
        return true;
418
    }
419
420
    /**
421
     * Reverses an {@link ArrayList}
422
     *
423
     * @return ArrayList
424
     */
425
    public function reverse()
426
    {
427
        $list = clone $this;
428
        $list->items = array_reverse($this->items);
429
430
        return $list;
431
    }
432
433
    /**
434
     * Parses a specified column into a sort field and direction
435
     *
436
     * @param string $column String to parse containing the column name
437
     * @param mixed $direction Optional Additional argument which may contain the direction
438
     * @return array Sort specification in the form array("Column", SORT_ASC).
439
     */
440
    protected function parseSortColumn($column, $direction = null)
441
    {
442
        // Substitute the direction for the column if column is a numeric index
443
        if ($direction && (empty($column) || is_numeric($column))) {
444
            $column = $direction;
445
            $direction = null;
446
        }
447
448
        // Parse column specification, considering possible ansi sql quoting
449
        // Note that table prefix is allowed, but discarded
450
        if (preg_match('/^("?(?<table>[^"\s]+)"?\\.)?"?(?<column>[^"\s]+)"?(\s+(?<direction>((asc)|(desc))(ending)?))?$/i', $column, $match)) {
451
            $column = $match['column'];
452
            if (empty($direction) && !empty($match['direction'])) {
453
                $direction = $match['direction'];
454
            }
455
        } else {
456
            throw new InvalidArgumentException("Invalid sort() column");
457
        }
458
459
        // Parse sort direction specification
460
        if (empty($direction) || preg_match('/^asc(ending)?$/i', $direction)) {
461
            $direction = SORT_ASC;
462
        } elseif (preg_match('/^desc(ending)?$/i', $direction)) {
463
            $direction = SORT_DESC;
464
        } else {
465
            throw new InvalidArgumentException("Invalid sort() direction");
466
        }
467
468
        return array($column, $direction);
469
    }
470
471
    /**
472
     * Sorts this list by one or more fields. You can either pass in a single
473
     * field name and direction, or a map of field names to sort directions.
474
     *
475
     * Note that columns may be double quoted as per ANSI sql standard
476
     *
477
     * @return static
478
     * @see SS_List::sort()
479
     * @example $list->sort('Name'); // default ASC sorting
480
     * @example $list->sort('Name DESC'); // DESC sorting
481
     * @example $list->sort('Name', 'ASC');
482
     * @example $list->sort(array('Name'=>'ASC,'Age'=>'DESC'));
483
     */
484
    public function sort()
485
    {
486
        $args = func_get_args();
487
488
        if (count($args)==0) {
489
            return $this;
490
        }
491
        if (count($args)>2) {
492
            throw new InvalidArgumentException('This method takes zero, one or two arguments');
493
        }
494
        $columnsToSort = [];
495
496
        // One argument and it's a string
497
        if (count($args)==1 && is_string($args[0])) {
498
            list($column, $direction) = $this->parseSortColumn($args[0]);
499
            $columnsToSort[$column] = $direction;
500
        } elseif (count($args)==2) {
501
            list($column, $direction) = $this->parseSortColumn($args[0], $args[1]);
502
            $columnsToSort[$column] = $direction;
503
        } elseif (is_array($args[0])) {
504
            foreach ($args[0] as $key => $value) {
505
                list($column, $direction) = $this->parseSortColumn($key, $value);
506
                $columnsToSort[$column] = $direction;
507
            }
508
        } else {
509
            throw new InvalidArgumentException("Bad arguments passed to sort()");
510
        }
511
512
        // Store the original keys of the items as a sort fallback, so we can preserve the original order in the event
513
        // that array_multisort is unable to work out a sort order for them. This also prevents array_multisort trying
514
        // to inspect object properties which can result in errors with circular dependencies
515
        $originalKeys = array_keys($this->items);
516
517
        // This the main sorting algorithm that supports infinite sorting params
518
        $multisortArgs = [];
519
        $values = [];
520
        $firstRun = true;
521
        foreach ($columnsToSort as $column => $direction) {
522
            // The reason these are added to columns is of the references, otherwise when the foreach
523
            // is done, all $values and $direction look the same
524
            $values[$column] = [];
525
            $sortDirection[$column] = $direction;
526
            // We need to subtract every value into a temporary array for sorting
527
            foreach ($this->items as $index => $item) {
528
                $values[$column][] = strtolower($this->extractValue($item, $column));
529
            }
530
            // PHP 5.3 requires below arguments to be reference when using array_multisort together
531
            // with call_user_func_array
532
            // First argument is the 'value' array to be sorted
533
            $multisortArgs[] = &$values[$column];
534
            // First argument is the direction to be sorted,
535
            $multisortArgs[] = &$sortDirection[$column];
536
            if ($firstRun) {
537
                $multisortArgs[] = SORT_REGULAR;
538
            }
539
            $firstRun = false;
540
        }
541
542
        $multisortArgs[] = &$originalKeys;
543
544
        $list = clone $this;
545
        // As the last argument we pass in a reference to the items that all the sorting will be applied upon
546
        $multisortArgs[] = &$list->items;
547
        call_user_func_array('array_multisort', $multisortArgs);
548
        return $list;
549
    }
550
551
    /**
552
     * Returns true if the given column can be used to filter the records.
553
     *
554
     * It works by checking the fields available in the first record of the list.
555
     *
556
     * @param string $by
557
     * @return bool
558
     */
559
    public function canFilterBy($by)
560
    {
561
        if (empty($this->items)) {
562
            return false;
563
        }
564
565
        $firstRecord = $this->first();
566
567
        return array_key_exists($by, $firstRecord);
568
    }
569
570
    /**
571
     * Filter the list to include items with these charactaristics
572
     *
573
     * @return ArrayList
574
     * @see SS_List::filter()
575
     * @example $list->filter('Name', 'bob'); // only bob in the list
576
     * @example $list->filter('Name', array('aziz', 'bob'); // aziz and bob in list
577
     * @example $list->filter(array('Name'=>'bob, 'Age'=>21)); // bob with the Age 21 in list
578
     * @example $list->filter(array('Name'=>'bob, 'Age'=>array(21, 43))); // bob with the Age 21 or 43
579
     * @example $list->filter(array('Name'=>array('aziz','bob'), 'Age'=>array(21, 43)));
580
     *          // aziz with the age 21 or 43 and bob with the Age 21 or 43
581
     */
582
    public function filter()
583
    {
584
585
        $keepUs = call_user_func_array([$this, 'normaliseFilterArgs'], func_get_args());
586
587
        $itemsToKeep = [];
588
        foreach ($this->items as $item) {
589
            $keepItem = true;
590
            foreach ($keepUs as $column => $value) {
591
                if ((is_array($value) && !in_array($this->extractValue($item, $column), $value))
592
                    || (!is_array($value) && $this->extractValue($item, $column) != $value)
593
                ) {
594
                    $keepItem = false;
595
                    break;
596
                }
597
            }
598
            if ($keepItem) {
599
                $itemsToKeep[] = $item;
600
            }
601
        }
602
603
        $list = clone $this;
604
        $list->items = $itemsToKeep;
605
        return $list;
606
    }
607
608
    /**
609
     * Return a copy of this list which contains items matching any of these charactaristics.
610
     *
611
     * @example // only bob in the list
612
     *          $list = $list->filterAny('Name', 'bob');
613
     * @example // azis or bob in the list
614
     *          $list = $list->filterAny('Name', array('aziz', 'bob');
615
     * @example // bob or anyone aged 21 in the list
616
     *          $list = $list->filterAny(array('Name'=>'bob, 'Age'=>21));
617
     * @example // bob or anyone aged 21 or 43 in the list
618
     *          $list = $list->filterAny(array('Name'=>'bob, 'Age'=>array(21, 43)));
619
     * @example // all bobs, phils or anyone aged 21 or 43 in the list
620
     *          $list = $list->filterAny(array('Name'=>array('bob','phil'), 'Age'=>array(21, 43)));
621
     *
622
     * @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...
623
     * @return static
624
     */
625
    public function filterAny()
626
    {
627
        $keepUs = $this->normaliseFilterArgs(...func_get_args());
628
629
        $itemsToKeep = [];
630
631
        foreach ($this->items as $item) {
632
            foreach ($keepUs as $column => $value) {
633
                $extractedValue = $this->extractValue($item, $column);
634
                $matches = is_array($value) ? in_array($extractedValue, $value) : $extractedValue == $value;
635
                if ($matches) {
636
                    $itemsToKeep[] = $item;
637
                    break;
638
                }
639
            }
640
        }
641
642
        $list = clone $this;
643
        $list->items = array_unique($itemsToKeep, SORT_REGULAR);
644
        return $list;
645
    }
646
647
    /**
648
     * Take the "standard" arguments that the filter/exclude functions take and return a single array with
649
     * 'colum' => 'value'
650
     *
651
     * @param $column array|string The column name to filter OR an assosicative array of column => value
652
     * @param $value array|string|null The values to filter the $column against
653
     *
654
     * @return array The normalised keyed array
655
     */
656
    protected function normaliseFilterArgs($column, $value = null)
657
    {
658
        $args = func_get_args();
659
        if (count($args) > 2) {
660
            throw new InvalidArgumentException('filter takes one array or two arguments');
661
        }
662
663
        if (count($args) === 1 && !is_array($args[0])) {
664
            throw new InvalidArgumentException('filter takes one array or two arguments');
665
        }
666
667
        $keepUs = [];
668
        if (count($args) === 2) {
669
            $keepUs[$args[0]] = $args[1];
670
        }
671
672
        if (count($args) === 1 && is_array($args[0])) {
673
            foreach ($args[0] as $key => $val) {
674
                $keepUs[$key] = $val;
675
            }
676
        }
677
678
        return $keepUs;
679
    }
680
681
    /**
682
     * Filter this list to only contain the given Primary IDs
683
     *
684
     * @param array $ids Array of integers, will be automatically cast/escaped.
685
     * @return ArrayList
686
     */
687
    public function byIDs($ids)
688
    {
689
        $ids = array_map('intval', $ids); // sanitize
690
        return $this->filter('ID', $ids);
691
    }
692
693
    public function byID($id)
694
    {
695
        $firstElement = $this->filter("ID", $id)->first();
696
697
        if ($firstElement === false) {
698
            return null;
699
        }
700
701
        return $firstElement;
702
    }
703
704
    /**
705
     * @see Filterable::filterByCallback()
706
     *
707
     * @example $list = $list->filterByCallback(function($item, $list) { return $item->Age == 9; })
708
     * @param callable $callback
709
     * @return ArrayList
710
     */
711
    public function filterByCallback($callback)
712
    {
713
        if (!is_callable($callback)) {
714
            throw new LogicException(sprintf(
715
                "SS_Filterable::filterByCallback() passed callback must be callable, '%s' given",
716
                gettype($callback)
717
            ));
718
        }
719
720
        $output = static::create();
721
722
        foreach ($this as $item) {
723
            if (call_user_func($callback, $item, $this)) {
724
                $output->push($item);
725
            }
726
        }
727
728
        return $output;
729
    }
730
731
    /**
732
     * Exclude the list to not contain items with these charactaristics
733
     *
734
     * @return ArrayList
735
     * @see SS_List::exclude()
736
     * @example $list->exclude('Name', 'bob'); // exclude bob from list
737
     * @example $list->exclude('Name', array('aziz', 'bob'); // exclude aziz and bob from list
738
     * @example $list->exclude(array('Name'=>'bob, 'Age'=>21)); // exclude bob that has Age 21
739
     * @example $list->exclude(array('Name'=>'bob, 'Age'=>array(21, 43))); // exclude bob with Age 21 or 43
740
     * @example $list->exclude(array('Name'=>array('bob','phil'), 'Age'=>array(21, 43)));
741
     *          // bob age 21 or 43, phil age 21 or 43 would be excluded
742
     */
743
    public function exclude()
744
    {
745
        $removeUs = $this->normaliseFilterArgs(...func_get_args());
746
747
        $hitsRequiredToRemove = count($removeUs);
748
        $matches = [];
749
        foreach ($removeUs as $column => $excludeValue) {
750
            foreach ($this->items as $key => $item) {
751
                if (!is_array($excludeValue) && $this->extractValue($item, $column) == $excludeValue) {
752
                    $matches[$key] = isset($matches[$key]) ? $matches[$key] + 1 : 1;
753
                } elseif (is_array($excludeValue) && in_array($this->extractValue($item, $column), $excludeValue)) {
754
                    $matches[$key] = isset($matches[$key]) ? $matches[$key] + 1 : 1;
755
                }
756
            }
757
        }
758
759
        $keysToRemove = array_keys($matches, $hitsRequiredToRemove);
760
761
        $itemsToKeep = [];
762
        foreach ($this->items as $key => $value) {
763
            if (!in_array($key, $keysToRemove)) {
764
                $itemsToKeep[] = $value;
765
            }
766
        }
767
768
        $list = clone $this;
769
        $list->items = $itemsToKeep;
770
        return $list;
771
    }
772
773
    protected function shouldExclude($item, $args)
774
    {
775
    }
776
777
778
    /**
779
     * Returns whether an item with $key exists
780
     *
781
     * @param mixed $offset
782
     * @return bool
783
     */
784
    public function offsetExists($offset)
785
    {
786
        return array_key_exists($offset, $this->items);
787
    }
788
789
    /**
790
     * Returns item stored in list with index $key
791
     *
792
     * @param mixed $offset
793
     * @return DataObject
794
     */
795
    public function offsetGet($offset)
796
    {
797
        if ($this->offsetExists($offset)) {
798
            return $this->items[$offset];
799
        }
800
        return null;
801
    }
802
803
    /**
804
     * Set an item with the key in $key
805
     *
806
     * @param mixed $offset
807
     * @param mixed $value
808
     */
809
    public function offsetSet($offset, $value)
810
    {
811
        if ($offset == null) {
812
            $this->items[] = $value;
813
        } else {
814
            $this->items[$offset] = $value;
815
        }
816
    }
817
818
    /**
819
     * Unset an item with the key in $key
820
     *
821
     * @param mixed $offset
822
     */
823
    public function offsetUnset($offset)
824
    {
825
        unset($this->items[$offset]);
826
    }
827
828
    /**
829
     * Extracts a value from an item in the list, where the item is either an
830
     * object or array.
831
     *
832
     * @param array|object $item
833
     * @param string $key
834
     * @return mixed
835
     */
836
    protected function extractValue($item, $key)
837
    {
838
        if (is_object($item)) {
839
            if (method_exists($item, 'hasMethod') && $item->hasMethod($key)) {
840
                return $item->{$key}();
841
            }
842
            return $item->{$key};
843
        }
844
845
        if (array_key_exists($key, $item)) {
846
            return $item[$key];
847
        }
848
849
        return null;
850
    }
851
}
852