ArrayList   F
last analyzed

Complexity

Total Complexity 133

Size/Duplication

Total Lines 860
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 251
c 0
b 0
f 0
dl 0
loc 860
rs 2
wmc 133

45 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A unshift() 0 3 1
A column() 0 9 2
A canSortBy() 0 3 1
A push() 0 3 1
A find() 0 8 3
A shift() 0 3 1
A first() 0 7 2
A exists() 0 3 1
A dataClass() 0 9 3
B limit() 0 32 7
A reverse() 0 6 1
A map() 0 4 1
A remove() 0 11 4
A add() 0 3 1
A last() 0 7 2
A setDataClass() 0 4 1
A each() 0 6 2
A columnUnique() 0 3 1
A count() 0 3 1
A toNestedArray() 0 17 4
A pop() 0 3 1
A merge() 0 4 2
A removeDuplicates() 0 21 4
A debug() 0 8 2
A toArray() 0 3 1
A replace() 0 6 3
A getIterator() 0 7 3
B sort() 0 65 11
B parseSortColumn() 0 30 10
B normaliseFilterArgs() 0 23 8
A filterByCallback() 0 18 4
A offsetExists() 0 3 1
A offsetUnset() 0 3 1
A byIDs() 0 4 1
A extractValue() 0 14 5
A offsetSet() 0 6 2
A shuffle() 0 5 1
A filterAny() 0 20 5
A shouldExclude() 0 2 1
A offsetGet() 0 6 2
B exclude() 0 28 11
A byID() 0 9 2
B filter() 0 24 8
A canFilterBy() 0 9 3

How to fix   Complexity   

Complex Class

Complex classes like ArrayList often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ArrayList, and based on these observations, apply Extract Interface, too.

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
        foreach ($this->items as $i => $item) {
111
            if (is_array($item)) {
112
                yield new ArrayData($item);
113
            } else {
114
                yield $item;
115
            }
116
        }
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
        $pattern = '/^("?(?<table>[^"\s]+)"?\\.)?"?(?<column>[^"\s]+)"?(\s+(?<direction>((asc)|(desc))(ending)?))?$/i';
475
        if (preg_match($pattern, $column, $match)) {
476
            $column = $match['column'];
477
            if (empty($direction) && !empty($match['direction'])) {
478
                $direction = $match['direction'];
479
            }
480
        } else {
481
            throw new InvalidArgumentException("Invalid sort() column");
482
        }
483
484
        // Parse sort direction specification
485
        if (empty($direction) || preg_match('/^asc(ending)?$/i', $direction)) {
486
            $direction = SORT_ASC;
487
        } elseif (preg_match('/^desc(ending)?$/i', $direction)) {
488
            $direction = SORT_DESC;
489
        } else {
490
            throw new InvalidArgumentException("Invalid sort() direction");
491
        }
492
493
        return array($column, $direction);
494
    }
495
496
    /**
497
     * Sorts this list by one or more fields. You can either pass in a single
498
     * field name and direction, or a map of field names to sort directions.
499
     *
500
     * Note that columns may be double quoted as per ANSI sql standard
501
     *
502
     * @return static
503
     * @see SS_List::sort()
504
     * @example $list->sort('Name'); // default ASC sorting
505
     * @example $list->sort('Name DESC'); // DESC sorting
506
     * @example $list->sort('Name', 'ASC');
507
     * @example $list->sort(array('Name'=>'ASC,'Age'=>'DESC'));
508
     */
509
    public function sort()
