DataList   F
last analyzed

Complexity

Total Complexity 141

Size/Duplication

Total Lines 1297
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 303
c 0
b 0
f 0
dl 0
loc 1297
rs 2
wmc 141

69 Methods

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

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
897
        }
898
899
        return $this->finalisedQuery;
900
    }
901
902
    /**
903
     * Return the number of items in this DataList
904
     *
905
     * @return int
906
     */
907
    public function count()
908
    {
909
        if ($this->finalisedQuery) {
910
            return $this->finalisedQuery->numRecords();
911
        }
912
913
        return $this->dataQuery->count();
914
    }
915
916
    /**
917
     * Return the maximum value of the given field in this DataList
918
     *
919
     * @param string $fieldName
920
     * @return mixed
921
     */
922
    public function max($fieldName)
923
    {
924
        return $this->dataQuery->max($fieldName);
925
    }
926
927
    /**
928
     * Return the minimum value of the given field in this DataList
929
     *
930
     * @param string $fieldName
931
     * @return mixed
932
     */
933
    public function min($fieldName)
934
    {
935
        return $this->dataQuery->min($fieldName);
936
    }
937
938
    /**
939
     * Return the average value of the given field in this DataList
940
     *
941
     * @param string $fieldName
942
     * @return mixed
943
     */
944
    public function avg($fieldName)
945
    {
946
        return $this->dataQuery->avg($fieldName);
947
    }
948
949
    /**
950
     * Return the sum of the values of the given field in this DataList
951
     *
952
     * @param string $fieldName
953
     * @return mixed
954
     */
955
    public function sum($fieldName)
956
    {
957
        return $this->dataQuery->sum($fieldName);
958
    }
959
960
961
    /**
962
     * Returns the first item in this DataList
963
     *
964
     * @return DataObject
965
     */
966
    public function first()
967
    {
968
        foreach ($this->dataQuery->firstRow()->execute() as $row) {
969
            return $this->createDataObject($row);
970
        }
971
        return null;
972
    }
973
974
    /**
975
     * Returns the last item in this DataList
976
     *
977
     *  @return DataObject
978
     */
979
    public function last()
980
    {
981
        foreach ($this->dataQuery->lastRow()->execute() as $row) {
982
            return $this->createDataObject($row);
983
        }
984
        return null;
985
    }
986
987
    /**
988
     * Returns true if this DataList has items
989
     *
990
     * @return bool
991
     */
992
    public function exists()
993
    {
994
        return $this->count() > 0;
995
    }
996
997
    /**
998
     * Find the first DataObject of this DataList where the given key = value
999
     *
1000
     * @param string $key
1001
     * @param string $value
1002
     * @return DataObject|null
1003
     */
1004
    public function find($key, $value)
1005
    {
1006
        return $this->filter($key, $value)->first();
1007
    }
1008
1009
    /**
1010
     * Restrict the columns to fetch into this DataList
1011
     *
1012
     * @param array $queriedColumns
1013
     * @return static
1014
     */
1015
    public function setQueriedColumns($queriedColumns)
1016
    {
1017
        return $this->alterDataQuery(function (DataQuery $query) use ($queriedColumns) {
1018
            $query->setQueriedColumns($queriedColumns);
1019
        });
1020
    }
1021
1022
    /**
1023
     * Filter this list to only contain the given Primary IDs
1024
     *
1025
     * @param array $ids Array of integers
1026
     * @throws InvalidArgumentException
1027
     * @return $this
1028
     */
1029
    public function byIDs($ids)
1030
    {
1031
        $intIDs = array();
1032
        foreach ($ids as $id) {
1033
            if (!is_numeric($id)) {
1034
                throw new InvalidArgumentException(
1035
                    'Invalid value passed to byIDs() in param array. All params have to be numeric or of type Integer.'
1036
                );
1037
            }
1038
            $intIDs[] = intval($id, 10);
1039
        }
1040
1041
        return $this->filter('ID', $intIDs);
1042
    }
1043
1044
    /**
1045
     * Return the first DataObject with the given ID
1046
     *
1047
     * @param int $id
1048
     * @return DataObject
1049
     * @throws InvalidArgumentException
1050
     */
1051
    public function byID($id)
