Completed
Push — master ( fe927f...32a670 )
by Robbie
21:23 queued 12:16
created

DataList::getGenerator()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 3
nc 2
nop 0
dl 0
loc 6
rs 10
c 0
b 0
f 0
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'));
0 ignored issues
show
Unused Code Comprehensibility introduced by
82% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
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
     * @param string $field Name of field or relation to apply
514
     * @param string &$columnName Quoted column name
515
     * @param bool $linearOnly Set to true to restrict to linear relations only. Set this
516
     * if this relation will be used for sorting, and should not include duplicate rows.
517
     * @return $this DataList with this relation applied
518
     */
519
    public function applyRelation($field, &$columnName = null, $linearOnly = false)
520
    {
521
        // If field is invalid, return it without modification
522
        if (!$this->isValidRelationName($field)) {
523
            $columnName = $field;
524
            return $this;
525
        }
526
527
        // Simple fields without relations are mapped directly
528
        if (strpos($field, '.') === false) {
529
            $columnName = '"' . $field . '"';
530
            return $this;
531
        }
532
533
        return $this->alterDataQuery(
534
            function (DataQuery $query) use ($field, &$columnName, $linearOnly) {
535
                $relations = explode('.', $field);
536
                $fieldName = array_pop($relations);
537
538
                // Apply relation
539
                $relationModelName = $query->applyRelation($relations, $linearOnly);
540
                $relationPrefix = $query->applyRelationPrefix($relations);
541
542
                // Find the db field the relation belongs to
543
                $columnName = DataObject::getSchema()
544
                    ->sqlColumnForField($relationModelName, $fieldName, $relationPrefix);
545
            }
546
        );
547
    }
548
549
    /**
550
     * Check if the given field specification could be interpreted as an unquoted relation name
551
     *
552
     * @param string $field
553
     * @return bool
554
     */
555
    protected function isValidRelationName($field)
556
    {
557
        return preg_match('/^[A-Z0-9._]+$/i', $field);
558
    }
559
560
    /**
561
     * Given a filter expression and value construct a {@see SearchFilter} instance
562
     *
563
     * @param string $filter E.g. `Name:ExactMatch:not`, `Name:ExactMatch`, `Name:not`, `Name`
564
     * @param mixed $value Value of the filter
565
     * @return SearchFilter
566
     */
567
    protected function createSearchFilter($filter, $value)
568
    {
569
        // Field name is always the first component
570
        $fieldArgs = explode(':', $filter);
571
        $fieldName = array_shift($fieldArgs);
572
573
        // Inspect type of second argument to determine context
574
        $secondArg = array_shift($fieldArgs);
575
        $modifiers = $fieldArgs;
576
        if (!$secondArg) {
577
            // Use default filter if none specified. E.g. `->filter(['Name' => $myname])`
578
            $filterServiceName = 'DataListFilter.default';
579
        } else {
580
            // The presence of a second argument is by default ambiguous; We need to query
581
            // Whether this is a valid modifier on the default filter, or a filter itself.
582
            /** @var SearchFilter $defaultFilterInstance */
583
            $defaultFilterInstance = Injector::inst()->get('DataListFilter.default');
584
            if (in_array(strtolower($secondArg), $defaultFilterInstance->getSupportedModifiers())) {
585
                // Treat second (and any subsequent) argument as modifiers, using default filter
586
                $filterServiceName = 'DataListFilter.default';
587
                array_unshift($modifiers, $secondArg);
588
            } else {
589
                // Second argument isn't a valid modifier, so assume is filter identifier
590
                $filterServiceName = "DataListFilter.{$secondArg}";
591
            }
592
        }
593
594
        // Build instance
595
        return Injector::inst()->create($filterServiceName, $fieldName, $value, $modifiers);
596
    }
597
598
    /**
599
     * Return a copy of this list which does not contain any items that match all params
600
     *
601
     * @example $list = $list->exclude('Name', 'bob'); // exclude bob from list
602
     * @example $list = $list->exclude('Name', array('aziz', 'bob'); // exclude aziz and bob from list
603
     * @example $list = $list->exclude(array('Name'=>'bob, 'Age'=>21)); // exclude bob that has Age 21
604
     * @example $list = $list->exclude(array('Name'=>'bob, 'Age'=>array(21, 43))); // exclude bob with Age 21 or 43
605
     * @example $list = $list->exclude(array('Name'=>array('bob','phil'), 'Age'=>array(21, 43)));
606
     *          // bob age 21 or 43, phil age 21 or 43 would be excluded
607
     *
608
     * @todo extract the sql from this method into a SQLGenerator class
609
     *
610
     * @param string|array
611
     * @param string [optional]
612
     *
613
     * @return $this
614
     */
