Passed
Push — 4 ( ac5c34...1a634f )
by Daniel
09:26
created

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