Passed
Push — pulls/manymanylist-add-callbac... ( 77b6c5...7684da )
by Ingo
08:25
created

DataList   F

Complexity

Total Complexity 140

Size/Duplication

Total Lines 1373
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 297
dl 0
loc 1373
rs 2
c 1
b 0
f 0
wmc 140

72 Methods

Rating   Name   Duplication   Size   Complexity  
A reverse() 0 4 1
A leftJoin() 0 4 1
B sort() 0 45 11
A createDataObject() 0 21 4
A __clone() 0 3 1
A exclude() 0 19 5
A getIterator() 0 3 1
A byIDs() 0 3 1
A getQueryParams() 0 3 1
A toNestedArray() 0 9 2
A addFilter() 0 10 2
A sql() 0 3 1
A addMany() 0 6 2
A distinct() 0 4 1
A setDataQueryParam() 0 13 3
A setByIDList() 0 25 6
A subtract() 0 8 2
A innerJoin() 0 4 1
A setAddCallback() 0 4 1
A __construct() 0 6 1
A offsetExists() 0 3 1
A column() 0 3 1
A removeByID() 0 14 5
A setRemoveCallback() 0 4 1
A relation() 0 7 1
A newObject() 0 4 1
A exists() 0 3 1
A find() 0 3 1
A limit() 0 4 1
A filter() 0 18 3
A each() 0 7 2
A applyRelation() 0 26 3
A removeByFilter() 0 6 2
A map() 0 3 1
A alterDataQuery() 0 28 5
A columnUnique() 0 3 1
A offsetGet() 0 3 1
A setDataQuery() 0 5 1
A dataClass() 0 3 1
A last() 0 6 2
A excludeAny() 0 19 5
A dataQuery() 0 3 1
A toArray() 0 11 2
A byID() 0 3 1
A remove() 0 4 1
A sum() 0 3 1
A add() 0 7 2
A canFilterBy() 0 21 5
A getGenerator() 0 6 2
A shuffle() 0 3 1
A where() 0 4 1
A getIDList() 0 4 2
A removeMany() 0 6 2
A getDeleteOnRemove() 0 3 1
A min() 0 3 1
A dbObject() 0 3 1
A createSearchFilter() 0 29 3
A whereAny() 0 4 1
A debug() 0 8 2
A isValidRelationName() 0 3 1
A filterAny() 0 19 5
A setQueriedColumns() 0 4 1
A avg() 0 3 1
A filterByCallback() 0 16 4
A first() 0 6 2
A count() 0 3 1
A offsetSet() 0 3 1
A removeAll() 0 6 2
A offsetUnset() 0 3 1
A canSortBy() 0 3 1
A setDeleteOnRemove() 0 5 1
A max() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like DataList 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 DataList, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace SilverStripe\ORM;
4
5
use SilverStripe\Core\Injector\Injector;
6
use SilverStripe\Dev\Debug;
7
use SilverStripe\ORM\Filters\SearchFilter;
8
use SilverStripe\ORM\Queries\SQLConditionGroup;
9
use SilverStripe\View\ViewableData;
10
use ArrayIterator;
11
use Exception;
12
use InvalidArgumentException;
13
use LogicException;
14
15
/**
16
 * Implements a "lazy loading" DataObjectSet.
17
 * Uses {@link DataQuery} to do the actual query generation.
18
 *
19
 * DataLists are _immutable_ as far as the query they represent is concerned. When you call a method that
20
 * alters the query, a new DataList instance is returned, rather than modifying the existing instance
21
 *
22
 * When you add or remove an element to the list the query remains the same, but because you have modified
23
 * the underlying data the contents of the list changes. These are some of those methods:
24
 *
25
 *   - add
26
 *   - addMany
27
 *   - remove
28
 *   - removeMany
29
 *   - removeByID
30
 *   - removeByFilter
31
 *   - removeAll
32
 *
33
 * Subclasses of DataList may add other methods that have the same effect.
34
 */