510
    {
511
        $args = func_get_args();
512
513
        if (count($args)==0) {
514
            return $this;
515
        }
516
        if (count($args)>2) {
517
            throw new InvalidArgumentException('This method takes zero, one or two arguments');
518
        }
519
        $columnsToSort = [];
520
521
        // One argument and it's a string
522
        if (count($args)==1 && is_string($args[0])) {
523
            list($column, $direction) = $this->parseSortColumn($args[0]);
524
            $columnsToSort[$column] = $direction;
525
        } elseif (count($args)==2) {
526
            list($column, $direction) = $this->parseSortColumn($args[0], $args[1]);
527
            $columnsToSort[$column] = $direction;
528
        } elseif (is_array($args[0])) {
529
            foreach ($args[0] as $key => $value) {
530
                list($column, $direction) = $this->parseSortColumn($key, $value);
531
                $columnsToSort[$column] = $direction;
532
            }
533
        } else {
534
            throw new InvalidArgumentException("Bad arguments passed to sort()");
535
        }
536
537
        // Store the original keys of the items as a sort fallback, so we can preserve the original order in the event
538
        // that array_multisort is unable to work out a sort order for them. This also prevents array_multisort trying
539
        // to inspect object properties which can result in errors with circular dependencies
540
        $originalKeys = array_keys($this->items);
541
542
        // This the main sorting algorithm that supports infinite sorting params
543
        $multisortArgs = [];
544
        $values = [];
545
        $firstRun = true;
546
        foreach ($columnsToSort as $column => $direction) {
547
            // The reason these are added to columns is of the references, otherwise when the foreach
548
            // is done, all $values and $direction look the same
549
            $values[$column] = [];
550
            $sortDirection[$column] = $direction;
551
            // We need to subtract every value into a temporary array for sorting
552
            foreach ($this->items as $index => $item) {
553
                $values[$column][] = strtolower($this->extractValue($item, $column));
554
            }
555
            // PHP 5.3 requires below arguments to be reference when using array_multisort together
556
            // with call_user_func_array
557
            // First argument is the 'value' array to be sorted
558
            $multisortArgs[] = &$values[$column];
559
            // First argument is the direction to be sorted,
560
            $multisortArgs[] = &$sortDirection[$column];
561
            if ($firstRun) {
562
                $multisortArgs[] = SORT_REGULAR;
563
            }
564
            $firstRun = false;
565
        }
566
567
        $multisortArgs[] = &$originalKeys;
568
569
        $list = clone $this;
570
        // As the last argument we pass in a reference to the items that all the sorting will be applied upon
571
        $multisortArgs[] = &$list->items;
572
        call_user_func_array('array_multisort', $multisortArgs);
573
        return $list;
574
    }
575
576
    /**
577
     * Shuffle the items in this array list
578
     *
579
     * @return $this
580
     */
581
    public function shuffle()
582
    {
583
        shuffle($this->items);
584
585
        return $this;
586
    }
587
588
    /**
589
     * Returns true if the given column can be used to filter the records.
590
     *
591
     * It works by checking the fields available in the first record of the list.
592
     *
593
     * @param string $by
594
     * @return bool
595
     */
596
    public function canFilterBy($by)
597
    {
598
        if (empty($this->items)) {
599
            return false;
600
        }
601
602
        $firstRecord = $this->first();
603
604
        return is_array($firstRecord) ? array_key_exists($by, $firstRecord) : property_exists($by, $firstRecord);
605
    }
606
607
    /**
608
     * Filter the list to include items with these charactaristics
609
     *
610
     * @return ArrayList
611
     * @see SS_List::filter()
612
     * @example $list->filter('Name', 'bob'); // only bob in the list
613
     * @example $list->filter('Name', array('aziz', 'bob'); // aziz and bob in list
614
     * @example $list->filter(array('Name'=>'bob, 'Age'=>21)); // bob with the Age 21 in list
615
     * @example $list->filter(array('Name'=>'bob, 'Age'=>array(21, 43))); // bob with the Age 21 or 43
616
     * @example $list->filter(array('Name'=>array('aziz','bob'), 'Age'=>array(21, 43)));
617
     *          // aziz with the age 21 or 43 and bob with the Age 21 or 43
618
     */
619
    public function filter()
620
    {
621
622
        $keepUs = call_user_func_array([$this, 'normaliseFilterArgs'], func_get_args());
623
624
        $itemsToKeep = [];
625
        foreach ($this->items as $item) {
626
            $keepItem = true;
627
            foreach ($keepUs as $column => $value) {
628
                if ((is_array($value) && !in_array($this->extractValue($item, $column), $value))
629
                    || (!is_array($value) && $this->extractValue($item, $column) != $value)
630
                ) {
631
                    $keepItem = false;
632
                    break;
633
                }
634
            }
635
            if ($keepItem) {
636
                $itemsToKeep[] = $item;
637
            }
638
        }
639
640
        $list = clone $this;
641
        $list->items = $itemsToKeep;
642
        return $list;
643
    }
