Passed
Push — fix-9162 ( d08835 )
by Sam
07:26
created

ArrayList::getIterator()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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

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