35
class DataList extends ViewableData implements SS_List, Filterable, Sortable, Limitable
36
{
37
38
    /**
39
     * The DataObject class name that this data list is querying
40
     *
41
     * @var string
42
     */
43
    protected $dataClass;
44
45
    /**
46
     * The {@link DataQuery} object responsible for getting this DataList's records
47
     *
48
     * @var DataQuery
49
     */
50
    protected $dataQuery;
51
52
    /**
53
     * @var Callable
54
     */
55
    protected $addCallback;
56
57
    /**
58
     * @var Callable
59
     */
60
    protected $removeCallback;
61
62
    /**
63
     * @var bool
64
     */
65
    protected $deleteOnRemove = true;
66
67
    /**
68
     * Create a new DataList.
69
     * No querying is done on construction, but the initial query schema is set up.
70
     *
71
     * @param string $dataClass - The DataObject class to query.
72
     */
73
    public function __construct($dataClass)
74
    {
75
        $this->dataClass = $dataClass;
76
        $this->dataQuery = new DataQuery($this->dataClass);
77
78
        parent::__construct();
79
    }
80
81
    /**
82
     * Get the dataClass name for this DataList, ie the DataObject ClassName
83
     *
84
     * @return string
85
     */
86
    public function dataClass()
87
    {
88
        return $this->dataClass;
89
    }
90
91
    /**
92
     * When cloning this object, clone the dataQuery object as well
93
     */
94
    public function __clone()
95
    {
96
        $this->dataQuery = clone $this->dataQuery;
97
    }
98
99
    /**
100
     * Return a copy of the internal {@link DataQuery} object
101
     *
102
     * Because the returned value is a copy, modifying it won't affect this list's contents. If
103
     * you want to alter the data query directly, use the alterDataQuery method
104
     *
105
     * @return DataQuery
106
     */
107
    public function dataQuery()
108
    {
109
        return clone $this->dataQuery;
110
    }
111
112
    /**
113
     * @var bool - Indicates if we are in an alterDataQueryCall already, so alterDataQuery can be re-entrant
114
     */
115
    protected $inAlterDataQueryCall = false;
116
117
    /**
118
     * Return a new DataList instance with the underlying {@link DataQuery} object altered
119
     *
120
     * If you want to alter the underlying dataQuery for this list, this wrapper method
121
     * will ensure that you can do so without mutating the existing List object.
122
     *
123
     * It clones this list, calls the passed callback function with the dataQuery of the new
124
     * list as it's first parameter (and the list as it's second), then returns the list
125
     *
126
     * Note that this function is re-entrant - it's safe to call this inside a callback passed to
127
     * alterDataQuery
128
     *
129
     * @param Callable $callback
130
     * @return static
131
     * @throws Exception
132
     */
133
    public function alterDataQuery($callback)
134
    {
135
        if ($this->inAlterDataQueryCall) {
136
            $list = $this;
137
138
            $res = call_user_func($callback, $list->dataQuery, $list);
139
            if ($res) {
140
                $list->dataQuery = $res;
141
            }
142
143
            return $list;
144
        }
145
146
        $list = clone $this;
147
        $list->inAlterDataQueryCall = true;
148
149
        try {
150
            $res = $callback($list->dataQuery, $list);
151
            if ($res) {
152
                $list->dataQuery = $res;
153
            }
154
        } catch (Exception $e) {
155
            $list->inAlterDataQueryCall = false;
156
            throw $e;
157
        }
158
159
        $list->inAlterDataQueryCall = false;
160
        return $list;
161
    }
162
163
    /**
164
     * Return a new DataList instance with the underlying {@link DataQuery} object changed
165
     *
166
     * @param DataQuery $dataQuery
167
     * @return static
168
     */
169
    public function setDataQuery(DataQuery $dataQuery)
170
    {
171
        $clone = clone $this;
172
        $clone->dataQuery = $dataQuery;
173
        return $clone;
174
    }
175
176
    /**
177
     * Returns a new DataList instance with the specified query parameter assigned
178
     *
179
     * @param string|array $keyOrArray Either the single key to set, or an array of key value pairs to set
180
     * @param mixed $val If $keyOrArray is not an array, this is the value to set
181
     * @return static
182
     */
183
    public function setDataQueryParam($keyOrArray, $val = null)
184
    {
185
        $clone = clone $this;
186
187
        if (is_array($keyOrArray)) {
188
            foreach ($keyOrArray as $key => $value) {
189
                $clone->dataQuery->setQueryParam($key, $value);
190
            }
191
        } else {
192
            $clone->dataQuery->setQueryParam($keyOrArray, $val);
193
        }
194
195
        return $clone;
196
    }
197
198
    /**
199
     * Returns the SQL query that will be used to get this DataList's records.  Good for debugging. :-)
200
     *
201
     * @param array $parameters Out variable for parameters required for this query
202
     * @return string The resulting SQL query (may be parameterised)
203
     */
204
    public function sql(&$parameters = [])
205
    {
206
        return $this->dataQuery->query()->sql($parameters);
207
    }
208
209
    /**
210
     * Return a new DataList instance with a WHERE clause added to this list's query.
211
     *
212
     * Supports parameterised queries.
213
     * See SQLSelect::addWhere() for syntax examples, although DataList
214
     * won't expand multiple method arguments as SQLSelect does.
215
     *
216
     * @param string|array|SQLConditionGroup $filter Predicate(s) to set, as escaped SQL statements or
217
     * paramaterised queries
218
     * @return static
219
     */
220
    public function where($filter)
221
    {
222
        return $this->alterDataQuery(function (DataQuery $query) use ($filter) {
223
            $query->where($filter);
224
        });
225
    }
226
227
    /**
228
     * Return a new DataList instance with a WHERE clause added to this list's query.
229
     * All conditions provided in the filter will be joined with an OR
230
     *
231
     * Supports parameterised queries.
232
     * See SQLSelect::addWhere() for syntax examples, although DataList
233
     * won't expand multiple method arguments as SQLSelect does.
234
     *
235
     * @param string|array|SQLConditionGroup $filter Predicate(s) to set, as escaped SQL statements or
236
     * paramaterised queries
237
     * @return static
238
     */
239
    public function whereAny($filter)
240
    {
241
        return $this->alterDataQuery(function (DataQuery $query) use ($filter) {
242
            $query->whereAny($filter);
243
        });
244
    }
245
246
247
248
    /**
249
     * Returns true if this DataList can be sorted by the given field.
250
     *
251
     * @param string $fieldName
252
     * @return boolean
253
     */
254
    public function canSortBy($fieldName)
255
    {
256
        return $this->dataQuery()->query()->canSortBy($fieldName);
257
    }
258
259
    /**
260
     * Returns true if this DataList can be filtered by the given field.
261
     *
262
     * @param string $fieldName (May be a related field in dot notation like Member.FirstName)
263
     * @return boolean
264
     */
265
    public function canFilterBy($fieldName)
266
    {
267
        $model = singleton($this->dataClass);
268
        $relations = explode(".", $fieldName);
269
        // First validate the relationships
270
        $fieldName = array_pop($relations);
271
        foreach ($relations as $r) {
272
            $relationClass = $model->getRelationClass($r);
273
            if (!$relationClass) {
274
                return false;
275
            }
276
            $model = singleton($relationClass);
277
            if (!$model) {
278
                return false;
279
            }
280
        }
281
        // Then check field
282
        if ($model->hasDatabaseField($fieldName)) {
283
            return true;
284
        }
285
        return false;
286
    }
287
288
    /**
289
     * Return a new DataList instance with the records returned in this query
290
     * restricted by a limit clause.
291
     *
292
     * @param int $limit
293
     * @param int $offset
294
     * @return static
295
     */
296
    public function limit($limit, $offset = 0)
297
    {
298
        return $this->alterDataQuery(function (DataQuery $query) use ($limit, $offset) {
299
            $query->limit($limit, $offset);
300
        });
301
    }
302
303
    /**
304
     * Return a new DataList instance with distinct records or not
305
     *
306
     * @param bool $value
307
     * @return static
308
     */
309
    public function distinct($value)
310
    {
311
        return $this->alterDataQuery(function (DataQuery $query) use ($value) {
312
            $query->distinct($value);
313
        });
314
    }
315
316
    /**
317
     * Return a new DataList instance as a copy of this data list with the sort
318
     * order set.
319
     *
320
     * @see SS_List::sort()
321
     * @see SQLSelect::orderby
322
     * @example $list = $list->sort('Name'); // default ASC sorting
323
     * @example $list = $list->sort('Name DESC'); // DESC sorting
324
     * @example $list = $list->sort('Name', 'ASC');
325
     * @example $list = $list->sort(array('Name'=>'ASC', 'Age'=>'DESC'));
326
     *
327
     * @param String|array Escaped SQL statement. If passed as array, all keys and values are assumed to be escaped.
0 ignored issues
show
Bug introduced by
The type SilverStripe\ORM\Escaped 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...
328
     * @return static
329
     */
330
    public function sort()
331
    {
332
        $count = func_num_args();
333
334
        if ($count == 0) {
335
            return $this;
336
        }
337
338
        if ($count > 2) {
339
            throw new InvalidArgumentException('This method takes zero, one or two arguments');
340
        }
341
342
        if ($count == 2) {
343
            $col = null;
344
            $dir = null;
345
            list($col, $dir) = func_get_args();
346
347
            // Validate direction
348
            if (!in_array(strtolower($dir), ['desc', 'asc'])) {
349
                user_error('Second argument to sort must be either ASC or DESC');
350
            }
351
352
            $sort = [$col => $dir];
353
        } else {
354
            $sort = func_get_arg(0);
355
        }
356
357
        return $this->alterDataQuery(function (DataQuery $query, DataList $list) use ($sort) {
358
359
            if (is_string($sort) && $sort) {
360
                if (false !== stripos($sort, ' asc') || false !== stripos($sort, ' desc')) {
361
                    $query->sort($sort);
362
                } else {
363
                    $list->applyRelation($sort, $column, true);
364
                    $query->sort($column, 'ASC');
365
                }
366
            } elseif (is_array($sort)) {
367
                // sort(array('Name'=>'desc'));
368
                $query->sort(null, null); // wipe the sort
369
370
                foreach ($sort as $column => $direction) {
371
                    // Convert column expressions to SQL fragment, while still allowing the passing of raw SQL
372
                    // fragments.
373
                    $list->applyRelation($column, $relationColumn, true);
374
                    $query->sort($relationColumn, $direction, false);
375
                }
376
            }
377
        });
378
    }
379
380
    /**
381
     * Return a copy of this list which only includes items with these characteristics
382
     *
383
     * @see Filterable::filter()
384
     *
385
     * @example $list = $list->filter('Name', 'bob'); // only bob in the list
386
     * @example $list = $list->filter('Name', array('aziz', 'bob'); // aziz and bob in list
387
     * @example $list = $list->filter(array('Name'=>'bob', 'Age'=>21)); // bob with the age 21
388
     * @example $list = $list->filter(array('Name'=>'bob', 'Age'=>array(21, 43))); // bob with the Age 21 or 43
389
     * @example $list = $list->filter(array('Name'=>array('aziz','bob'), 'Age'=>array(21, 43)));
390
     *          // aziz with the age 21 or 43 and bob with the Age 21 or 43
391
     *
392
     * Note: When filtering on nullable columns, null checks will be automatically added.
393
     * E.g. ->filter('Field:not', 'value) will generate '... OR "Field" IS NULL', and
394
     * ->filter('Field:not', null) will generate '"Field" IS NOT NULL'
395
     *
396
     * @todo extract the sql from $customQuery into a SQLGenerator class
397
     *
398
     * @param string|array Escaped SQL statement. If passed as array, all keys and values will be escaped internally
399
     * @return $this
400
     */
401
    public function filter()
402
    {
403
        // Validate and process arguments
404
        $arguments = func_get_args();
405
        switch (sizeof($arguments)) {
406
            case 1:
407
                $filters = $arguments[0];
408
409
                break;
410
            case 2:
411
                $filters = [$arguments[0] => $arguments[1]];
412
413
                break;
414
            default:
415
                throw new InvalidArgumentException('Incorrect number of arguments passed to filter()');
416
        }
417
418
        return $this->addFilter($filters);
419
    }
420
421
    /**
422
     * Return a new instance of the list with an added filter
423
     *
424
     * @param array $filterArray
425
     * @return $this
426
     */
427
    public function addFilter($filterArray)
428
    {
429
        $list = $this;
430
431
        foreach ($filterArray as $expression => $value) {
432
            $filter = $this->createSearchFilter($expression, $value);
433
            $list = $list->alterDataQuery([$filter, 'apply']);
434
        }
435
436
        return $list;
437
    }
438
439
    /**
440
     * Return a copy of this list which contains items matching any of these characteristics.
441
     *
442
     * @example // only bob in the list
443
     *          $list = $list->filterAny('Name', 'bob');
444
     *          // SQL: WHERE "Name" = 'bob'
445
     * @example // azis or bob in the list
446
     *          $list = $list->filterAny('Name', array('aziz', 'bob');
447
     *          // SQL: WHERE ("Name" IN ('aziz','bob'))
448
     * @example // bob or anyone aged 21 in the list
449
     *          $list = $list->filterAny(array('Name'=>'bob, 'Age'=>21));
450
     *          // SQL: WHERE ("Name" = 'bob' OR "Age" = '21')
451
     * @example // bob or anyone aged 21 or 43 in the list
452
     *          $list = $list->filterAny(array('Name'=>'bob, 'Age'=>array(21, 43)));
453
     *          // SQL: WHERE ("Name" = 'bob' OR ("Age" IN ('21', '43'))
454
     * @example // all bobs, phils or anyone aged 21 or 43 in the list
455
     *          $list = $list->filterAny(array('Name'=>array('bob','phil'), 'Age'=>array(21, 43)));
456
     *          // SQL: WHERE (("Name" IN ('bob', 'phil')) OR ("Age" IN ('21', '43'))
457
     *
458
     * @todo extract the sql from this method into a SQLGenerator class
459
     *
460
     * @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...
461
     * @return static
462
     */
463
    public function filterAny()
464
    {
465
        $numberFuncArgs = count(func_get_args());
466
        $whereArguments = [];
467
468
        if ($numberFuncArgs == 1 && is_array(func_get_arg(0))) {
469
            $whereArguments = func_get_arg(0);
470
        } elseif ($numberFuncArgs == 2) {
471
            $whereArguments[func_get_arg(0)] = func_get_arg(1);
472
        } else {
473
            throw new InvalidArgumentException('Incorrect number of arguments passed to filterAny()');
474
        }
475
476
        return $this->alterDataQuery(function (DataQuery $query) use ($whereArguments) {
477
            $subquery = $query->disjunctiveGroup();
478
479
            foreach ($whereArguments as $field => $value) {
480
                $filter = $this->createSearchFilter($field, $value);
481
                $filter->apply($subquery);
482
            }
483
        });
484
    }
485
486
    /**
487
     * Note that, in the current implementation, the filtered list will be an ArrayList, but this may change in a
488
     * future implementation.
489
     * @param Callable $callback
490
     * @return ArrayList (this may change in future implementations)
491
     *@see Filterable::filterByCallback()
492
     *
493
     * @example $list = $list->filterByCallback(function($item, $list) { return $item->Age == 9; })
494
     */
495
    public function filterByCallback($callback)
496
    {
497
        if (!is_callable($callback)) {
498
            throw new LogicException(sprintf(
499
                "SS_Filterable::filterByCallback() passed callback must be callable, '%s' given",
500
                gettype($callback)
501
            ));
502
        }
503
        /** @var ArrayList $output */
504
        $output = ArrayList::create();
505
        foreach ($this as $item) {
506
            if (call_user_func($callback, $item, $this)) {
507
                $output->push($item);
508
            }
509
        }
510
        return $output;
511
    }
512
513
    /**
514
     * Given a field or relation name, apply it safely to this datalist.
515
     *
516
     * Unlike getRelationName, this is immutable and will fallback to the quoted field
517
     * name if not a relation.
518
     *
519
     * Example use (simple WHERE condition on data sitting in a related table):
520
     *
521
     * <code>
522
     *  $columnName = null;
523
     *  $list = Page::get()
524
     *    ->applyRelation('TaxonomyTerms.ID', $columnName)
525
     *    ->where([$columnName => 'my value']);
526
     * </code>
527
     *
528
     *
529
     * @param string $field Name of field or relation to apply
530
     * @param string &$columnName Quoted column name
531
     * @param bool $linearOnly Set to true to restrict to linear relations only. Set this
532
     * if this relation will be used for sorting, and should not include duplicate rows.
533
     * @return $this DataList with this relation applied
534
     */
535
    public function applyRelation($field, &$columnName = null, $linearOnly = false)
536
    {
537
        // If field is invalid, return it without modification
538
        if (!$this->isValidRelationName($field)) {
539
            $columnName = $field;
540
            return $this;
541
        }
542
543
        // Simple fields without relations are mapped directly
544
        if (strpos($field, '.') === false) {
545
            $columnName = '"' . $field . '"';
546
            return $this;
547
        }
548
549
        return $this->alterDataQuery(
550
            function (DataQuery $query) use ($field, &$columnName, $linearOnly) {
551
                $relations = explode('.', $field);
552
                $fieldName = array_pop($relations);
553
554
                // Apply relation
555
                $relationModelName = $query->applyRelation($relations, $linearOnly);
556
                $relationPrefix = $query->applyRelationPrefix($relations);
557
558
                // Find the db field the relation belongs to
559
                $columnName = DataObject::getSchema()
560
                    ->sqlColumnForField($relationModelName, $fieldName, $relationPrefix);
561
            }
562
        );
563
    }
564
565
    /**
566
     * Check if the given field specification could be interpreted as an unquoted relation name
567
     *
568
     * @param string $field
569
     * @return bool
570
     */
571
    protected function isValidRelationName($field)
572
    {
573
        return preg_match('/^[A-Z0-9._]+$/i', $field);
0 ignored issues
show
Bug Best Practice introduced by
The expression return preg_match('/^[A-Z0-9._]+$/i', $field) returns the type integer which is incompatible with the documented return type boolean.
Loading history...
574
    }
575
576
    /**
577
     * Given a filter expression and value construct a {@see SearchFilter} instance
578
     *
579
     * @param string $filter E.g. `Name:ExactMatch:not`, `Name:ExactMatch`, `Name:not`, `Name`
580
     * @param mixed $value Value of the filter
581
     * @return SearchFilter
582
     */
583
    protected function createSearchFilter($filter, $value)
584
    {
585
        // Field name is always the first component
586
        $fieldArgs = explode(':', $filter);
587
        $fieldName = array_shift($fieldArgs);
588
589
        // Inspect type of second argument to determine context
590
        $secondArg = array_shift($fieldArgs);
591
        $modifiers = $fieldArgs;
592
        if (!$secondArg) {
593
            // Use default filter if none specified. E.g. `->filter(['Name' => $myname])`
594
            $filterServiceName = 'DataListFilter.default';
595
        } else {
596
            // The presence of a second argument is by default ambiguous; We need to query
597
            // Whether this is a valid modifier on the default filter, or a filter itself.
598
            /** @var SearchFilter $defaultFilterInstance */
599
            $defaultFilterInstance = Injector::inst()->get('DataListFilter.default');
600
            if (in_array(strtolower($secondArg), $defaultFilterInstance->getSupportedModifiers())) {
601
                // Treat second (and any subsequent) argument as modifiers, using default filter
602
                $filterServiceName = 'DataListFilter.default';
603
                array_unshift($modifiers, $secondArg);
604
            } else {
605
                // Second argument isn't a valid modifier, so assume is filter identifier
606
                $filterServiceName = "DataListFilter.{$secondArg}";
607
            }
608
        }
609
610
        // Build instance
611
        return Injector::inst()->create($filterServiceName, $fieldName, $value, $modifiers);
612
    }
613
614
    /**
615
     * Return a copy of this list which does not contain any items that match all params
616
     *
617
     * @example $list = $list->exclude('Name', 'bob'); // exclude bob from list
618
     * @example $list = $list->exclude('Name', array('aziz', 'bob'); // exclude aziz and bob from list
619
     * @example $list = $list->exclude(array('Name'=>'bob, 'Age'=>21)); // exclude bob that has Age 21
620
     * @example $list = $list->exclude(array('Name'=>'bob, 'Age'=>array(21, 43))); // exclude bob with Age 21 or 43
621
     * @example $list = $list->exclude(array('Name'=>array('bob','phil'), 'Age'=>array(21, 43)));
622
     *          // bob age 21 or 43, phil age 21 or 43 would be excluded
623
     *
624
     * @todo extract the sql from this method into a SQLGenerator class
625
     *
626
     * @param string|array
627
     * @param string [optional]
628
     *
629
     * @return $this
630
     */
631
    public function exclude()
632
    {
633
        $numberFuncArgs = count(func_get_args());
634
        $whereArguments = [];
635
636
        if ($numberFuncArgs == 1 && is_array(func_get_arg(0))) {
637
            $whereArguments = func_get_arg(0);
638
        } elseif ($numberFuncArgs == 2) {
639
            $whereArguments[func_get_arg(0)] = func_get_arg(1);
640
        } else {
641
            throw new InvalidArgumentException('Incorrect number of arguments passed to exclude()');
642
        }
643
644
        return $this->alterDataQuery(function (DataQuery $query) use ($whereArguments) {
645
            $subquery = $query->disjunctiveGroup();
646
647
            foreach ($whereArguments as $field => $value) {
648
                $filter = $this->createSearchFilter($field, $value);
649
                $filter->exclude($subquery);
650
            }
651
        });
652
    }
653
654
    /**
655
     * Return a copy of this list which does not contain any items with any of these params
656
     *
657
     * @example $list = $list->excludeAny('Name', 'bob'); // exclude bob from list
658
     * @example $list = $list->excludeAny('Name', array('aziz', 'bob'); // exclude aziz and bob from list
659
     * @example $list = $list->excludeAny(array('Name'=>'bob, 'Age'=>21)); // exclude bob or Age 21
660
     * @example $list = $list->excludeAny(array('Name'=>'bob, 'Age'=>array(21, 43))); // exclude bob or Age 21 or 43
661
     * @example $list = $list->excludeAny(array('Name'=>array('bob','phil'), 'Age'=>array(21, 43)));
662
     *          // bob, phil, 21 or 43 would be excluded
663
     *
664
     * @param string|array
665
     * @param string [optional]
666
     *
667
     * @return $this
668
     */
669
    public function excludeAny()
670
    {
671
        $numberFuncArgs = count(func_get_args());
672
        $whereArguments = [];
673
674
        if ($numberFuncArgs == 1 && is_array(func_get_arg(0))) {
675
            $whereArguments = func_get_arg(0);
676
        } elseif ($numberFuncArgs == 2) {
677
            $whereArguments[func_get_arg(0)] = func_get_arg(1);
678
        } else {
679
            throw new InvalidArgumentException('Incorrect number of arguments passed to excludeAny()');
680
        }
681
682
        return $this->alterDataQuery(function (DataQuery $dataQuery) use ($whereArguments) {
683
            foreach ($whereArguments as $field => $value) {
684
                $filter = $this->createSearchFilter($field, $value);
685
                $filter->exclude($dataQuery);
686
            }
687
            return $dataQuery;
688
        });
689
    }
690
691
    /**
692
     * This method returns a copy of this list that does not contain any DataObjects that exists in $list
693
     *
694
     * The $list passed needs to contain the same dataclass as $this
695
     *
696
     * @param DataList $list
697
     * @return static
698
     * @throws InvalidArgumentException
699
     */
700
    public function subtract(DataList $list)
701
    {
702
        if ($this->dataClass() != $list->dataClass()) {
703
            throw new InvalidArgumentException('The list passed must have the same dataclass as this class');
704
        }
705
706
        return $this->alterDataQuery(function (DataQuery $query) use ($list) {
707
            $query->subtract($list->dataQuery());
708
        });
709
    }
710
711
    /**
712
     * Return a new DataList instance with an inner join clause added to this list's query.
713
     *
714
     * @param string $table Table name (unquoted and as escaped SQL)
715
     * @param string $onClause Escaped SQL statement, e.g. '"Table1"."ID" = "Table2"."ID"'
716
     * @param string $alias - if you want this table to be aliased under another name
717
     * @param int $order A numerical index to control the order that joins are added to the query; lower order values
718
     * will cause the query to appear first. The default is 20, and joins created automatically by the
719
     * ORM have a value of 10.
720
     * @param array $parameters Any additional parameters if the join is a parameterised subquery
721
     * @return static
722
     */
723
    public function innerJoin($table, $onClause, $alias = null, $order = 20, $parameters = [])
724
    {
725
        return $this->alterDataQuery(function (DataQuery $query) use ($table, $onClause, $alias, $order, $parameters) {
726
            $query->innerJoin($table, $onClause, $alias, $order, $parameters);
727
        });
728
    }
729
730
    /**
731
     * Return a new DataList instance with a left join clause added to this list's query.
732
     *
733
     * @param string $table Table name (unquoted and as escaped SQL)
734
     * @param string $onClause Escaped SQL statement, e.g. '"Table1"."ID" = "Table2"."ID"'
735
     * @param string $alias - if you want this table to be aliased under another name
736
     * @param int $order A numerical index to control the order that joins are added to the query; lower order values
737
     * will cause the query to appear first. The default is 20, and joins created automatically by the
738
     * ORM have a value of 10.
739
     * @param array $parameters Any additional parameters if the join is a parameterised subquery
740
     * @return static
741
     */
742
    public function leftJoin($table, $onClause, $alias = null, $order = 20, $parameters = [])
743
    {
744
        return $this->alterDataQuery(function (DataQuery $query) use ($table, $onClause, $alias, $order, $parameters) {
745
            $query->leftJoin($table, $onClause, $alias, $order, $parameters);
746
        });
747
    }
748
749
    /**
750
     * Return an array of the actual items that this DataList contains at this stage.
751
     * This is when the query is actually executed.
752
     *
753
     * @return array
754
     */
755
    public function toArray()
756
    {
757
        $query = $this->dataQuery->query();
758
        $rows = $query->execute();
759
        $results = [];
760
761
        foreach ($rows as $row) {
762
            $results[] = $this->createDataObject($row);
763
        }
764
765
        return $results;
766
    }
767
768
    /**
769
     * Return this list as an array and every object it as an sub array as well
770
     *
771
     * @return array
772
     */
773
    public function toNestedArray()
774
    {
775
        $result = [];
776
777
        foreach ($this as $item) {
778
            $result[] = $item->toMap();
779
        }
780
781
        return $result;
782
    }
783
784
    /**
785
     * Walks the list using the specified callback
786
     *
787
     * @param Callable $callback
788
     * @return $this
789
     */
790
    public function each($callback)
791
    {
792
        foreach ($this as $row) {
793
            $callback($row);
794
        }
795
796
        return $this;
797
    }
798
799
    /**
800
     * Returns a generator for this DataList
801
     *
802
     * @return \Generator&DataObject[]
803
     */
804
    public function getGenerator()
805
    {
806
        $query = $this->dataQuery->query()->execute();
807
808
        while ($row = $query->record()) {
809
            yield $this->createDataObject($row);
810
        }
811
    }
812
813
    public function debug()
814
    {
815
        $val = "<h2>" . static::class . "</h2><ul>";
816
        foreach ($this->toNestedArray() as $item) {
817
            $val .= "<li style=\"list-style-type: disc; margin-left: 20px\">" . Debug::text($item) . "</li>";
818
        }
819
        $val .= "</ul>";
820
        return $val;
821
    }
822
823
    /**
824
     * Returns a map of this list
825
     *
826
     * @param string $keyField - the 'key' field of the result array
827
     * @param string $titleField - the value field of the result array
828
     * @return Map
829
     */
830
    public function map($keyField = 'ID', $titleField = 'Title')
831
    {
832
        return new Map($this, $keyField, $titleField);
833
    }
834
835
    /**
836
     * Create a DataObject from the given SQL row
837
     *
838
     * @param array $row
839
     * @return DataObject
840
     */
841
    public function createDataObject($row)
842
    {
843
        $class = $this->dataClass;
844
845
        if (empty($row['ClassName'])) {
846
            $row['ClassName'] = $class;
847
        }
848
849
        // Failover from RecordClassName to ClassName
850
        if (empty($row['RecordClassName'])) {
851
            $row['RecordClassName'] = $row['ClassName'];
852
        }
853
854
        // Instantiate the class mentioned in RecordClassName only if it exists, otherwise default to $this->dataClass
855
        if (class_exists($row['RecordClassName'])) {
856
            $class = $row['RecordClassName'];
857
        }
858
859
        $item = Injector::inst()->create($class, $row, false, $this->getQueryParams());
860
861
        return $item;
862
    }
863
864
    /**
865
     * Get query parameters for this list.
866
     * These values will be assigned as query parameters to newly created objects from this list.
867
     *
868
     * @return array
869
     */
870
    public function getQueryParams()
871
    {
872
        return $this->dataQuery()->getQueryParams();
873
    }
874
875
    /**
876
     * Returns an Iterator for this DataList.
877
     * This function allows you to use DataLists in foreach loops
878
     *
879
     * @return ArrayIterator
880
     */
881
    public function getIterator()
882
    {
883
        return new ArrayIterator($this->toArray());
884
    }
885
886
    /**
887
     * Return the number of items in this DataList
888
     *
889
     * @return int
890
     */
891
    public function count()
892
    {
893
        return $this->dataQuery->count();
894
    }
895
896
    /**
897
     * Return the maximum value of the given field in this DataList
898
     *
899
     * @param string $fieldName
900
     * @return mixed
901
     */
902
    public function max($fieldName)
903
    {
904
        return $this->dataQuery->max($fieldName);
905
    }
906
907
    /**
908
     * Return the minimum value of the given field in this DataList
909
     *
910
     * @param string $fieldName
911
     * @return mixed
912
     */
913
    public function min($fieldName)
914
    {
915
        return $this->dataQuery->min($fieldName);
916
    }
917
918
    /**
919
     * Return the average value of the given field in this DataList
920
     *
921
     * @param string $fieldName
922
     * @return mixed
923
     */
924
    public function avg($fieldName)
925
    {
926
        return $this->dataQuery->avg($fieldName);
927
    }
928
929
    /**
930
     * Return the sum of the values of the given field in this DataList
931
     *
932
     * @param string $fieldName
933
     * @return mixed
934
     */
935
    public function sum($fieldName)
936
    {
937
        return $this->dataQuery->sum($fieldName);
938
    }
939
940
941
    /**
942
     * Returns the first item in this DataList
943
     *
944
     * The object returned is not cached, unlike {@link DataObject::get_one()}
945
     *
946
     * @return DataObject
947
     */
948
    public function first()
949
    {
950
        foreach ($this->dataQuery->firstRow()->execute() as $row) {
951
            return $this->createDataObject($row);
952
        }
953
        return null;
954
    }
955
956
    /**
957
     * Returns the last item in this DataList
958
     *
959
     * The object returned is not cached, unlike {@link DataObject::get_one()}
960
     *
961
     * @return DataObject
962
     */
963
    public function last()
964
    {
965
        foreach ($this->dataQuery->lastRow()->execute() as $row) {
966
            return $this->createDataObject($row);
967
        }
968
        return null;
969
    }
970
971
    /**
972
     * Returns true if this DataList has items
973
     *
974
     * @return bool
975
     */
976
    public function exists()
977
    {
978
        return $this->count() > 0;
979
    }
980
981
    /**
982
     * Find the first DataObject of this DataList where the given key = value
983
     *
984
     * The object returned is not cached, unlike {@link DataObject::get_one()}
985
     *
986
     * @param string $key
987
     * @param string $value
988
     * @return DataObject|null
989
     */
990
    public function find($key, $value)
991
    {
992
        return $this->filter($key, $value)->first();
993
    }
994
995
    /**
996
     * Restrict the columns to fetch into this DataList
997
     *
998
     * @param array $queriedColumns
999
     * @return static
1000
     */
1001
    public function setQueriedColumns($queriedColumns)
1002
    {
1003
        return $this->alterDataQuery(function (DataQuery $query) use ($queriedColumns) {
1004
            $query->setQueriedColumns($queriedColumns);
1005
        });
1006
    }
1007
1008
    /**
1009
     * Filter this list to only contain the given Primary IDs
1010
     *
1011
     * @param array $ids Array of integers
1012
     * @return $this
1013
     */
1014
    public function byIDs($ids)
1015
    {
1016
        return $this->filter('ID', $ids);
1017
    }
1018
1019
    /**
1020
     * Return the first DataObject with the given ID
1021
     *
1022
     * The object returned is not cached, unlike {@link DataObject::get_by_id()}
1023
     *
1024
     * @param int $id
1025
     * @return DataObject
1026
     */
1027
    public function byID($id)
1028
    {
1029
        return $this->filter('ID', $id)->first();
1030
    }
1031
1032
    /**
1033
     * Returns an array of a single field value for all items in the list.
1034
     *
1035
     * @param string $colName
1036
     * @return array
1037
     */
1038
    public function column($colName = "ID")
1039
    {
1040
        return $this->dataQuery->distinct(false)->column($colName);
1041
    }
1042
1043
    /**
1044
     * Returns a unque array of a single field value for all items in the list.
1045
     *
1046
     * @param string $colName
1047
     * @return array
1048
     */
1049
    public function columnUnique($colName = "ID")
1050
    {
1051
        return $this->dataQuery->distinct(true)->column($colName);
1052
    }
1053
1054
    // Member altering methods
1055
1056
    /**
1057
     * Sets the ComponentSet to be the given ID list.
1058
     * Records will be added and deleted as appropriate.
1059
     *
1060
     * @param array $idList List of IDs.
1061
     */
1062
    public function setByIDList($idList)
1063
    {
1064
        $has = [];
1065
1066
        // Index current data
1067
        foreach ($this->column() as $id) {
1068
            $has[$id] = true;
1069
        }
1070
1071
        // Keep track of items to delete
1072
        $itemsToDelete = $has;
1073
1074
        // add items in the list
1075
        // $id is the database ID of the record
1076
        if ($idList) {
1077
            foreach ($idList as $id) {
1078
                unset($itemsToDelete[$id]);
1079
                if ($id && !isset($has[$id])) {
1080
                    $this->add($id);
1081
                }
1082
            }
1083
        }
1084
1085
        // Remove any items that haven't been mentioned
1086
        $this->removeMany(array_keys($itemsToDelete));
1087
    }
1088
1089
    /**
1090
     * Returns an array with both the keys and values set to the IDs of the records in this list.
1091
     * Does not respect sort order. Use ->column("ID") to get an ID list with the current sort.
1092
     *
1093
     * @return array
1094
     */
1095
    public function getIDList()
1096
    {
1097
        $ids = $this->column("ID");
1098
        return $ids ? array_combine($ids, $ids) : [];
0 ignored issues
show
Bug Best Practice introduced by
The expression return $ids ? array_combine($ids, $ids) : array() could also return false which is incompatible with the documented return type array. Did you maybe forget to handle an error condition?

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

Loading history...
1099
    }
1100
1101
    /**
1102
     * Returns a HasManyList or ManyMany list representing the querying of a relation across all
1103
     * objects in this data list.  For it to work, the relation must be defined on the data class
1104
     * that you used to create this DataList.
1105
     *
1106
     * Example: Get members from all Groups:
1107
     *
1108
     *     DataList::Create(\SilverStripe\Security\Group::class)->relation("Members")
1109
     *
1110
     * @param string $relationName
1111
     * @return HasManyList|ManyManyList
1112
     */
1113
    public function relation($relationName)
1114
    {
1115
        $ids = $this->column('ID');
1116
        $singleton = DataObject::singleton($this->dataClass);
1117
        /** @var HasManyList|ManyManyList $relation */
1118
        $relation = $singleton->$relationName($ids);
1119
        return $relation;
1120
    }
1121
1122
    public function dbObject($fieldName)
1123
    {
1124
        return singleton($this->dataClass)->dbObject($fieldName);
1125
    }
1126
1127
    /**
1128
     * Add a number of items to the component set.
1129
     *
1130
     * @param array $items Items to add, as either DataObjects or IDs.
1131
     * @return $this
1132
     */
1133
    public function addMany($items)
1134
    {
1135
        foreach ($items as $item) {
1136
            $this->add($item);
1137
        }
1138
        return $this;
1139
    }
1140
1141
    /**
1142
     * Remove the items from this list with the given IDs
1143
     *
1144
     * @param array $idList
1145
     * @return $this
1146
     */
1147
    public function removeMany($idList)
1148
    {
1149
        foreach ($idList as $id) {
1150
            $this->removeByID($id);
1151
        }
1152
        return $this;
1153
    }
1154
1155
    /**
1156
     * Remove every element in this DataList matching the given $filter.
1157
     *
1158
     * @param string|array $filter - a sql type where filter
1159
     * @return $this
1160
     */
1161
    public function removeByFilter($filter)
1162
    {
1163
        foreach ($this->where($filter) as $item) {
1164
            $this->remove($item);
1165
        }
1166
        return $this;
1167
    }
1168
1169
    /**
1170
     * Shuffle the datalist using a random function provided by the SQL engine
1171
     *
1172
     * @return $this
1173
     */
1174
    public function shuffle()
1175
    {
1176
        return $this->sort(DB::get_conn()->random());
1177
    }
1178
1179
    /**
1180
     * Remove every element in this DataList.
1181
     *
1182
     * @return $this
1183
     */
1184
    public function removeAll()
1185
    {
1186
        foreach ($this as $item) {
1187
            $this->remove($item);
1188
        }
1189
        return $this;
1190
    }
1191
1192
    /**
1193
     * Set a callback that is called after the add() action is completed.
1194
     *
1195
     * Callback will be passed ($this, $item, $extraFields).
1196
     * If a relation methods is manually defined, this can be called to adjust the behaviour
1197
     * when adding records to this list.
1198
     *
1199
     * Needs to be defined through an overloaded relationship getter
1200
     * to ensure it is set consistently. These getters return a new object
1201
     * every time they're called.
1202
     *
1203
     * Example:
1204
     *
1205
     * ```php
1206
     * class MyParent extends DataObject()
1207
     * {
1208
     *   private static $many_many = [
1209
     *     'MyRelationship' => MyChild::class,
1210
     *   ];
1211
     *
1212
     *   // Standard relationship
1213
     *   public function MyRelationship()
1214
     *   {
1215
     *     $list = $this->getManyManyComponents('MyRelationship');
1216
     *     $list->setAddCallback(function ($item) {
1217
     *       // ...
1218
     *     });
1219
     *     return $list;
1220
     *   }
1221
     *
1222
     *   // "Virtual" relationship
1223
     *   public static function active_children()
1224
     *   {
1225
     *     $list = MyChild::get()->filter('Active', true); // returns a DataList
1226
     *     $list->setAddCallback(function ($item) {
1227
     *       $item->Active = true;
1228
     *       $item->write();
1229
     *     });
1230
     *     $list->setDeleteOnRemove(false);
1231
     *     $list->setRemoveCallback(function ($ids) {
1232
     *       foreach ($ids as $id) {
1233
     *         $item = MyChild::get()->byID($itemID);
1234
     *         $item->Active = false;
1235
     *         $item->write();
1236
     *       }
1237
1238
     *     });
1239
     *     return $list;
1240
     *   }
1241
     * }
1242
     * ```
1243
     *
1244
     * Note that subclasses of RelationList must implement the callback for it to function
1245
     *
1246
     * @return this
1247
     */
1248
    public function setAddCallback($callback): self
1249
    {
1250
        $this->addCallback = $callback;
1251
        return $this;
1252
    }
1253
1254
    /**
1255
     * This method are overloaded by HasManyList and ManyMany list to perform more sophisticated
1256
     * list manipulation
1257
     *
1258
     * @param mixed $item
1259
     */
1260
    public function add($item)
1261
    {
1262
        // No state change needs to happen by default.
1263
1264
        if ($this->addCallback) {
1265
            $callback = $this->addCallback;
1266
            $callback($this, $item);
1267
        }
1268
    }
1269
1270
    /**
1271
     * Return a new item to add to this DataList.
1272
     *
1273
     * @todo This doesn't factor in filters.
1274
     * @param array $initialFields
1275
     * @return DataObject
1276
     */
1277
    public function newObject($initialFields = null)
1278
    {
1279
        $class = $this->dataClass;
1280
        return Injector::inst()->create($class, $initialFields, false);
1281
    }
1282
1283
    /**
1284
     * Remove this item by deleting it
1285
     *
1286
     * @param DataObject $item
1287
     * @todo Allow for amendment of this behaviour - for example, we can remove an item from
1288
     * an "ActiveItems" DataList by chaning the status to inactive.
1289
     */
1290
    public function remove($item)
1291
    {
1292
        // By default, we remove an item from a DataList by deleting it.
1293
        $this->removeByID($item->ID);
1294
    }
1295
1296
    /**
1297
     * Remove an item from this DataList by ID
1298
     *
1299
     * @param int $itemID The primary ID
1300
     */
1301
    public function removeByID($itemID)
1302
    {
1303
        $item = $this->byID($itemID);
1304
1305
        // If we're not deleting the record,
1306
        // a removeCallback might be handling it differently (see setAddCallback() docs)
1307
        if ($item && $this->deleteOnRemove) {
1308
            $item->delete();
1309
        }
1310
1311
        // Call removeCallback, if applicable
1312
        if ($item && $this->removeCallback) {
1313
            $callback = $this->removeCallback;
1314
            $callback($this, [$item->ID]);
1315
        }
1316
    }
1317
1318
    /**
1319
     * Set a callback that is called after the remove() action is completed.
1320
     * Callback will be passed ($this, $removedIds).
1321
     *
1322
     * See {@link setAddCallback()} for details.
1323
     *
1324
     * @return this
1325
     */
1326
    public function setRemoveCallback($callback): self
1327
    {
1328
        $this->removeCallback = $callback;
1329
        return $this;
1330
    }
1331
1332
    /**
1333
     * Delete records in this list on remove*() calls.
1334
     * Only makes sense in lists which aren't bound to a relationship.
1335
     * Often used in conjunction with {@link setRemoveCallback()}
1336
     * to create "virtual" relationships. See {@link setAddCallback()} for details.
1337
     *
1338
     * @param bool $bool
1339
     * @return self
1340
     */
1341
    public function setDeleteOnRemove(bool $bool): self
1342
    {
1343
        $this->deleteOnRemove = $bool;
1344
1345
        return $this;
1346
    }
1347
1348
    public function getDeleteOnRemove(): bool
1349
    {
1350
        return $this->deleteOnRemove;
1351
    }
1352
1353
    /**
1354
     * Reverses a list of items.
1355
     *
1356
     * @return static
1357
     */
1358
    public function reverse()
1359
    {
1360
        return $this->alterDataQuery(function (DataQuery $query) {
1361
            $query->reverseSort();
1362
        });
1363
    }
1364
1365
    /**
1366
     * Returns whether an item with $key exists
1367
     *
1368
     * @param mixed $key
1369
     * @return bool
1370
     */
1371
    public function offsetExists($key)
1372
    {
1373
        return ($this->limit(1, $key)->first() != null);
1374
    }
1375
1376
    /**
1377
     * Returns item stored in list with index $key
1378
     *
1379
     * The object returned is not cached, unlike {@link DataObject::get_one()}
1380
     *
1381
     * @param mixed $key
1382
     * @return DataObject
1383
     */
1384
    public function offsetGet($key)
1385
    {
1386
        return $this->limit(1, $key)->first();
1387
    }
1388
1389
    /**
1390
     * Set an item with the key in $key
1391
     *
1392
     * @param mixed $key
1393
     * @param mixed $value
1394
     */
1395
    public function offsetSet($key, $value)
1396
    {
1397
        user_error("Can't alter items in a DataList using array-access", E_USER_ERROR);
1398
    }
1399
1400
    /**
1401
     * Unset an item with the key in $key
1402
     *
1403
     * @param mixed $key
1404
     */
1405
    public function offsetUnset($key)
1406
    {
1407
        user_error("Can't alter items in a DataList using array-access", E_USER_ERROR);
1408
    }
1409
}
1410