644
645
    /**
646
     * Return a copy of this list which contains items matching any of these charactaristics.
647
     *
648
     * @example // only bob in the list
649
     *          $list = $list->filterAny('Name', 'bob');
650
     * @example // azis or bob in the list
651
     *          $list = $list->filterAny('Name', array('aziz', 'bob');
652
     * @example // bob or anyone aged 21 in the list
653
     *          $list = $list->filterAny(array('Name'=>'bob, 'Age'=>21));
654
     * @example // bob or anyone aged 21 or 43 in the list
655
     *          $list = $list->filterAny(array('Name'=>'bob, 'Age'=>array(21, 43)));
656
     * @example // all bobs, phils or anyone aged 21 or 43 in the list
657
     *          $list = $list->filterAny(array('Name'=>array('bob','phil'), 'Age'=>array(21, 43)));
658
     *
659
     * @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...
660
     * @return static
661
     */
662
    public function filterAny()
663
    {
664
        $keepUs = $this->normaliseFilterArgs(...func_get_args());
665
666
        $itemsToKeep = [];
667
668
        foreach ($this->items as $item) {
669
            foreach ($keepUs as $column => $value) {
670
                $extractedValue = $this->extractValue($item, $column);
671
                $matches = is_array($value) ? in_array($extractedValue, $value) : $extractedValue == $value;
672
                if ($matches) {
673
                    $itemsToKeep[] = $item;
674
                    break;
675
                }
676
            }
677
        }
678
679
        $list = clone $this;
680
        $list->items = array_unique($itemsToKeep, SORT_REGULAR);
681
        return $list;
682
    }
683
684
    /**
685
     * Take the "standard" arguments that the filter/exclude functions take and return a single array with
686
     * 'colum' => 'value'
687
     *
688
     * @param $column array|string The column name to filter OR an assosicative array of column => value
689
     * @param $value array|string|null The values to filter the $column against
690
     *
691
     * @return array The normalised keyed array
692
     */
693
    protected function normaliseFilterArgs($column, $value = null)
694
    {
695
        $args = func_get_args();
696
        if (count($args) > 2) {
697
            throw new InvalidArgumentException('filter takes one array or two arguments');
698
        }
699
700
        if (count($args) === 1 && !is_array($args[0])) {
701
            throw new InvalidArgumentException('filter takes one array or two arguments');
702
        }
703
704
        $keepUs = [];
705
        if (count($args) === 2) {
706
            $keepUs[$args[0]] = $args[1];
707
        }
708
709
        if (count($args) === 1 && is_array($args[0])) {
710
            foreach ($args[0] as $key => $val) {
711
                $keepUs[$key] = $val;
712
            }
713
        }
714
715
        return $keepUs;
716
    }
717
718
    /**
719
     * Filter this list to only contain the given Primary IDs
720
     *
721
     * @param array $ids Array of integers, will be automatically cast/escaped.
722
     * @return ArrayList
723
     */
724
    public function byIDs($ids)
725
    {
726
        $ids = array_map('intval', $ids); // sanitize
727
        return $this->filter('ID', $ids);
728
    }
729
730
    public function byID($id)
731
    {
732
        $firstElement = $this->filter("ID", $id)->first();
733
734
        if ($firstElement === false) {
735
            return null;
736
        }
737
738
        return $firstElement;
739
    }
740
741
    /**
742
     * @see Filterable::filterByCallback()
743
     *
744
     * @example $list = $list->filterByCallback(function($item, $list) { return $item->Age == 9; })
745
     * @param callable $callback
746
     * @return ArrayList
747
     */
748
    public function filterByCallback($callback)