615
    public function exclude()
616
    {
617
        $numberFuncArgs = count(func_get_args());
618
        $whereArguments = [];
619
620
        if ($numberFuncArgs == 1 && is_array(func_get_arg(0))) {
621
            $whereArguments = func_get_arg(0);
622
        } elseif ($numberFuncArgs == 2) {
623
            $whereArguments[func_get_arg(0)] = func_get_arg(1);
624
        } else {
625
            throw new InvalidArgumentException('Incorrect number of arguments passed to exclude()');
626
        }
627
628
        return $this->alterDataQuery(function (DataQuery $query) use ($whereArguments) {
629
            $subquery = $query->disjunctiveGroup();
630
631
            foreach ($whereArguments as $field => $value) {
632
                $filter = $this->createSearchFilter($field, $value);
633
                $filter->exclude($subquery);
634
            }
635
        });
636
    }
637
638
    /**
639
     * Return a copy of this list which does not contain any items with any of these params
640
     *
641
     * @example $list = $list->excludeAny('Name', 'bob'); // exclude bob from list
642
     * @example $list = $list->excludeAny('Name', array('aziz', 'bob'); // exclude aziz and bob from list
643
     * @example $list = $list->excludeAny(array('Name'=>'bob, 'Age'=>21)); // exclude bob or Age 21
644
     * @example $list = $list->excludeAny(array('Name'=>'bob, 'Age'=>array(21, 43))); // exclude bob or Age 21 or 43
645
     * @example $list = $list->excludeAny(array('Name'=>array('bob','phil'), 'Age'=>array(21, 43)));
646
     *          // bob, phil, 21 or 43 would be excluded
647
     *
648
     * @param string|array
649
     * @param string [optional]
650
     *
651
     * @return $this
652
     */
653
    public function excludeAny()
654
    {
655
        $numberFuncArgs = count(func_get_args());
656
        $whereArguments = [];
657
658
        if ($numberFuncArgs == 1 && is_array(func_get_arg(0))) {
659
            $whereArguments = func_get_arg(0);
660
        } elseif ($numberFuncArgs == 2) {
661
            $whereArguments[func_get_arg(0)] = func_get_arg(1);
662
        } else {
663
            throw new InvalidArgumentException('Incorrect number of arguments passed to excludeAny()');
664
        }
665
666
        return $this->alterDataQuery(function (DataQuery $dataQuery) use ($whereArguments) {
667
            foreach ($whereArguments as $field => $value) {
668
                $filter = $this->createSearchFilter($field, $value);
669
                $filter->exclude($dataQuery);
670
            }
671
            return $dataQuery;
672
        });
673
    }
674
675
    /**
676
     * This method returns a copy of this list that does not contain any DataObjects that exists in $list
677
     *
678
     * The $list passed needs to contain the same dataclass as $this
679
     *
680
     * @param DataList $list
681
     * @return static
682
     * @throws InvalidArgumentException
683
     */
684
    public function subtract(DataList $list)
685
    {
686
        if ($this->dataClass() != $list->dataClass()) {
687
            throw new InvalidArgumentException('The list passed must have the same dataclass as this class');
688
        }
689
690
        return $this->alterDataQuery(function (DataQuery $query) use ($list) {
691
            $query->subtract($list->dataQuery());
692
        });
693
    }
694
695
    /**
696
     * Return a new DataList instance with an inner join clause added to this list's query.
697
     *
698
     * @param string $table Table name (unquoted and as escaped SQL)
699
     * @param string $onClause Escaped SQL statement, e.g. '"Table1"."ID" = "Table2"."ID"'
700
     * @param string $alias - if you want this table to be aliased under another name
701
     * @param int $order A numerical index to control the order that joins are added to the query; lower order values
702
     * will cause the query to appear first. The default is 20, and joins created automatically by the
703
     * ORM have a value of 10.
704
     * @param array $parameters Any additional parameters if the join is a parameterised subquery
705
     * @return static
706
     */
707
    public function innerJoin($table, $onClause, $alias = null, $order = 20, $parameters = [])
