Passed
Push — 4 ( ef6b03...860fa2 )
by Damian
10:26
created

DataList   F

Complexity

Total Complexity 128

Size/Duplication

Total Lines 1195
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 1195
rs 0.6314
wmc 128

65 Methods

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

How to fix   Complexity   

Complex Class

Complex classes like DataList often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use DataList, and based on these observations, apply Extract Interface, too.

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