Passed
Push — master ( 288a96...964b09 )
by Daniel
10:30
created

DataList::getFinalisedQuery()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 3
nc 2
nop 0
dl 0
loc 7
rs 9.4285
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
        } else {
139
            $list = clone $this;
140
            $list->inAlterDataQueryCall = true;
141
142
            try {
143
                $res = call_user_func($callback, $list->dataQuery, $list);
144
                if ($res) {
145
                    $list->dataQuery = $res;
146
                }
147
            } catch (Exception $e) {
148
                $list->inAlterDataQueryCall = false;
149
                throw $e;
150
            }
151
152
            $list->inAlterDataQueryCall = false;
153
            return $list;
154
        }
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 => $val) {
183
                $clone->dataQuery->setQueryParam($key, $val);
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 = array())
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), array('desc','asc'))) {
343
                user_error('Second argument to sort must be either ASC or DESC');
344
            }
345
346
            $sort = array($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 (stristr($sort, ' asc') || stristr($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 = array($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(array($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 = array();
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 = array();
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 = array();
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 = array())
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 = array())
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 = array();
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 = array();
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
    public function debug()
784
    {
785
        $val = "<h2>" . static::class . "</h2><ul>";
786
        foreach ($this->toNestedArray() as $item) {
787
            $val .= "<li style=\"list-style-type: disc; margin-left: 20px\">" . Debug::text($item) . "</li>";
788
        }
789
        $val .= "</ul>";
790
        return $val;
791
    }
792
793
    /**
794
     * Returns a map of this list
795
     *
796
     * @param string $keyField - the 'key' field of the result array
797
     * @param string $titleField - the value field of the result array
798
     * @return Map
799
     */
800
    public function map($keyField = 'ID', $titleField = 'Title')
801
    {
802
        return new Map($this, $keyField, $titleField);
803
    }
804
805
    /**
806
     * Create a DataObject from the given SQL row
807
     *
808
     * @param array $row
809
     * @return DataObject
810
     */
811
    public function createDataObject($row)
812
    {
813
        $class = $this->dataClass;
814
815
        if (empty($row['ClassName'])) {
816
            $row['ClassName'] = $class;
817
        }
818
819
        // Failover from RecordClassName to ClassName
820
        if (empty($row['RecordClassName'])) {
821
            $row['RecordClassName'] = $row['ClassName'];
822
        }
823
824
        // Instantiate the class mentioned in RecordClassName only if it exists, otherwise default to $this->dataClass
825
        if (class_exists($row['RecordClassName'])) {
826
            $class = $row['RecordClassName'];
827
        }
828
829
        $item = Injector::inst()->create($class, $row, false, $this->getQueryParams());
830
831
        return $item;
832
    }
833
834
    /**
835
     * Get query parameters for this list.
836
     * These values will be assigned as query parameters to newly created objects from this list.
837
     *
838
     * @return array
839
     */
840
    public function getQueryParams()
841
    {
842
        return $this->dataQuery()->getQueryParams();
843
    }
844
845
    /**
846
     * Returns an Iterator for this DataList.
847
     * This function allows you to use DataLists in foreach loops
848
     *
849
     * @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...
850
     */
851
    public function getIterator()
852
    {
853
        foreach ($this->getFinalisedQuery() as $row) {
854
            yield $this->createDataObject($row);
855
        }
856
857
        // Re-set the finaliseQuery so that it can be re-executed
858
        $this->finalisedQuery = null;
859
    }
860
861
    /**
862
     * Returns the Query result for this DataList. Repeated calls will return
863
     * a cached result, unless the DataQuery underlying this list has been
864
     * modified
865
     *
866
     * @return SilverStripe\ORM\Connect\Query
867
     * @internal This API may change in minor releases
868
     */
869
    protected function getFinalisedQuery()
870
    {
871
        if (!$this->finalisedQuery) {
872
            $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...
873
        }
874
875
        return $this->finalisedQuery;
876
    }
877
878
    /**
879
     * Return the number of items in this DataList
880
     *
881
     * @return int
882
     */
883
    public function count()
884
    {
885
        if ($this->finalisedQuery) {
886
            return $this->finalisedQuery->numRecords();
887
        }
888
889
        return $this->dataQuery->count();
890
    }
891
892
    /**
893
     * Return the maximum value of the given field in this DataList
894
     *
895
     * @param string $fieldName
896
     * @return mixed
897
     */
898
    public function max($fieldName)
899
    {
900
        return $this->dataQuery->max($fieldName);
901
    }
902
903
    /**
904
     * Return the minimum value of the given field in this DataList
905
     *
906
     * @param string $fieldName
907
     * @return mixed
908
     */
909
    public function min($fieldName)
910
    {
911
        return $this->dataQuery->min($fieldName);
912
    }
913
914
    /**
915
     * Return the average value of the given field in this DataList
916
     *
917
     * @param string $fieldName
918
     * @return mixed
919
     */
920
    public function avg($fieldName)
921
    {
922
        return $this->dataQuery->avg($fieldName);
923
    }
924
925
    /**
926
     * Return the sum of the values of the given field in this DataList
927
     *
928
     * @param string $fieldName
929
     * @return mixed
930
     */
931
    public function sum($fieldName)
932
    {
933
        return $this->dataQuery->sum($fieldName);
934
    }
935
936
937
    /**
938
     * Returns the first item in this DataList
939
     *
940
     * @return DataObject
941
     */
942
    public function first()
943
    {
944
        foreach ($this->dataQuery->firstRow()->execute() as $row) {
945
            return $this->createDataObject($row);
946
        }
947
        return null;
948
    }
949
950
    /**
951
     * Returns the last item in this DataList
952
     *
953
     *  @return DataObject
954
     */
955
    public function last()
956
    {
957
        foreach ($this->dataQuery->lastRow()->execute() as $row) {
958
            return $this->createDataObject($row);
959
        }
960
        return null;
961
    }
962
963
    /**
964
     * Returns true if this DataList has items
965
     *
966
     * @return bool
967
     */
968
    public function exists()
969
    {
970
        return $this->count() > 0;
971
    }
972
973
    /**
974
     * Find the first DataObject of this DataList where the given key = value
975
     *
976
     * @param string $key
977
     * @param string $value
978
     * @return DataObject|null
979
     */
980
    public function find($key, $value)
981
    {
982
        return $this->filter($key, $value)->first();
983
    }
984
985
    /**
986
     * Restrict the columns to fetch into this DataList
987
     *
988
     * @param array $queriedColumns
989
     * @return static
990
     */
991
    public function setQueriedColumns($queriedColumns)
992
    {
993
        return $this->alterDataQuery(function (DataQuery $query) use ($queriedColumns) {
994
            $query->setQueriedColumns($queriedColumns);
995
        });
996
    }
997
998
    /**
999
     * Filter this list to only contain the given Primary IDs
1000
     *
1001
     * @param array $ids Array of integers
1002
     * @throws InvalidArgumentException
1003
     * @return $this
1004
     */
1005
    public function byIDs($ids)
1006
    {
1007
        $intIDs = array();
1008
        foreach ($ids as $id) {
1009
            if (!is_numeric($id)) {
1010
                throw new InvalidArgumentException(
1011
                    'Invalid value passed to byIDs() in param array. All params have to be numeric or of type Integer.'
1012
                );
1013
            }
1014
            $intIDs[] = intval($id, 10);
1015
        }
1016
1017
        return $this->filter('ID', $intIDs);
1018
    }
1019
1020
    /**
1021
     * Return the first DataObject with the given ID
1022
     *
1023
     * @param int $id
1024
     * @return DataObject
1025
     * @throws InvalidArgumentException
1026
     */
1027
    public function byID($id)
1028
    {
1029
        if (!is_numeric($id)) {
0 ignored issues
show
introduced by
The condition is_numeric($id) is always true.
Loading history...
1030
            throw new InvalidArgumentException(
1031
                'Incorrect param type for $id passed to byID(). Numeric value is expected.'
1032
            );
1033
        }
1034
        return $this->filter('ID', intval($id, 10))->first();
1035
    }
1036
1037
    /**
1038
     * Returns an array of a single field value for all items in the list.
1039
     *
1040
     * @param string $colName
1041
     * @return array
1042
     */
1043
    public function column($colName = "ID")
1044
    {
1045
        if ($this->finalisedQuery) {
1046
            return $this->finalisedQuery->column($colName);
1047
        }
1048
1049
        return $this->dataQuery->column($colName);
1050
    }
1051
1052
    // Member altering methods
1053
1054
    /**
1055
     * Sets the ComponentSet to be the given ID list.
1056
     * Records will be added and deleted as appropriate.
1057
     *
1058
     * @param array $idList List of IDs.
1059
     */
1060
    public function setByIDList($idList)
1061
    {
1062
        $has = array();
1063
1064
        // Index current data
1065
        foreach ($this->column() as $id) {
1066
            $has[$id] = true;
1067
        }
1068
1069
        // Keep track of items to delete
1070
        $itemsToDelete = $has;
1071
1072
        // add items in the list
1073
        // $id is the database ID of the record
1074
        if ($idList) {
1075
            foreach ($idList as $id) {
1076
                unset($itemsToDelete[$id]);
1077
                if ($id && !isset($has[$id])) {
1078
                    $this->add($id);
1079
                }
1080
            }
1081
        }
1082
1083
        // Remove any items that haven't been mentioned
1084
        $this->removeMany(array_keys($itemsToDelete));
1085
    }
1086
1087
    /**
1088
     * Returns an array with both the keys and values set to the IDs of the records in this list.
1089
     * Does not respect sort order. Use ->column("ID") to get an ID list with the current sort.
1090
     *
1091
     * @return array
1092
     */
1093
    public function getIDList()
1094
    {
1095
        $ids = $this->column("ID");
1096
        return $ids ? array_combine($ids, $ids) : array();
1097
    }
1098
1099
    /**
1100
     * Returns a HasManyList or ManyMany list representing the querying of a relation across all
1101
     * objects in this data list.  For it to work, the relation must be defined on the data class
1102
     * that you used to create this DataList.
1103
     *
1104
     * Example: Get members from all Groups:
1105
     *
1106
     *     DataList::Create("Group")->relation("Members")
1107
     *
1108
     * @param string $relationName
1109
     * @return HasManyList|ManyManyList
1110
     */
1111
    public function relation($relationName)
1112
    {
1113
        $ids = $this->column('ID');
1114
        $singleton = DataObject::singleton($this->dataClass);
1115
        /** @var HasManyList|ManyManyList $relation */
1116
        $relation = $singleton->$relationName($ids);
1117
        return $relation;
1118
    }
1119
1120
    public function dbObject($fieldName)
1121
    {
1122
        return singleton($this->dataClass)->dbObject($fieldName);
1123
    }
1124
1125
    /**
1126
     * Add a number of items to the component set.
1127
     *
1128
     * @param array $items Items to add, as either DataObjects or IDs.
1129
     * @return $this
1130
     */
1131
    public function addMany($items)
1132
    {
1133
        foreach ($items as $item) {
1134
            $this->add($item);
1135
        }
1136
        return $this;
1137
    }
1138
1139
    /**
1140
     * Remove the items from this list with the given IDs
1141
     *
1142
     * @param array $idList
1143
     * @return $this
1144
     */
1145
    public function removeMany($idList)
1146
    {
1147
        foreach ($idList as $id) {
1148
            $this->removeByID($id);
1149
        }
1150
        return $this;
1151
    }
1152
1153
    /**
1154
     * Remove every element in this DataList matching the given $filter.
1155
     *
1156
     * @param string|array $filter - a sql type where filter
1157
     * @return $this
1158
     */
1159
    public function removeByFilter($filter)
1160
    {
1161
        foreach ($this->where($filter) as $item) {
1162
            $this->remove($item);
1163
        }
1164
        return $this;
1165
    }
1166
1167
    /**
1168
     * Remove every element in this DataList.
1169
     *
1170
     * @return $this
1171
     */
1172
    public function removeAll()
1173
    {
1174
        foreach ($this as $item) {
1175
            $this->remove($item);
1176
        }
1177
        return $this;
1178
    }
1179
1180
    /**
1181
     * This method are overloaded by HasManyList and ManyMany list to perform more sophisticated
1182
     * list manipulation
1183
     *
1184
     * @param mixed $item
1185
     * @param array|null $extraFields Any extra fields, if supported by this list
1186
     */
1187
    public function add($item)
1188
    {
1189
        // Nothing needs to happen by default
1190
        // TO DO: If a filter is given to this data list then
1191
    }
1192
1193
    /**
1194
     * Return a new item to add to this DataList.
1195
     *
1196
     * @todo This doesn't factor in filters.
1197
     * @param array $initialFields
1198
     * @return DataObject
1199
     */
1200
    public function newObject($initialFields = null)
1201
    {
1202
        $class = $this->dataClass;
1203
        return Injector::inst()->create($class, $initialFields, false);
1204
    }
1205
1206
    /**
1207
     * Remove this item by deleting it
1208
     *
1209
     * @param DataObject $item
1210
     * @todo Allow for amendment of this behaviour - for example, we can remove an item from
1211
     * an "ActiveItems" DataList by chaning the status to inactive.
1212
     */
1213
    public function remove($item)
1214
    {
1215
        // By default, we remove an item from a DataList by deleting it.
1216
        $this->removeByID($item->ID);
1217
    }
1218
1219
    /**
1220
     * Remove an item from this DataList by ID
1221
     *
1222
     * @param int $itemID The primary ID
1223
     */
1224
    public function removeByID($itemID)
1225
    {
1226
        $item = $this->byID($itemID);
1227
1228
        if ($item) {
1229
            $item->delete();
1230
        }
1231
    }
1232
1233
    /**
1234
     * Reverses a list of items.
1235
     *
1236
     * @return static
1237
     */
1238
    public function reverse()
1239
    {
1240
        return $this->alterDataQuery(function (DataQuery $query) {
1241
            $query->reverseSort();
1242
        });
1243
    }
1244
1245
    /**
1246
     * Returns whether an item with $key exists
1247
     *
1248
     * @param mixed $key
1249
     * @return bool
1250
     */
1251
    public function offsetExists($key)
1252
    {
1253
        return ($this->limit(1, $key)->first() != null);
1254
    }
1255
1256
    /**
1257
     * Returns item stored in list with index $key
1258
     *
1259
     * @param mixed $key
1260
     * @return DataObject
1261
     */
1262
    public function offsetGet($key)
1263
    {
1264
        return $this->limit(1, $key)->first();
1265
    }
1266
1267
    /**
1268
     * Set an item with the key in $key
1269
     *
1270
     * @param mixed $key
1271
     * @param mixed $value
1272
     */
1273
    public function offsetSet($key, $value)
1274
    {
1275
        user_error("Can't alter items in a DataList using array-access", E_USER_ERROR);
1276
    }
1277
1278
    /**
1279
     * Unset an item with the key in $key
1280
     *
1281
     * @param mixed $key
1282
     */
1283
    public function offsetUnset($key)
1284
    {
1285
        user_error("Can't alter items in a DataList using array-access", E_USER_ERROR);
1286
    }
1287
}
1288