708
    {
709
        return $this->alterDataQuery(function (DataQuery $query) use ($table, $onClause, $alias, $order, $parameters) {
710
            $query->innerJoin($table, $onClause, $alias, $order, $parameters);
711
        });
712
    }
713
714
    /**
715
     * Return a new DataList instance with a left join clause added to this list's query.
716
     *
717
     * @param string $table Table name (unquoted and as escaped SQL)
718
     * @param string $onClause Escaped SQL statement, e.g. '"Table1"."ID" = "Table2"."ID"'
719
     * @param string $alias - if you want this table to be aliased under another name
720
     * @param int $order A numerical index to control the order that joins are added to the query; lower order values
721
     * will cause the query to appear first. The default is 20, and joins created automatically by the
722
     * ORM have a value of 10.
723
     * @param array $parameters Any additional parameters if the join is a parameterised subquery
724
     * @return static
725
     */
726
    public function leftJoin($table, $onClause, $alias = null, $order = 20, $parameters = [])
727
    {
728
        return $this->alterDataQuery(function (DataQuery $query) use ($table, $onClause, $alias, $order, $parameters) {
729
            $query->leftJoin($table, $onClause, $alias, $order, $parameters);
730
        });
731
    }
732
733
    /**
734
     * Return an array of the actual items that this DataList contains at this stage.
735
     * This is when the query is actually executed.
736
     *
737
     * @return array
738
     */
739
    public function toArray()
740
    {
741
        $query = $this->dataQuery->query();
742
        $rows = $query->execute();
743
        $results = [];
744
745
        foreach ($rows as $row) {
746
            $results[] = $this->createDataObject($row);
747
        }
748
749
        return $results;
750
    }
751
752
    /**
753
     * Return this list as an array and every object it as an sub array as well
754
     *
755
     * @return array
756
     */
757
    public function toNestedArray()
758
    {
759
        $result = [];
760
761
        foreach ($this as $item) {
762
            $result[] = $item->toMap();
763
        }
764
765
        return $result;
766
    }
767
768
    /**
769
     * Walks the list using the specified callback
770
     *
771
     * @param callable $callback
772
     * @return $this
773
     */
774
    public function each($callback)
775
    {
776
        foreach ($this as $row) {
777
            $callback($row);
778
        }
779
780
        return $this;
781
    }
782
783
    /**
784
     * Returns a generator for this DataList
785
     *
786
     * @return \Generator&DataObject[]
787
     */
788
    public function getGenerator()
789
    {
790
        $query = $this->dataQuery->query()->execute();
791
792
        while ($row = $query->record()) {
793
            yield $this->createDataObject($row);
794
        }
795
    }
796
797
    public function debug()
798
    {
799
        $val = "<h2>" . static::class . "</h2><ul>";
800
        foreach ($this->toNestedArray() as $item) {
801
            $val .= "<li style=\"list-style-type: disc; margin-left: 20px\">" . Debug::text($item) . "</li>";
802
        }
803
        $val .= "</ul>";
804
        return $val;
805
    }
806
807
    /**
808
     * Returns a map of this list
809
     *
810
     * @param string $keyField - the 'key' field of the result array
811
     * @param string $titleField - the value field of the result array
812
     * @return Map
813
     */
814
    public function map($keyField = 'ID', $titleField = 'Title')
815
    {
816
        return new Map($this, $keyField, $titleField);
817
    }
818
819
    /**
820
     * Create a DataObject from the given SQL row
821
     *
822
     * @param array $row
823
     * @return DataObject
824
     */
825
    public function createDataObject($row)
826
    {
827
        $class = $this->dataClass;
828
829
        if (empty($row['ClassName'])) {
830
            $row['ClassName'] = $class;
831
        }
832
833
        // Failover from RecordClassName to ClassName
834
        if (empty($row['RecordClassName'])) {
835
            $row['RecordClassName'] = $row['ClassName'];
836
        }
837
838
        // Instantiate the class mentioned in RecordClassName only if it exists, otherwise default to $this->dataClass
839
        if (class_exists($row['RecordClassName'])) {
840
            $class = $row['RecordClassName'];
841
        }
842
843
        $item = Injector::inst()->create($class, $row, false, $this->getQueryParams());
844
845
        return $item;
846
    }
847
848
    /**
849
     * Get query parameters for this list.
850
     * These values will be assigned as query parameters to newly created objects from this list.
851
     *
852
     * @return array
853
     */
854
    public function getQueryParams()