749
    {
750
        if (!is_callable($callback)) {
751
            throw new LogicException(sprintf(
752
                "SS_Filterable::filterByCallback() passed callback must be callable, '%s' given",
753
                gettype($callback)
754
            ));
755
        }
756
757
        $output = static::create();
758
759
        foreach ($this as $item) {
760
            if (call_user_func($callback, $item, $this)) {
761
                $output->push($item);
762
            }
763
        }
764
765
        return $output;
766
    }
767
768
    /**
769
     * Exclude the list to not contain items with these charactaristics
770
     *
771
     * @return ArrayList
772
     * @see SS_List::exclude()
773
     * @example $list->exclude('Name', 'bob'); // exclude bob from list
774
     * @example $list->exclude('Name', array('aziz', 'bob'); // exclude aziz and bob from list
775
     * @example $list->exclude(array('Name'=>'bob, 'Age'=>21)); // exclude bob that has Age 21
776
     * @example $list->exclude(array('Name'=>'bob, 'Age'=>array(21, 43))); // exclude bob with Age 21 or 43
777
     * @example $list->exclude(array('Name'=>array('bob','phil'), 'Age'=>array(21, 43)));
778
     *          // bob age 21 or 43, phil age 21 or 43 would be excluded
779
     */
780
    public function exclude()
781
    {
782
        $removeUs = $this->normaliseFilterArgs(...func_get_args());
783
784
        $hitsRequiredToRemove = count($removeUs);
785
        $matches = [];
786
        foreach ($removeUs as $column => $excludeValue) {
787
            foreach ($this->items as $key => $item) {
788
                if (!is_array($excludeValue) && $this->extractValue($item, $column) == $excludeValue) {
789
                    $matches[$key] = isset($matches[$key]) ? $matches[$key] + 1 : 1;
790
                } elseif (is_array($excludeValue) && in_array($this->extractValue($item, $column), $excludeValue)) {
791
                    $matches[$key] = isset($matches[$key]) ? $matches[$key] + 1 : 1;
792
                }
793
            }
794
        }
795
796
        $keysToRemove = array_keys($matches, $hitsRequiredToRemove);
797
798
        $itemsToKeep = [];
799
        foreach ($this->items as $key => $value) {
800
            if (!in_array($key, $keysToRemove)) {
801
                $itemsToKeep[] = $value;
802
            }
803
        }
804
805
        $list = clone $this;
806
        $list->items = $itemsToKeep;
807
        return $list;
808
    }
809
810
    protected function shouldExclude($item, $args)
811
    {
812
    }
813
814
815
    /**
816
     * Returns whether an item with $key exists
817
     *
818
     * @param mixed $offset
819
     * @return bool
820
     */
821
    public function offsetExists($offset)
822
    {
823
        return array_key_exists($offset, $this->items);
824
    }
825
826
    /**
827
     * Returns item stored in list with index $key
828
     *
829
     * @param mixed $offset
830
     * @return DataObject
831
     */
832
    public function offsetGet($offset)
833
    {
834
        if ($this->offsetExists($offset)) {
835
            return $this->items[$offset];
836
        }
837
        return null;
838
    }
839
840
    /**
841
     * Set an item with the key in $key
842
     *
843
     * @param mixed $offset
844
     * @param mixed $value
845
     */
846
    public function offsetSet($offset, $value)
847
    {
848
        if ($offset == null) {
849
            $this->items[] = $value;
850
        } else {
851
            $this->items[$offset] = $value;
852
        }
853
    }
854
855
    /**
856
     * Unset an item with the key in $key
857
     *
858
     * @param mixed $offset
859
     */
860
    public function offsetUnset($offset)
861
    {
862
        unset($this->items[$offset]);
863
    }
864
865
    /**
866
     * Extracts a value from an item in the list, where the item is either an
867
     * object or array.
868
     *
869
     * @param array|object $item
870
     * @param string $key
871
     * @return mixed
872
     */
873
    protected function extractValue($item, $key)
874
    {
875
        if (is_object($item)) {
876
            if (method_exists($item, 'hasMethod') && $item->hasMethod($key)) {
877
                return $item->{$key}();
878
            }
879
            return $item->{$key};
880
        }
881
882
        if (array_key_exists($key, $item)) {
883
            return $item[$key];
884
        }
885
886
        return null;
887
    }
888
}
889