Completed
Push — generators ( 74902c...d5c396 )
by Loz
10:24
created

DataList::getTemplateIterator()   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
c 0
b 0
f 0
nc 2
nop 0
dl 0
loc 6
rs 9.4285
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, TemplateIterator
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
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.
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'));
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()}
455
     * @return static
456
     */
457 View Code Duplication
    public function filterAny()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
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 View Code Duplication
    public function filterByCallback($callback)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
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 with these charactaristics
600
     *
601
     * @see SS_List::exclude()
602
     * @example $list = $list->exclude('Name', 'bob'); // exclude bob from list
603
     * @example $list = $list->exclude('Name', array('aziz', 'bob'); // exclude aziz and bob from list
604
     * @example $list = $list->exclude(array('Name'=>'bob, 'Age'=>21)); // exclude bob that has Age 21
605
     * @example $list = $list->exclude(array('Name'=>'bob, 'Age'=>array(21, 43))); // exclude bob with Age 21 or 43
606
     * @example $list = $list->exclude(array('Name'=>array('bob','phil'), 'Age'=>array(21, 43)));
607
     *          // bob age 21 or 43, phil age 21 or 43 would be excluded
608
     *
609
     * @todo extract the sql from this method into a SQLGenerator class
610
     *
611
     * @param string|array Escaped SQL statement. If passed as array, all keys and values will be escaped internally
612
     * @return $this
613
     */
614 View Code Duplication
    public function exclude()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
615
    {
616
        $numberFuncArgs = count(func_get_args());
617
        $whereArguments = array();
618
619
        if ($numberFuncArgs == 1 && is_array(func_get_arg(0))) {
620
            $whereArguments = func_get_arg(0);
621
        } elseif ($numberFuncArgs == 2) {
622
            $whereArguments[func_get_arg(0)] = func_get_arg(1);
623
        } else {
624
            throw new InvalidArgumentException('Incorrect number of arguments passed to exclude()');
625
        }
626
627
        return $this->alterDataQuery(function (DataQuery $query) use ($whereArguments) {
628
            $subquery = $query->disjunctiveGroup();
629
630
            foreach ($whereArguments as $field => $value) {
631
                $filter = $this->createSearchFilter($field, $value);
632
                $filter->exclude($subquery);
633
            }
634
        });
635
    }
636
637
    /**
638
     * This method returns a copy of this list that does not contain any DataObjects that exists in $list
639
     *
640
     * The $list passed needs to contain the same dataclass as $this
641
     *
642
     * @param DataList $list
643
     * @return static
644
     * @throws InvalidArgumentException
645
     */
646
    public function subtract(DataList $list)
647
    {
648
        if ($this->dataClass() != $list->dataClass()) {
649
            throw new InvalidArgumentException('The list passed must have the same dataclass as this class');
650
        }
651
652
        return $this->alterDataQuery(function (DataQuery $query) use ($list) {
653
            $query->subtract($list->dataQuery());
654
        });
655
    }
656
657
    /**
658
     * Return a new DataList instance with an inner join clause added to this list's query.
659
     *
660
     * @param string $table Table name (unquoted and as escaped SQL)
661
     * @param string $onClause Escaped SQL statement, e.g. '"Table1"."ID" = "Table2"."ID"'
662
     * @param string $alias - if you want this table to be aliased under another name
663
     * @param int $order A numerical index to control the order that joins are added to the query; lower order values
664
     * will cause the query to appear first. The default is 20, and joins created automatically by the
665
     * ORM have a value of 10.
666
     * @param array $parameters Any additional parameters if the join is a parameterised subquery
667
     * @return static
668
     */
669
    public function innerJoin($table, $onClause, $alias = null, $order = 20, $parameters = array())
670
    {
671
        return $this->alterDataQuery(function (DataQuery $query) use ($table, $onClause, $alias, $order, $parameters) {
672
            $query->innerJoin($table, $onClause, $alias, $order, $parameters);
673
        });
674
    }
675
676
    /**
677
     * Return a new DataList instance with a left join clause added to this list's query.
678
     *
679
     * @param string $table Table name (unquoted and as escaped SQL)
680
     * @param string $onClause Escaped SQL statement, e.g. '"Table1"."ID" = "Table2"."ID"'
681
     * @param string $alias - if you want this table to be aliased under another name
682
     * @param int $order A numerical index to control the order that joins are added to the query; lower order values
683
     * will cause the query to appear first. The default is 20, and joins created automatically by the
684
     * ORM have a value of 10.
685
     * @param array $parameters Any additional parameters if the join is a parameterised subquery
686
     * @return static
687
     */
688
    public function leftJoin($table, $onClause, $alias = null, $order = 20, $parameters = array())
689
    {
690
        return $this->alterDataQuery(function (DataQuery $query) use ($table, $onClause, $alias, $order, $parameters) {
691
            $query->leftJoin($table, $onClause, $alias, $order, $parameters);
692
        });
693
    }
694
695
    /**
696
     * Return an array of the actual items that this DataList contains at this stage.
697
     * This is when the query is actually executed.
698
     *
699
     * @return array
700
     */
701
    public function toArray()
702
    {
703
        $query = $this->dataQuery->query();
704
        $rows = $query->execute();
705
        $results = array();
706
707
        foreach ($rows as $row) {
708
            $results[] = $this->createDataObject($row);
709
        }
710
711
        return $results;
712
    }
713
714
    /**
715
     * Return this list as an array and every object it as an sub array as well
716
     *
717
     * @return array
718
     */
719
    public function toNestedArray()
720
    {
721
        $result = array();
722
723
        foreach ($this as $item) {
724
            $result[] = $item->toMap();
725
        }
726
727
        return $result;
728
    }
729
730
    /**
731
     * Walks the list using the specified callback
732
     *
733
     * @param callable $callback
734
     * @return $this
735
     */
736
    public function each($callback)
737
    {
738
        foreach ($this as $row) {
739
            $callback($row);
740
        }
741
742
        return $this;
743
    }
744
745 View Code Duplication
    public function debug()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
746
    {
747
        $val = "<h2>" . static::class . "</h2><ul>";
748
        foreach ($this->toNestedArray() as $item) {
749
            $val .= "<li style=\"list-style-type: disc; margin-left: 20px\">" . Debug::text($item) . "</li>";
750
        }
751
        $val .= "</ul>";
752
        return $val;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $val; (string) is incompatible with the return type of the parent method SilverStripe\View\ViewableData::Debug of type SilverStripe\View\ViewableData_Debugger.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
753
    }
754
755
    /**
756
     * Returns a map of this list
757
     *
758
     * @param string $keyField - the 'key' field of the result array
759
     * @param string $titleField - the value field of the result array
760
     * @return Map
761
     */
762
    public function map($keyField = 'ID', $titleField = 'Title')
763
    {
764
        return new Map($this, $keyField, $titleField);
765
    }
766
767
    /**
768
     * Create a DataObject from the given SQL row
769
     *
770
     * @param array $row
771
     * @return DataObject
772
     */
773
    public function createDataObject($row)
774
    {
775
        $class = $this->dataClass;
776
777
        if (empty($row['ClassName'])) {
778
            $row['ClassName'] = $class;
779
        }
780
781
        // Failover from RecordClassName to ClassName
782
        if (empty($row['RecordClassName'])) {
783
            $row['RecordClassName'] = $row['ClassName'];
784
        }
785
786
        // Instantiate the class mentioned in RecordClassName only if it exists, otherwise default to $this->dataClass
787
        if (class_exists($row['RecordClassName'])) {
788
            $class = $row['RecordClassName'];
789
        }
790
791
        $item = Injector::inst()->create($class, $row, false, $this->getQueryParams());
792
793
        return $item;
794
    }
795
796
    /**
797
     * Get query parameters for this list.
798
     * These values will be assigned as query parameters to newly created objects from this list.
799
     *
800
     * @return array
801
     */
802
    public function getQueryParams()
803
    {
804
        return $this->dataQuery()->getQueryParams();
805
    }
806
807
    /**
808
     * Returns an Iterator for this DataList.
809
     * This function allows you to use DataLists in foreach loops
810
     *
811
     * @return Generator
812
     */
813
    public function getIterator()
814
    {
815
        foreach ($this->getFinalisedQuery() as $row) {
816
            yield $this->createDataObject($row);
817
        }
818
    }
819
820
    /**
821
     * @return Generator|DataObject[]
822
     */
823
    public function getTemplateIterator()
824
    {
825
        foreach ($this->getFinalisedQuery() as $row) {
826
            yield $this->createDataObject($row);
827
        }
828
    }
829
830
    /**
831
     * @return int
832
     */
833
    public function getTemplateIteratorCount()
834
    {
835
        return $this->getFinalisedQuery()->numRecords();
836
    }
837
838
    /**
839
     * Returns the Query result for this DataList. Repeated calls will return
840
     * a cached result, unless the DataQuery underlying this list has been
841
     * modified
842
     *
843
     * @return SilverStripe\ORM\Connect\Query
844
     */
845
    protected function getFinalisedQuery()
846
    {
847
        if (!$this->finalisedQuery) {
848
            $this->finalisedQuery = $this->dataQuery->query()->execute();
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->dataQuery->query()->execute() of type object<SilverStripe\ORM\Connect\Query> is incompatible with the declared type object<SilverStripe\ORM\...ripe\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...
849
        }
850
851
        return $this->finalisedQuery;
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->finalisedQuery; of type SilverStripe\ORM\Connect...tripe\ORM\Connect\Query adds the type SilverStripe\ORM\Connect\Query to the return on line 851 which is incompatible with the return type documented by SilverStripe\ORM\DataList::getFinalisedQuery of type SilverStripe\ORM\SilverStripe\ORM\Connect\Query.
Loading history...
852
    }
853
854
    /**
855
     * Return the number of items in this DataList
856
     *
857
     * @return int
858
     */
859
    public function count()
860
    {
861
        if ($this->finalisedQuery) {
862
            return $this->finalisedQuery->numRecords();
863
        }
864
865
        return $this->dataQuery->count();
866
    }
867
868
    /**
869
     * Return the maximum value of the given field in this DataList
870
     *
871
     * @param string $fieldName
872
     * @return mixed
873
     */
874
    public function max($fieldName)
875
    {
876
        return $this->dataQuery->max($fieldName);
877
    }
878
879
    /**
880
     * Return the minimum value of the given field in this DataList
881
     *
882
     * @param string $fieldName
883
     * @return mixed
884
     */
885
    public function min($fieldName)
886
    {
887
        return $this->dataQuery->min($fieldName);
888
    }
889
890
    /**
891
     * Return the average value of the given field in this DataList
892
     *
893
     * @param string $fieldName
894
     * @return mixed
895
     */
896
    public function avg($fieldName)
897
    {
898
        return $this->dataQuery->avg($fieldName);
899
    }
900
901
    /**
902
     * Return the sum of the values of the given field in this DataList
903
     *
904
     * @param string $fieldName
905
     * @return mixed
906
     */
907
    public function sum($fieldName)
908
    {
909
        return $this->dataQuery->sum($fieldName);
910
    }
911
912
913
    /**
914
     * Returns the first item in this DataList
915
     *
916
     * @return DataObject
917
     */
918
    public function first()
919
    {
920
        foreach ($this->dataQuery->firstRow()->execute() as $row) {
921
            return $this->createDataObject($row);
922
        }
923
        return null;
924
    }
925
926
    /**
927
     * Returns the last item in this DataList
928
     *
929
     *  @return DataObject
930
     */
931
    public function last()
932
    {
933
        foreach ($this->dataQuery->lastRow()->execute() as $row) {
934
            return $this->createDataObject($row);
935
        }
936
        return null;
937
    }
938
939
    /**
940
     * Returns true if this DataList has items
941
     *
942
     * @return bool
943
     */
944
    public function exists()
945
    {
946
        return $this->count() > 0;
947
    }
948
949
    /**
950
     * Find the first DataObject of this DataList where the given key = value
951
     *
952
     * @param string $key
953
     * @param string $value
954
     * @return DataObject|null
955
     */
956
    public function find($key, $value)
957
    {
958
        return $this->filter($key, $value)->first();
959
    }
960
961
    /**
962
     * Restrict the columns to fetch into this DataList
963
     *
964
     * @param array $queriedColumns
965
     * @return static
966
     */
967
    public function setQueriedColumns($queriedColumns)
968
    {
969
        return $this->alterDataQuery(function (DataQuery $query) use ($queriedColumns) {
970
            $query->setQueriedColumns($queriedColumns);
971
        });
972
    }
973
974
    /**
975
     * Filter this list to only contain the given Primary IDs
976
     *
977
     * @param array $ids Array of integers
978
     * @return $this
979
     */
980
    public function byIDs($ids)
981
    {
982
        return $this->filter('ID', $ids);
983
    }
984
985
    /**
986
     * Return the first DataObject with the given ID
987
     *
988
     * @param int $id
989
     * @return DataObject
990
     */
991
    public function byID($id)
992
    {
993
        return $this->filter('ID', $id)->first();
994
    }
995
996
    /**
997
     * Returns an array of a single field value for all items in the list.
998
     *
999
     * @param string $colName
1000
     * @return array
1001
     */
1002
    public function column($colName = "ID")
1003
    {
1004
        if ($this->finalisedQuery) {
1005
            return $this->finalisedQuery->column($colName);
1006
        }
1007
1008
        return $this->dataQuery->column($colName);
1009
    }
1010
1011
    // Member altering methods
1012
1013
    /**
1014
     * Sets the ComponentSet to be the given ID list.
1015
     * Records will be added and deleted as appropriate.
1016
     *
1017
     * @param array $idList List of IDs.
1018
     */
1019
    public function setByIDList($idList)
1020
    {
1021
        $has = array();
1022
1023
        // Index current data
1024
        foreach ($this->column() as $id) {
1025
            $has[$id] = true;
1026
        }
1027
1028
        // Keep track of items to delete
1029
        $itemsToDelete = $has;
1030
1031
        // add items in the list
1032
        // $id is the database ID of the record
1033
        if ($idList) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $idList of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1034
            foreach ($idList as $id) {
1035
                unset($itemsToDelete[$id]);
1036
                if ($id && !isset($has[$id])) {
1037
                    $this->add($id);
1038
                }
1039
            }
1040
        }
1041
1042
        // Remove any items that haven't been mentioned
1043
        $this->removeMany(array_keys($itemsToDelete));
1044
    }
1045
1046
    /**
1047
     * Returns an array with both the keys and values set to the IDs of the records in this list.
1048
     * Does not respect sort order. Use ->column("ID") to get an ID list with the current sort.
1049
     *
1050
     * @return array
1051
     */
1052
    public function getIDList()
1053
    {
1054
        $ids = $this->column("ID");
1055
        return $ids ? array_combine($ids, $ids) : array();
1056
    }
1057
1058
    /**
1059
     * Returns a HasManyList or ManyMany list representing the querying of a relation across all
1060
     * objects in this data list.  For it to work, the relation must be defined on the data class
1061
     * that you used to create this DataList.
1062
     *
1063
     * Example: Get members from all Groups:
1064
     *
1065
     *     DataList::Create("Group")->relation("Members")
1066
     *
1067
     * @param string $relationName
1068
     * @return HasManyList|ManyManyList
1069
     */
1070
    public function relation($relationName)
1071
    {
1072
        $ids = $this->column('ID');
1073
        return singleton($this->dataClass)->$relationName()->forForeignID($ids);
1074
    }
1075
1076
    public function dbObject($fieldName)
1077
    {
1078
        return singleton($this->dataClass)->dbObject($fieldName);
1079
    }
1080
1081
    /**
1082
     * Add a number of items to the component set.
1083
     *
1084
     * @param array $items Items to add, as either DataObjects or IDs.
1085
     * @return $this
1086
     */
1087
    public function addMany($items)
1088
    {
1089
        foreach ($items as $item) {
1090
            $this->add($item);
1091
        }
1092
        return $this;
1093
    }
1094
1095
    /**
1096
     * Remove the items from this list with the given IDs
1097
     *
1098
     * @param array $idList
1099
     * @return $this
1100
     */
1101
    public function removeMany($idList)
1102
    {
1103
        foreach ($idList as $id) {
1104
            $this->removeByID($id);
1105
        }
1106
        return $this;
1107
    }
1108
1109
    /**
1110
     * Remove every element in this DataList matching the given $filter.
1111
     *
1112
     * @param string|array $filter - a sql type where filter
1113
     * @return $this
1114
     */
1115
    public function removeByFilter($filter)
1116
    {
1117
        foreach ($this->where($filter) as $item) {
1118
            $this->remove($item);
1119
        }
1120
        return $this;
1121
    }
1122
1123
    /**
1124
     * Remove every element in this DataList.
1125
     *
1126
     * @return $this
1127
     */
1128
    public function removeAll()
1129
    {
1130
        foreach ($this as $item) {
1131
            $this->remove($item);
1132
        }
1133
        return $this;
1134
    }
1135
1136
    /**
1137
     * This method are overloaded by HasManyList and ManyMany list to perform more sophisticated
1138
     * list manipulation
1139
     *
1140
     * @param mixed $item
1141
     */
1142
    public function add($item)
1143
    {
1144
        // Nothing needs to happen by default
1145
        // TO DO: If a filter is given to this data list then
1146
    }
1147
1148
    /**
1149
     * Return a new item to add to this DataList.
1150
     *
1151
     * @todo This doesn't factor in filters.
1152
     * @param array $initialFields
1153
     * @return DataObject
1154
     */
1155
    public function newObject($initialFields = null)
1156
    {
1157
        $class = $this->dataClass;
1158
        return Injector::inst()->create($class, $initialFields, false);
1159
    }
1160
1161
    /**
1162
     * Remove this item by deleting it
1163
     *
1164
     * @param DataObject $item
1165
     * @todo Allow for amendment of this behaviour - for example, we can remove an item from
1166
     * an "ActiveItems" DataList by chaning the status to inactive.
1167
     */
1168
    public function remove($item)
1169
    {
1170
        // By default, we remove an item from a DataList by deleting it.
1171
        $this->removeByID($item->ID);
1172
    }
1173
1174
    /**
1175
     * Remove an item from this DataList by ID
1176
     *
1177
     * @param int $itemID The primary ID
1178
     */
1179
    public function removeByID($itemID)
1180
    {
1181
        $item = $this->byID($itemID);
1182
1183
        if ($item) {
1184
            $item->delete();
1185
        }
1186
    }
1187
1188
    /**
1189
     * Reverses a list of items.
1190
     *
1191
     * @return static
1192
     */
1193
    public function reverse()
1194
    {
1195
        return $this->alterDataQuery(function (DataQuery $query) {
1196
            $query->reverseSort();
1197
        });
1198
    }
1199
1200
    /**
1201
     * Returns whether an item with $key exists
1202
     *
1203
     * @param mixed $key
1204
     * @return bool
1205
     */
1206
    public function offsetExists($key)
1207
    {
1208
        return ($this->limit(1, $key)->first() != null);
1209
    }
1210
1211
    /**
1212
     * Returns item stored in list with index $key
1213
     *
1214
     * @param mixed $key
1215
     * @return DataObject
1216
     */
1217
    public function offsetGet($key)
1218
    {
1219
        return $this->limit(1, $key)->first();
1220
    }
1221
1222
    /**
1223
     * Set an item with the key in $key
1224
     *
1225
     * @param mixed $key
1226
     * @param mixed $value
1227
     */
1228
    public function offsetSet($key, $value)
1229
    {
1230
        user_error("Can't alter items in a DataList using array-access", E_USER_ERROR);
1231
    }
1232
1233
    /**
1234
     * Unset an item with the key in $key
1235
     *
1236
     * @param mixed $key
1237
     */
1238
    public function offsetUnset($key)
1239
    {
1240
        user_error("Can't alter items in a DataList using array-access", E_USER_ERROR);
1241
    }
1242
}
1243