855
    {
856
        return $this->dataQuery()->getQueryParams();
857
    }
858
859
    /**
860
     * Returns an Iterator for this DataList.
861
     * This function allows you to use DataLists in foreach loops
862
     *
863
     * @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...
864
     */
865
    public function getIterator()
866
    {
867
        foreach ($this->getFinalisedQuery() as $row) {
868
            yield $this->createDataObject($row);
869
        }
870
871
        // Re-set the finaliseQuery so that it can be re-executed
872
        $this->finalisedQuery = null;
873
    }
874
875
    /**
876
     * Returns the Query result for this DataList. Repeated calls will return
877
     * a cached result, unless the DataQuery underlying this list has been
878
     * modified
879
     *
880
     * @return SilverStripe\ORM\Connect\Query
881
     * @internal This API may change in minor releases
882
     */
883
    protected function getFinalisedQuery()
884
    {
885
        if (!$this->finalisedQuery) {
886
            $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...
887
        }
888
889
        return $this->finalisedQuery;
890
    }
891
892
    /**
893
     * Return the number of items in this DataList
894
     *
895
     * @return int
896
     */
897
    public function count()
898
    {
899
        if ($this->finalisedQuery) {
900
            return $this->finalisedQuery->numRecords();
901
        }
902
903
        return $this->dataQuery->count();
904
    }
905
906
    /**
907
     * Return the maximum value of the given field in this DataList
908
     *
909
     * @param string $fieldName
910
     * @return mixed
911
     */
912
    public function max($fieldName)
913
    {
914
        return $this->dataQuery->max($fieldName);
915
    }
916
917
    /**
918
     * Return the minimum value of the given field in this DataList
919
     *
920
     * @param string $fieldName
921
     * @return mixed
922
     */
923
    public function min($fieldName)
924
    {
925
        return $this->dataQuery->min($fieldName);
926
    }
927
928
    /**
929
     * Return the average value of the given field in this DataList
930
     *
931
     * @param string $fieldName
932
     * @return mixed
933
     */
934
    public function avg($fieldName)
935
    {
936
        return $this->dataQuery->avg($fieldName);
937
    }
938
939
    /**
940
     * Return the sum of the values of the given field in this DataList
941
     *
942
     * @param string $fieldName
943
     * @return mixed
944
     */
945
    public function sum($fieldName)
946
    {
947
        return $this->dataQuery->sum($fieldName);
948
    }
949
950
951
    /**
952
     * Returns the first item in this DataList
953
     *
954
     * @return DataObject
955
     */
956
    public function first()
957
    {
958
        foreach ($this->dataQuery->firstRow()->execute() as $row) {
959
            return $this->createDataObject($row);
960
        }
961
        return null;
962
    }
963
964
    /**
965
     * Returns the last item in this DataList
966
     *
967
     *  @return DataObject
968
     */
969
    public function last()
970
    {
971
        foreach ($this->dataQuery->lastRow()->execute() as $row) {
972
            return $this->createDataObject($row);
973
        }
974
        return null;
975
    }
976
977
    /**
978
     * Returns true if this DataList has items
979
     *
980
     * @return bool
981
     */
982
    public function exists()
983
    {
984
        return $this->count() > 0;
985
    }
986
987
    /**
988
     * Find the first DataObject of this DataList where the given key = value
989
     *
990
     * @param string $key
991
     * @param string $value
992
     * @return DataObject|null
993
     */
994
    public function find($key, $value)
995
    {
996
        return $this->filter($key, $value)->first();
997
    }
998
999
    /**
1000
     * Restrict the columns to fetch into this DataList
1001
     *
1002
     * @param array $queriedColumns
1003
     * @return static
1004
     */
1005
    public function setQueriedColumns($queriedColumns)
1006
    {
1007
        return $this->alterDataQuery(function (DataQuery $query) use ($queriedColumns) {
1008
            $query->setQueriedColumns($queriedColumns);
1009
        });
1010
    }
1011
1012
    /**
1013
     * Filter this list to only contain the given Primary IDs
1014
     *
1015
     * @param array $ids Array of integers
1016
     * @throws InvalidArgumentException
1017
     * @return $this
1018
     */
1019
    public function byIDs($ids)