1052
    {
1053
        if (!is_numeric($id)) {
0 ignored issues
show
introduced by
The condition is_numeric($id) is always true.
Loading history...
1054
            throw new InvalidArgumentException(
1055
                'Incorrect param type for $id passed to byID(). Numeric value is expected.'
1056
            );
1057
        }
1058
        return $this->filter('ID', intval($id, 10))->first();
1059
    }
1060
1061
    /**
1062
     * Returns an array of a single field value for all items in the list.
1063
     *
1064
     * @param string $colName
1065
     * @return array
1066
     */
1067
    public function column($colName = "ID")
1068
    {
1069
        if ($this->finalisedQuery) {
1070
            return $this->finalisedQuery->distinct(false)->column($colName);
1071
        }
1072
1073
        return $this->dataQuery->distinct(false)->column($colName);
1074
    }
1075
1076
    /**
1077
     * Returns a unique array of a single field value for all items in the list.
1078
     *
1079
     * @param string $colName
1080
     * @return array
1081
     */
1082
    public function columnUnique($colName = "ID")
1083
    {
1084
        if ($this->finalisedQuery) {
1085
            return $this->finalisedQuery->distinct(true)->column($colName);
1086
        }
1087
        return $this->dataQuery->distinct(true)->column($colName);
1088
    }
1089
1090
    // Member altering methods
1091
1092
    /**
1093
     * Sets the ComponentSet to be the given ID list.
1094
     * Records will be added and deleted as appropriate.
1095
     *
1096
     * @param array $idList List of IDs.
1097
     */
1098
    public function setByIDList($idList)
1099
    {
1100
        $has = [];
1101
1102
        // Index current data
1103
        foreach ($this->column() as $id) {
1104
            $has[$id] = true;
1105
        }
1106
1107
        // Keep track of items to delete
1108
        $itemsToDelete = $has;
1109
1110
        // add items in the list
1111
        // $id is the database ID of the record
1112
        if ($idList) {
1113
            foreach ($idList as $id) {
1114
                unset($itemsToDelete[$id]);
1115
                if ($id && !isset($has[$id])) {
1116
                    $this->add($id);
1117
                }
1118
            }
1119
        }
1120
1121
        // Remove any items that haven't been mentioned
1122
        $this->removeMany(array_keys($itemsToDelete));
1123
    }
1124
1125
    /**
1126
     * Returns an array with both the keys and values set to the IDs of the records in this list.
1127
     * Does not respect sort order. Use ->column("ID") to get an ID list with the current sort.
1128
     *
1129
     * @return array
1130
     */
1131
    public function getIDList()
1132
    {
1133
        $ids = $this->column("ID");
1134
        return $ids ? array_combine($ids, $ids) : [];
1135
    }
1136
1137
    /**
1138
     * Returns a HasManyList or ManyMany list representing the querying of a relation across all
1139
     * objects in this data list.  For it to work, the relation must be defined on the data class
1140
     * that you used to create this DataList.
1141
     *
1142
     * Example: Get members from all Groups:
1143
     *
1144
     *     DataList::Create(\SilverStripe\Security\Group::class)->relation("Members")
1145
     *
1146
     * @param string $relationName
1147
     * @return HasManyList|ManyManyList
1148
     */
1149
    public function relation($relationName)
1150
    {
1151
        $ids = $this->column('ID');
1152
        $singleton = DataObject::singleton($this->dataClass);
1153
        /** @var HasManyList|ManyManyList $relation */
1154
        $relation = $singleton->$relationName($ids);
1155
        return $relation;
1156
    }
1157
1158
    public function dbObject($fieldName)
1159
    {
1160
        return singleton($this->dataClass)->dbObject($fieldName);
1161
    }
1162
1163
    /**
1164
     * Add a number of items to the component set.
1165
     *
1166
     * @param array $items Items to add, as either DataObjects or IDs.
1167
     * @return $this
1168
     */
1169
    public function addMany($items)
1170
    {
1171
        foreach ($items as $item) {
1172
            $this->add($item);
1173
        }
1174
        return $this;
1175
    }
1176
1177
    /**
1178
     * Remove the items from this list with the given IDs
1179
     *
1180
     * @param array $idList
1181
     * @return $this
1182
     */
1183
    public function removeMany($idList)
1184
    {
1185
        foreach ($idList as $id) {
1186
            $this->removeByID($id);
1187
        }
1188
        return $this;
1189
    }
1190
1191
    /**
1192
     * Remove every element in this DataList matching the given $filter.
1193
     *
1194
     * @param string|array $filter - a sql type where filter
1195
     * @return $this
1196
     */
1197
    public function removeByFilter($filter)
1198
    {
1199
        foreach ($this->where($filter) as $item) {
1200
            $this->remove($item);
1201
        }
1202
        return $this;
1203
    }
1204
1205
    /**
1206
     * Shuffle the datalist using a random function provided by the SQL engine
1207
     *
1208
     * @return $this
1209
     */
1210
    public function shuffle()
1211
    {
1212
        return $this->sort(DB::get_conn()->random());
1213
    }
1214
1215
    /**
1216
     * Remove every element in this DataList.
1217
     *
1218
     * @return $this
1219
     */
1220
    public function removeAll()
1221
    {
1222
        foreach ($this as $item) {
1223
            $this->remove($item);
1224
        }
1225
        return $this;
1226
    }
1227
1228
    /**
1229
     * This method are overloaded by HasManyList and ManyMany list to perform more sophisticated
1230
     * list manipulation
1231
     *
1232
     * @param mixed $item
1233
     */
1234
    public function add($item)
1235
    {
1236
        // Nothing needs to happen by default
1237
        // TO DO: If a filter is given to this data list then
1238
    }
1239
1240
    /**
1241
     * Return a new item to add to this DataList.
1242
     *
1243
     * @todo This doesn't factor in filters.
1244
     * @param array $initialFields
1245
     * @return DataObject
1246
     */
1247
    public function newObject($initialFields = null)
1248
    {
1249
        $class = $this->dataClass;
1250
        return Injector::inst()->create($class, $initialFields, false);
1251
    }
1252
1253
    /**
1254
     * Remove this item by deleting it
1255
     *
1256
     * @param DataObject $item
1257
     * @todo Allow for amendment of this behaviour - for example, we can remove an item from
1258
     * an "ActiveItems" DataList by chaning the status to inactive.
1259
     */
1260
    public function remove($item)
1261
    {
1262
        // By default, we remove an item from a DataList by deleting it.
1263
        $this->removeByID($item->ID);
1264
    }
1265
1266
    /**
1267
     * Remove an item from this DataList by ID
1268
     *
1269
     * @param int $itemID The primary ID
1270
     */
1271
    public function removeByID($itemID)
1272
    {
1273
        $item = $this->byID($itemID);
1274
1275
        if ($item) {
0 ignored issues
show
introduced by
$item is of type SilverStripe\ORM\DataObject, thus it always evaluated to true.
Loading history...
1276
            $item->delete();
1277
        }
1278
    }
1279
1280
    /**
1281
     * Reverses a list of items.
1282
     *
1283
     * @return static
1284
     */
1285
    public function reverse()
1286
    {
1287
        return $this->alterDataQuery(function (DataQuery $query) {
1288
            $query->reverseSort();
1289
        });
1290
    }
1291
1292
    /**
1293
     * Returns whether an item with $key exists
1294
     *
1295
     * @param mixed $key
1296
     * @return bool
1297
     */
1298
    public function offsetExists($key)
1299
    {
1300
        return ($this->limit(1, $key)->first() != null);
1301
    }
1302
1303
    /**
1304
     * Returns item stored in list with index $key
1305
     *
1306
     * @param mixed $key
1307
     * @return DataObject
1308
     */
1309
    public function offsetGet($key)
1310
    {
1311
        return $this->limit(1, $key)->first();
1312
    }
1313
1314
    /**
1315
     * Set an item with the key in $key
1316
     *
1317
     * @param mixed $key
1318
     * @param mixed $value
1319
     */
1320
    public function offsetSet($key, $value)
1321
    {
1322
        user_error("Can't alter items in a DataList using array-access", E_USER_ERROR);
1323
    }
1324
1325
    /**
1326
     * Unset an item with the key in $key
1327
     *
1328
     * @param mixed $key
1329
     */
1330
    public function offsetUnset($key)
1331
    {
1332
        user_error("Can't alter items in a DataList using array-access", E_USER_ERROR);
1333
    }
1334
}
1335