1020
    {
1021
        $intIDs = array();
1022
        foreach ($ids as $id) {
1023
            if (!is_numeric($id)) {
1024
                throw new InvalidArgumentException(
1025
                    'Invalid value passed to byIDs() in param array. All params have to be numeric or of type Integer.'
1026
                );
1027
            }
1028
            $intIDs[] = intval($id, 10);
1029
        }
1030
1031
        return $this->filter('ID', $intIDs);
1032
    }
1033
1034
    /**
1035
     * Return the first DataObject with the given ID
1036
     *
1037
     * @param int $id
1038
     * @return DataObject
1039
     * @throws InvalidArgumentException
1040
     */
1041
    public function byID($id)
1042
    {
1043
        if (!is_numeric($id)) {
0 ignored issues
show
introduced by
The condition is_numeric($id) is always true.
Loading history...
1044
            throw new InvalidArgumentException(
1045
                'Incorrect param type for $id passed to byID(). Numeric value is expected.'
1046
            );
1047
        }
1048
        return $this->filter('ID', intval($id, 10))->first();
1049
    }
1050
1051
    /**
1052
     * Returns an array of a single field value for all items in the list.
1053
     *
1054
     * @param string $colName
1055
     * @return array
1056
     */
1057
    public function column($colName = "ID")
1058
    {
1059
        if ($this->finalisedQuery) {
1060
            return $this->finalisedQuery->distinct(false)->column($colName);
1061
        }
1062
1063
        return $this->dataQuery->distinct(false)->column($colName);
1064
    }
1065
1066
    /**
1067
     * Returns a unique array of a single field value for all items in the list.
1068
     *
1069
     * @param string $colName
1070
     * @return array
1071
     */
1072
    public function columnUnique($colName = "ID")
1073
    {
1074
        if ($this->finalisedQuery) {
1075
            return $this->finalisedQuery->distinct(true)->column($colName);
1076
        }
1077
        return $this->dataQuery->distinct(true)->column($colName);
1078
    }
1079
1080
    // Member altering methods
1081
1082
    /**
1083
     * Sets the ComponentSet to be the given ID list.
1084
     * Records will be added and deleted as appropriate.
1085
     *
1086
     * @param array $idList List of IDs.
1087
     */
1088
    public function setByIDList($idList)
1089
    {
1090
        $has = [];
1091
1092
        // Index current data
1093
        foreach ($this->column() as $id) {
1094
            $has[$id] = true;
1095
        }
1096
1097
        // Keep track of items to delete
1098
        $itemsToDelete = $has;
1099
1100
        // add items in the list
1101
        // $id is the database ID of the record
1102
        if ($idList) {
1103
            foreach ($idList as $id) {
1104
                unset($itemsToDelete[$id]);
1105
                if ($id && !isset($has[$id])) {
1106
                    $this->add($id);
1107
                }
1108
            }
1109
        }
1110
1111
        // Remove any items that haven't been mentioned
1112
        $this->removeMany(array_keys($itemsToDelete));
1113
    }
1114
1115
    /**
1116
     * Returns an array with both the keys and values set to the IDs of the records in this list.
1117
     * Does not respect sort order. Use ->column("ID") to get an ID list with the current sort.
1118
     *
1119
     * @return array
1120
     */
1121
    public function getIDList()
1122
    {
1123
        $ids = $this->column("ID");
1124
        return $ids ? array_combine($ids, $ids) : [];
1125
    }
1126
1127
    /**
1128
     * Returns a HasManyList or ManyMany list representing the querying of a relation across all
1129
     * objects in this data list.  For it to work, the relation must be defined on the data class
1130
     * that you used to create this DataList.
1131
     *
1132
     * Example: Get members from all Groups:
1133
     *
1134
     *     DataList::Create(\SilverStripe\Security\Group::class)->relation("Members")
1135
     *
1136
     * @param string $relationName
1137
     * @return HasManyList|ManyManyList
1138
     */
1139
    public function relation($relationName)
1140
    {
1141
        $ids = $this->column('ID');
1142
        $singleton = DataObject::singleton($this->dataClass);
1143
        /** @var HasManyList|ManyManyList $relation */
1144
        $relation = $singleton->$relationName($ids);
1145
        return $relation;
1146
    }
1147
1148
    public function dbObject($fieldName)
1149
    {
1150
        return singleton($this->dataClass)->dbObject($fieldName);
1151
    }
1152
1153
    /**
1154
     * Add a number of items to the component set.
1155
     *
1156
     * @param array $items Items to add, as either DataObjects or IDs.
1157
     * @return $this
1158
     */
1159
    public function addMany($items)
1160
    {
1161
        foreach ($items as $item) {
1162
            $this->add($item);
1163
        }
1164
        return $this;
1165
    }
1166
1167
    /**
1168
     * Remove the items from this list with the given IDs
1169
     *
1170
     * @param array $idList
1171
     * @return $this
1172
     */
1173
    public function removeMany($idList)
1174
    {
1175
        foreach ($idList as $id) {
1176
            $this->removeByID($id);
1177
        }
1178
        return $this;
1179
    }
1180
1181
    /**
1182
     * Remove every element in this DataList matching the given $filter.
1183
     *
1184
     * @param string|array $filter - a sql type where filter
1185
     * @return $this
1186
     */
1187
    public function removeByFilter($filter)
1188
    {
1189
        foreach ($this->where($filter) as $item) {
1190
            $this->remove($item);
1191
        }
1192
        return $this;
1193
    }
1194
1195
    /**
1196
     * Remove every element in this DataList.
1197
     *
1198
     * @return $this
1199
     */
1200
    public function removeAll()
1201
    {
1202
        foreach ($this as $item) {
1203
            $this->remove($item);
1204
        }
1205
        return $this;
1206
    }
1207
1208
    /**
1209
     * This method are overloaded by HasManyList and ManyMany list to perform more sophisticated
1210
     * list manipulation
1211
     *
1212
     * @param mixed $item
1213
     */
1214
    public function add($item)
1215
    {
1216
        // Nothing needs to happen by default
1217
        // TO DO: If a filter is given to this data list then
1218
    }
1219
1220
    /**
1221
     * Return a new item to add to this DataList.
1222
     *
1223
     * @todo This doesn't factor in filters.
1224
     * @param array $initialFields
1225
     * @return DataObject
1226
     */
1227
    public function newObject($initialFields = null)
1228
    {
1229
        $class = $this->dataClass;
1230
        return Injector::inst()->create($class, $initialFields, false);
1231
    }
1232
1233
    /**
1234
     * Remove this item by deleting it
1235
     *
1236
     * @param DataObject $item
1237
     * @todo Allow for amendment of this behaviour - for example, we can remove an item from
1238
     * an "ActiveItems" DataList by chaning the status to inactive.
1239
     */
1240
    public function remove($item)
1241
    {
1242
        // By default, we remove an item from a DataList by deleting it.
1243
        $this->removeByID($item->ID);
1244
    }
1245
1246
    /**
1247
     * Remove an item from this DataList by ID
1248
     *
1249
     * @param int $itemID The primary ID
1250
     */
1251
    public function removeByID($itemID)
1252
    {
1253
        $item = $this->byID($itemID);
1254
1255
        if ($item) {
0 ignored issues
show
introduced by
$item is of type SilverStripe\ORM\DataObject, thus it always evaluated to true.
Loading history...
1256
            $item->delete();
1257
        }
1258
    }
1259
1260
    /**
1261
     * Reverses a list of items.
1262
     *
1263
     * @return static
1264
     */
1265
    public function reverse()
1266
    {
1267
        return $this->alterDataQuery(function (DataQuery $query) {
1268
            $query->reverseSort();
1269
        });
1270
    }
1271
1272
    /**
1273
     * Returns whether an item with $key exists
1274
     *
1275
     * @param mixed $key
1276
     * @return bool
1277
     */
1278
    public function offsetExists($key)
1279
    {
1280
        return ($this->limit(1, $key)->first() != null);
1281
    }
1282
1283
    /**
1284
     * Returns item stored in list with index $key
1285
     *
1286
     * @param mixed $key
1287
     * @return DataObject
1288
     */
1289
    public function offsetGet($key)
1290
    {
1291
        return $this->limit(1, $key)->first();
1292
    }
1293
1294
    /**
1295
     * Set an item with the key in $key
1296
     *
1297
     * @param mixed $key
1298
     * @param mixed $value
1299
     */
1300
    public function offsetSet($key, $value)
1301
    {
1302
        user_error("Can't alter items in a DataList using array-access", E_USER_ERROR);
1303
    }
1304
1305
    /**
1306
     * Unset an item with the key in $key
1307
     *
1308
     * @param mixed $key
1309
     */
1310
    public function offsetUnset($key)
1311
    {
1312
        user_error("Can't alter items in a DataList using array-access", E_USER_ERROR);
1313
    }
1314
}
1315