Completed
Push — master ( fc353d...f40ed0 )
by Ingo
27s
created

DataList::setDataQueryParam()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 8
nc 2
nop 2
dl 0
loc 14
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\ORM;
4
5
use BadMethodCallException;
6
use SearchFilter;
7
use SilverStripe\ORM\Queries\SQLConditionGroup;
8
use ViewableData;
9
use Exception;
10
use InvalidArgumentException;
11
use Injector;
12
use LogicException;
13
use Debug;
14
use ArrayIterator;
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
 * @package framework
37
 * @subpackage orm
38
 */
39
class DataList extends ViewableData implements SS_List, SS_Filterable, SS_Sortable, SS_Limitable {
40
41
	/**
42
	 * The DataObject class name that this data list is querying
43
	 *
44
	 * @var string
45
	 */
46
	protected $dataClass;
47
48
	/**
49
	 * The {@link DataQuery} object responsible for getting this DataList's records
50
	 *
51
	 * @var DataQuery
52
	 */
53
	protected $dataQuery;
54
55
	/**
56
	 * The DataModel from which this DataList comes.
57
	 *
58
	 * @var DataModel
59
	 */
60
	protected $model;
61
62
	/**
63
	 * Create a new DataList.
64
	 * No querying is done on construction, but the initial query schema is set up.
65
	 *
66
	 * @param string $dataClass - The DataObject class to query.
67
	 */
68
	public function __construct($dataClass) {
69
		$this->dataClass = $dataClass;
70
		$this->dataQuery = new DataQuery($this->dataClass);
71
72
		parent::__construct();
73
	}
74
75
	/**
76
	 * Set the DataModel
77
	 *
78
	 * @param DataModel $model
79
	 */
80
	public function setDataModel(DataModel $model) {
81
		$this->model = $model;
82
	}
83
84
	/**
85
	 * Get the dataClass name for this DataList, ie the DataObject ClassName
86
	 *
87
	 * @return string
88
	 */
89
	public function dataClass() {
90
		return $this->dataClass;
91
	}
92
93
	/**
94
	 * When cloning this object, clone the dataQuery object as well
95
	 */
96
	public function __clone() {
97
		$this->dataQuery = clone $this->dataQuery;
98
	}
99
100
	/**
101
	 * Return a copy of the internal {@link DataQuery} object
102
	 *
103
	 * Because the returned value is a copy, modifying it won't affect this list's contents. If
104
	 * you want to alter the data query directly, use the alterDataQuery method
105
	 *
106
	 * @return DataQuery
107
	 */
108
	public function dataQuery() {
109
		return clone $this->dataQuery;
110
	}
111
112
	/**
113
	 * @var bool - Indicates if we are in an alterDataQueryCall already, so alterDataQuery can be re-entrant
114
	 */
115
	protected $inAlterDataQueryCall = false;
116
117
	/**
118
	 * Return a new DataList instance with the underlying {@link DataQuery} object altered
119
	 *
120
	 * If you want to alter the underlying dataQuery for this list, this wrapper method
121
	 * will ensure that you can do so without mutating the existing List object.
122
	 *
123
	 * It clones this list, calls the passed callback function with the dataQuery of the new
124
	 * list as it's first parameter (and the list as it's second), then returns the list
125
	 *
126
	 * Note that this function is re-entrant - it's safe to call this inside a callback passed to
127
	 * alterDataQuery
128
	 *
129
	 * @param callable $callback
130
	 * @return static
131
	 * @throws Exception
132
	 */
133
	public function alterDataQuery($callback) {
134
		if ($this->inAlterDataQueryCall) {
135
			$list = $this;
136
137
			$res = call_user_func($callback, $list->dataQuery, $list);
138
			if ($res) $list->dataQuery = $res;
139
140
			return $list;
141
		}
142
		else {
143
			$list = clone $this;
144
			$list->inAlterDataQueryCall = true;
145
146
			try {
147
				$res = call_user_func($callback, $list->dataQuery, $list);
148
				if ($res) $list->dataQuery = $res;
149
			}
150
			catch (Exception $e) {
151
				$list->inAlterDataQueryCall = false;
152
				throw $e;
153
			}
154
155
			$list->inAlterDataQueryCall = false;
156
			return $list;
157
		}
158
	}
159
160
	/**
161
	 * Return a new DataList instance with the underlying {@link DataQuery} object changed
162
	 *
163
	 * @param DataQuery $dataQuery
164
	 * @return static
165
	 */
166
	public function setDataQuery(DataQuery $dataQuery) {
167
		$clone = clone $this;
168
		$clone->dataQuery = $dataQuery;
169
		return $clone;
170
	}
171
172
	/**
173
	 * Returns a new DataList instance with the specified query parameter assigned
174
	 *
175
	 * @param string|array $keyOrArray Either the single key to set, or an array of key value pairs to set
176
	 * @param mixed $val If $keyOrArray is not an array, this is the value to set
177
	 * @return static
178
	 */
179
	public function setDataQueryParam($keyOrArray, $val = null) {
180
		$clone = clone $this;
181
182
		if(is_array($keyOrArray)) {
183
			foreach($keyOrArray as $key => $val) {
184
				$clone->dataQuery->setQueryParam($key, $val);
185
			}
186
		}
187
		else {
188
			$clone->dataQuery->setQueryParam($keyOrArray, $val);
189
		}
190
191
		return $clone;
192
	}
193
194
	/**
195
	 * Returns the SQL query that will be used to get this DataList's records.  Good for debugging. :-)
196
	 *
197
	 * @param array $parameters Out variable for parameters required for this query
198
	 * @return string The resulting SQL query (may be paramaterised)
199
	 */
200
	public function sql(&$parameters = array()) {
201
		return $this->dataQuery->query()->sql($parameters);
202
	}
203
204
	/**
205
	 * Return a new DataList instance with a WHERE clause added to this list's query.
206
	 *
207
	 * Supports parameterised queries.
208
	 * See SQLSelect::addWhere() for syntax examples, although DataList
209
	 * won't expand multiple method arguments as SQLSelect does.
210
	 *
211
	 * @param string|array|SQLConditionGroup $filter Predicate(s) to set, as escaped SQL statements or
212
	 * paramaterised queries
213
	 * @return static
214
	 */
215
	public function where($filter) {
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
		return $this->alterDataQuery(function(DataQuery $query) use ($filter){
235
			$query->whereAny($filter);
236
		});
237
	}
238
239
240
241
	/**
242
	 * Returns true if this DataList can be sorted by the given field.
243
	 *
244
	 * @param string $fieldName
245
	 * @return boolean
246
	 */
247
	public function canSortBy($fieldName) {
248
		return $this->dataQuery()->query()->canSortBy($fieldName);
249
	}
250
251
	/**
252
	 * Returns true if this DataList can be filtered by the given field.
253
	 *
254
	 * @param string $fieldName (May be a related field in dot notation like Member.FirstName)
255
	 * @return boolean
256
	 */
257
	public function canFilterBy($fieldName) {
258
		$model = singleton($this->dataClass);
259
		$relations = explode(".", $fieldName);
260
		// First validate the relationships
261
		$fieldName = array_pop($relations);
262
		foreach ($relations as $r) {
263
			$relationClass = $model->getRelationClass($r);
264
			if (!$relationClass) return false;
265
			$model = singleton($relationClass);
266
			if (!$model) return false;
267
		}
268
		// Then check field
269
		if ($model->hasDatabaseField($fieldName)){
270
			return true;
271
		}
272
		return false;
273
	}
274
275
	/**
276
	 * Return a new DataList instance with the records returned in this query
277
	 * restricted by a limit clause.
278
	 *
279
	 * @param int $limit
280
	 * @param int $offset
281
	 * @return static
282
	 */
283
	public function limit($limit, $offset = 0) {
284
		return $this->alterDataQuery(function(DataQuery $query) use ($limit, $offset){
285
			$query->limit($limit, $offset);
286
		});
287
	}
288
289
	/**
290
	 * Return a new DataList instance with distinct records or not
291
	 *
292
	 * @param bool $value
293
	 * @return static
294
	 */
295
	public function distinct($value) {
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.
313
	 * @return static
314
	 */
315
	public function sort() {
316
		$count = func_num_args();
317
318
		if($count == 0) {
319
			return $this;
320
		}
321
322
		if($count > 2) {
323
			throw new InvalidArgumentException('This method takes zero, one or two arguments');
324
		}
325
326
		if ($count == 2) {
327
			$col = null;
328
			$dir = null;
329
			list($col, $dir) = func_get_args();
330
331
			// Validate direction
332
			if(!in_array(strtolower($dir),array('desc','asc'))){
333
				user_error('Second argument to sort must be either ASC or DESC');
334
			}
335
336
			$sort = array($col => $dir);
337
		}
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
			}
352
353
			else if(is_array($sort)) {
354
				// 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...
355
				$query->sort(null, null); // wipe the sort
356
357
				foreach($sort as $column => $direction) {
358
					// Convert column expressions to SQL fragment, while still allowing the passing of raw SQL
359
					// fragments.
360
					$list->applyRelation($column, $relationColumn, true);
361
					$query->sort($relationColumn, $direction, false);
362
				}
363
			}
364
		});
365
	}
366
367
	/**
368
	 * Return a copy of this list which only includes items with these charactaristics
369
	 *
370
	 * @see SS_List::filter()
371
	 *
372
	 * @example $list = $list->filter('Name', 'bob'); // only bob in the list
373
	 * @example $list = $list->filter('Name', array('aziz', 'bob'); // aziz and bob in list
374
	 * @example $list = $list->filter(array('Name'=>'bob', 'Age'=>21)); // bob with the age 21
375
	 * @example $list = $list->filter(array('Name'=>'bob', 'Age'=>array(21, 43))); // bob with the Age 21 or 43
376
	 * @example $list = $list->filter(array('Name'=>array('aziz','bob'), 'Age'=>array(21, 43)));
377
	 *          // aziz with the age 21 or 43 and bob with the Age 21 or 43
378
	 *
379
	 * Note: When filtering on nullable columns, null checks will be automatically added.
380
	 * E.g. ->filter('Field:not', 'value) will generate '... OR "Field" IS NULL', and
381
	 * ->filter('Field:not', null) will generate '"Field" IS NOT NULL'
382
	 *
383
	 * @todo extract the sql from $customQuery into a SQLGenerator class
384
	 *
385
	 * @param string|array Escaped SQL statement. If passed as array, all keys and values will be escaped internally
386
	 * @return $this
387
	 */
388
	public function filter() {
389
		// Validate and process arguments
390
		$arguments = func_get_args();
391
		switch(sizeof($arguments)) {
392
			case 1: $filters = $arguments[0]; break;
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
Coding Style introduced by
Terminating statement must be on a line by itself

As per the PSR-2 coding standard, the break (or other terminating) statement must be on a line of its own.

switch ($expr) {
     case "A":
         doSomething();
         break; //wrong
     case "B":
         doSomething();
         break; //right
     case "C:":
         doSomething();
         return true; //right
 }

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
393
			case 2: $filters = array($arguments[0] => $arguments[1]); break;
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
Coding Style introduced by
Terminating statement must be on a line by itself

As per the PSR-2 coding standard, the break (or other terminating) statement must be on a line of its own.

switch ($expr) {
     case "A":
         doSomething();
         break; //wrong
     case "B":
         doSomething();
         break; //right
     case "C:":
         doSomething();
         return true; //right
 }

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
394
			default:
395
				throw new InvalidArgumentException('Incorrect number of arguments passed to filter()');
396
		}
397
398
		return $this->addFilter($filters);
399
	}
400
401
	/**
402
	 * Return a new instance of the list with an added filter
403
	 *
404
	 * @param array $filterArray
405
	 * @return $this
406
	 */
407
	public function addFilter($filterArray) {
408
		$list = $this;
409
410
		foreach($filterArray as $expression => $value) {
411
			$filter = $this->createSearchFilter($expression, $value);
412
			$list = $list->alterDataQuery(array($filter, 'apply'));
413
		}
414
415
		return $list;
416
	}
417
418
	/**
419
	 * Return a copy of this list which contains items matching any of these charactaristics.
420
	 *
421
	 * @example // only bob in the list
422
	 *          $list = $list->filterAny('Name', 'bob');
423
	 *          // SQL: WHERE "Name" = 'bob'
424
	 * @example // azis or bob in the list
425
	 *          $list = $list->filterAny('Name', array('aziz', 'bob');
426
	 *          // SQL: WHERE ("Name" IN ('aziz','bob'))
427
	 * @example // bob or anyone aged 21 in the list
428
	 *          $list = $list->filterAny(array('Name'=>'bob, 'Age'=>21));
429
	 *          // SQL: WHERE ("Name" = 'bob' OR "Age" = '21')
430
	 * @example // bob or anyone aged 21 or 43 in the list
431
	 *          $list = $list->filterAny(array('Name'=>'bob, 'Age'=>array(21, 43)));
432
	 *          // SQL: WHERE ("Name" = 'bob' OR ("Age" IN ('21', '43'))
433
	 * @example // all bobs, phils or anyone aged 21 or 43 in the list
434
	 *          $list = $list->filterAny(array('Name'=>array('bob','phil'), 'Age'=>array(21, 43)));
435
	 *          // SQL: WHERE (("Name" IN ('bob', 'phil')) OR ("Age" IN ('21', '43'))
436
	 *
437
	 * @todo extract the sql from this method into a SQLGenerator class
438
	 *
439
	 * @param string|array See {@link filter()}
440
	 * @return static
441
	 */
442
	public function filterAny() {
443
		$numberFuncArgs = count(func_get_args());
444
		$whereArguments = array();
445
446
		if($numberFuncArgs == 1 && is_array(func_get_arg(0))) {
447
			$whereArguments = func_get_arg(0);
448
		} elseif($numberFuncArgs == 2) {
449
			$whereArguments[func_get_arg(0)] = func_get_arg(1);
450
		} else {
451
			throw new InvalidArgumentException('Incorrect number of arguments passed to exclude()');
452
		}
453
454
		return $this->alterDataQuery(function(DataQuery $query) use ($whereArguments) {
455
			$subquery = $query->disjunctiveGroup();
456
457
			foreach($whereArguments as $field => $value) {
458
				$filter = $this->createSearchFilter($field, $value);
459
				$filter->apply($subquery);
460
			}
461
		});
462
	}
463
464
	/**
465
	 * Note that, in the current implementation, the filtered list will be an ArrayList, but this may change in a
466
	 * future implementation.
467
	 * @see SS_Filterable::filterByCallback()
468
	 *
469
	 * @example $list = $list->filterByCallback(function($item, $list) { return $item->Age == 9; })
470
	 * @param callable $callback
471
	 * @return ArrayList (this may change in future implementations)
472
	 */
473
	public function filterByCallback($callback) {
474
		if(!is_callable($callback)) {
475
			throw new LogicException(sprintf(
476
				"SS_Filterable::filterByCallback() passed callback must be callable, '%s' given",
477
				gettype($callback)
478
			));
479
		}
480
		/** @var ArrayList $output */
481
		$output = ArrayList::create();
482
		foreach($this as $item) {
483
			if(call_user_func($callback, $item, $this)) {
484
				$output->push($item);
485
			}
486
		}
487
		return $output;
488
	}
489
490
	/**
491
	 * Given a field or relation name, apply it safely to this datalist.
492
	 *
493
	 * Unlike getRelationName, this is immutable and will fallback to the quoted field
494
	 * name if not a relation.
495
	 *
496
	 * @param string $field Name of field or relation to apply
497
	 * @param string &$columnName Quoted column name
498
	 * @param bool $linearOnly Set to true to restrict to linear relations only. Set this
499
	 * if this relation will be used for sorting, and should not include duplicate rows.
500
	 * @return $this DataList with this relation applied
501
	 */
502
	public function applyRelation($field, &$columnName = null, $linearOnly = false) {
503
		// If field is invalid, return it without modification
504
		if(!$this->isValidRelationName($field)) {
505
			$columnName = $field;
506
			return $this;
507
		}
508
509
		// Simple fields without relations are mapped directly
510
		if(strpos($field,'.') === false) {
511
			$columnName = '"'.$field.'"';
512
			return $this;
513
		}
514
515
		return $this->alterDataQuery(
516
			function(DataQuery $query) use ($field, &$columnName, $linearOnly) {
517
				$relations = explode('.', $field);
518
				$fieldName = array_pop($relations);
519
520
				// Apply
521
				$relationModelName = $query->applyRelation($relations, $linearOnly);
522
523
				// Find the db field the relation belongs to
524
				$columnName = DataObject::getSchema()->sqlColumnForField($relationModelName, $fieldName);
525
			}
526
		);
527
	}
528
529
	/**
530
	 * Check if the given field specification could be interpreted as an unquoted relation name
531
	 *
532
	 * @param string $field
533
	 * @return bool
534
	 */
535
	protected function isValidRelationName($field) {
536
		return preg_match('/^[A-Z0-9._]+$/i', $field);
537
	}
538
539
	/**
540
	 * Given a filter expression and value construct a {@see SearchFilter} instance
541
	 *
542
	 * @param string $filter E.g. `Name:ExactMatch:not`, `Name:ExactMatch`, `Name:not`, `Name`
543
	 * @param mixed $value Value of the filter
544
	 * @return SearchFilter
545
	 */
546
	protected function createSearchFilter($filter, $value) {
547
		// Field name is always the first component
548
		$fieldArgs = explode(':', $filter);
549
		$fieldName = array_shift($fieldArgs);
550
551
		// Inspect type of second argument to determine context
552
		$secondArg = array_shift($fieldArgs);
553
		$modifiers = $fieldArgs;
554
		if(!$secondArg) {
555
			// Use default filter if none specified. E.g. `->filter(['Name' => $myname])`
556
			$filterServiceName = 'DataListFilter.default';
557
		} else {
558
			// The presence of a second argument is by default ambiguous; We need to query
559
			// Whether this is a valid modifier on the default filter, or a filter itself.
560
			/** @var SearchFilter $defaultFilterInstance */
561
			$defaultFilterInstance = Injector::inst()->get('DataListFilter.default');
562
			if (in_array(strtolower($secondArg), $defaultFilterInstance->getSupportedModifiers())) {
563
				// Treat second (and any subsequent) argument as modifiers, using default filter
564
				$filterServiceName = 'DataListFilter.default';
565
				array_unshift($modifiers, $secondArg);
566
			} else {
567
				// Second argument isn't a valid modifier, so assume is filter identifier
568
				$filterServiceName = "DataListFilter.{$secondArg}";
569
			}
570
		}
571
572
		// Build instance
573
		return Injector::inst()->create($filterServiceName, $fieldName, $value, $modifiers);
574
	}
575
576
	/**
577
	 * Return a copy of this list which does not contain any items with these charactaristics
578
	 *
579
	 * @see SS_List::exclude()
580
	 * @example $list = $list->exclude('Name', 'bob'); // exclude bob from list
581
	 * @example $list = $list->exclude('Name', array('aziz', 'bob'); // exclude aziz and bob from list
582
	 * @example $list = $list->exclude(array('Name'=>'bob, 'Age'=>21)); // exclude bob that has Age 21
583
	 * @example $list = $list->exclude(array('Name'=>'bob, 'Age'=>array(21, 43))); // exclude bob with Age 21 or 43
584
	 * @example $list = $list->exclude(array('Name'=>array('bob','phil'), 'Age'=>array(21, 43)));
585
	 *          // bob age 21 or 43, phil age 21 or 43 would be excluded
586
	 *
587
	 * @todo extract the sql from this method into a SQLGenerator class
588
	 *
589
	 * @param string|array Escaped SQL statement. If passed as array, all keys and values will be escaped internally
590
	 * @return $this
591
	 */
592
	public function exclude() {
593
		$numberFuncArgs = count(func_get_args());
594
		$whereArguments = array();
595
596
		if($numberFuncArgs == 1 && is_array(func_get_arg(0))) {
597
			$whereArguments = func_get_arg(0);
598
		} elseif($numberFuncArgs == 2) {
599
			$whereArguments[func_get_arg(0)] = func_get_arg(1);
600
		} else {
601
			throw new InvalidArgumentException('Incorrect number of arguments passed to exclude()');
602
		}
603
604
		return $this->alterDataQuery(function(DataQuery $query) use ($whereArguments) {
605
			$subquery = $query->disjunctiveGroup();
606
607
			foreach($whereArguments as $field => $value) {
608
				$filter = $this->createSearchFilter($field, $value);
609
				$filter->exclude($subquery);
610
			}
611
		});
612
	}
613
614
	/**
615
	 * This method returns a copy of this list that does not contain any DataObjects that exists in $list
616
	 *
617
	 * The $list passed needs to contain the same dataclass as $this
618
	 *
619
	 * @param DataList $list
620
	 * @return static
621
	 * @throws BadMethodCallException
622
	 */
623
	public function subtract(DataList $list) {
624
		if($this->dataClass() != $list->dataClass()) {
625
			throw new InvalidArgumentException('The list passed must have the same dataclass as this class');
626
		}
627
628
		return $this->alterDataQuery(function(DataQuery $query) use ($list){
629
			$query->subtract($list->dataQuery());
630
		});
631
	}
632
633
	/**
634
	 * Return a new DataList instance with an inner join clause added to this list's query.
635
	 *
636
	 * @param string $table Table name (unquoted and as escaped SQL)
637
	 * @param string $onClause Escaped SQL statement, e.g. '"Table1"."ID" = "Table2"."ID"'
638
	 * @param string $alias - if you want this table to be aliased under another name
639
	 * @param int $order A numerical index to control the order that joins are added to the query; lower order values
640
	 * will cause the query to appear first. The default is 20, and joins created automatically by the
641
	 * ORM have a value of 10.
642
	 * @param array $parameters Any additional parameters if the join is a parameterised subquery
643
	 * @return static
644
	 */
645
	public function innerJoin($table, $onClause, $alias = null, $order = 20, $parameters = array()) {
646
		return $this->alterDataQuery(function(DataQuery $query) use ($table, $onClause, $alias, $order, $parameters){
647
			$query->innerJoin($table, $onClause, $alias, $order, $parameters);
648
		});
649
	}
650
651
	/**
652
	 * Return a new DataList instance with a left join clause added to this list's query.
653
	 *
654
	 * @param string $table Table name (unquoted and as escaped SQL)
655
	 * @param string $onClause Escaped SQL statement, e.g. '"Table1"."ID" = "Table2"."ID"'
656
	 * @param string $alias - if you want this table to be aliased under another name
657
	 * @param int $order A numerical index to control the order that joins are added to the query; lower order values
658
	 * will cause the query to appear first. The default is 20, and joins created automatically by the
659
	 * ORM have a value of 10.
660
	 * @param array $parameters Any additional parameters if the join is a parameterised subquery
661
	 * @return static
662
	 */
663
	public function leftJoin($table, $onClause, $alias = null, $order = 20, $parameters = array()) {
664
		return $this->alterDataQuery(function(DataQuery $query) use ($table, $onClause, $alias, $order, $parameters){
665
			$query->leftJoin($table, $onClause, $alias, $order, $parameters);
666
		});
667
	}
668
669
	/**
670
	 * Return an array of the actual items that this DataList contains at this stage.
671
	 * This is when the query is actually executed.
672
	 *
673
	 * @return array
674
	 */
675
	public function toArray() {
676
		$query = $this->dataQuery->query();
677
		$rows = $query->execute();
678
		$results = array();
679
680
		foreach($rows as $row) {
681
			$results[] = $this->createDataObject($row);
682
		}
683
684
		return $results;
685
	}
686
687
	/**
688
	 * Return this list as an array and every object it as an sub array as well
689
	 *
690
	 * @return array
691
	 */
692
	public function toNestedArray() {
693
		$result = array();
694
695
		foreach($this as $item) {
696
			$result[] = $item->toMap();
697
		}
698
699
		return $result;
700
	}
701
702
	/**
703
	 * Walks the list using the specified callback
704
	 *
705
	 * @param callable $callback
706
	 * @return $this
707
	 */
708
	public function each($callback) {
709
		foreach($this as $row) {
710
			$callback($row);
711
		}
712
713
		return $this;
714
	}
715
716
	public function debug() {
717
		$val = "<h2>" . $this->class . "</h2><ul>";
718
719
		foreach($this->toNestedArray() as $item) {
720
			$val .= "<li style=\"list-style-type: disc; margin-left: 20px\">" . Debug::text($item) . "</li>";
721
		}
722
		$val .= "</ul>";
723
		return $val;
724
	}
725
726
	/**
727
	 * Returns a map of this list
728
	 *
729
	 * @param string $keyField - the 'key' field of the result array
730
	 * @param string $titleField - the value field of the result array
731
	 * @return SS_Map
732
	 */
733
	public function map($keyField = 'ID', $titleField = 'Title') {
734
		return new SS_Map($this, $keyField, $titleField);
735
	}
736
737
	/**
738
	 * Create a DataObject from the given SQL row
739
	 *
740
	 * @param array $row
741
	 * @return DataObject
742
	 */
743
	protected function createDataObject($row) {
744
		$class = $this->dataClass;
745
746
		// Failover from RecordClassName to ClassName
747
		if(empty($row['RecordClassName'])) {
748
			$row['RecordClassName'] = $row['ClassName'];
749
		}
750
751
		// Instantiate the class mentioned in RecordClassName only if it exists, otherwise default to $this->dataClass
752
		if(class_exists($row['RecordClassName'])) {
753
			$class = $row['RecordClassName'];
754
		}
755
756
		$item = Injector::inst()->create($class, $row, false, $this->model, $this->getQueryParams());
757
758
		return $item;
759
	}
760
761
	/**
762
	 * Get query parameters for this list.
763
	 * These values will be assigned as query parameters to newly created objects from this list.
764
	 *
765
	 * @return array
766
	 */
767
	public function getQueryParams() {
768
		return $this->dataQuery()->getQueryParams();
769
	}
770
771
	/**
772
	 * Returns an Iterator for this DataList.
773
	 * This function allows you to use DataLists in foreach loops
774
	 *
775
	 * @return ArrayIterator
776
	 */
777
	public function getIterator() {
778
		return new ArrayIterator($this->toArray());
779
	}
780
781
	/**
782
	 * Return the number of items in this DataList
783
	 *
784
	 * @return int
785
	 */
786
	public function count() {
787
		return $this->dataQuery->count();
788
	}
789
790
	/**
791
	 * Return the maximum value of the given field in this DataList
792
	 *
793
	 * @param string $fieldName
794
	 * @return mixed
795
	 */
796
	public function max($fieldName) {
797
		return $this->dataQuery->max($fieldName);
798
	}
799
800
	/**
801
	 * Return the minimum value of the given field in this DataList
802
	 *
803
	 * @param string $fieldName
804
	 * @return mixed
805
	 */
806
	public function min($fieldName) {
807
		return $this->dataQuery->min($fieldName);
808
	}
809
810
	/**
811
	 * Return the average value of the given field in this DataList
812
	 *
813
	 * @param string $fieldName
814
	 * @return mixed
815
	 */
816
	public function avg($fieldName) {
817
		return $this->dataQuery->avg($fieldName);
818
	}
819
820
	/**
821
	 * Return the sum of the values of the given field in this DataList
822
	 *
823
	 * @param string $fieldName
824
	 * @return mixed
825
	 */
826
	public function sum($fieldName) {
827
		return $this->dataQuery->sum($fieldName);
828
	}
829
830
831
	/**
832
	 * Returns the first item in this DataList
833
	 *
834
	 * @return DataObject
835
	 */
836
	public function first() {
837
		foreach($this->dataQuery->firstRow()->execute() as $row) {
838
			return $this->createDataObject($row);
839
		}
840
		return null;
841
	}
842
843
	/**
844
	 * Returns the last item in this DataList
845
	 *
846
	 *  @return DataObject
847
	 */
848
	public function last() {
849
		foreach($this->dataQuery->lastRow()->execute() as $row) {
850
			return $this->createDataObject($row);
851
		}
852
		return null;
853
	}
854
855
	/**
856
	 * Returns true if this DataList has items
857
	 *
858
	 * @return bool
859
	 */
860
	public function exists() {
861
		return $this->count() > 0;
862
	}
863
864
	/**
865
	 * Find the first DataObject of this DataList where the given key = value
866
	 *
867
	 * @param string $key
868
	 * @param string $value
869
	 * @return DataObject|null
870
	 */
871
	public function find($key, $value) {
872
		return $this->filter($key, $value)->first();
873
	}
874
875
	/**
876
	 * Restrict the columns to fetch into this DataList
877
	 *
878
	 * @param array $queriedColumns
879
	 * @return static
880
	 */
881
	public function setQueriedColumns($queriedColumns) {
882
		return $this->alterDataQuery(function(DataQuery $query) use ($queriedColumns){
883
			$query->setQueriedColumns($queriedColumns);
884
		});
885
	}
886
887
	/**
888
	 * Filter this list to only contain the given Primary IDs
889
	 *
890
	 * @param array $ids Array of integers
891
	 * @return $this
892
	 */
893
	public function byIDs($ids) {
894
		return $this->filter('ID', $ids);
895
	}
896
897
	/**
898
	 * Return the first DataObject with the given ID
899
	 *
900
	 * @param int $id
901
	 * @return DataObject
902
	 */
903
	public function byID($id) {
904
		return $this->filter('ID', $id)->first();
905
	}
906
907
	/**
908
	 * Returns an array of a single field value for all items in the list.
909
	 *
910
	 * @param string $colName
911
	 * @return array
912
	 */
913
	public function column($colName = "ID") {
914
		return $this->dataQuery->column($colName);
915
	}
916
917
	// Member altering methods
918
919
	/**
920
	 * Sets the ComponentSet to be the given ID list.
921
	 * Records will be added and deleted as appropriate.
922
	 *
923
	 * @param array $idList List of IDs.
924
	 */
925
	public function setByIDList($idList) {
926
		$has = array();
927
928
		// Index current data
929
		foreach($this->column() as $id) {
930
			$has[$id] = true;
931
		}
932
933
		// Keep track of items to delete
934
		$itemsToDelete = $has;
935
936
		// add items in the list
937
		// $id is the database ID of the record
938
		if($idList) foreach($idList as $id) {
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...
939
			unset($itemsToDelete[$id]);
940
			if($id && !isset($has[$id])) {
941
				$this->add($id);
942
			}
943
		}
944
945
		// Remove any items that haven't been mentioned
946
		$this->removeMany(array_keys($itemsToDelete));
947
	}
948
949
	/**
950
	 * Returns an array with both the keys and values set to the IDs of the records in this list.
951
	 * Does not respect sort order. Use ->column("ID") to get an ID list with the current sort.
952
	 *
953
	 * @return array
954
	 */
955
	public function getIDList() {
956
		$ids = $this->column("ID");
957
		return $ids ? array_combine($ids, $ids) : array();
958
	}
959
960
	/**
961
	 * Returns a HasManyList or ManyMany list representing the querying of a relation across all
962
	 * objects in this data list.  For it to work, the relation must be defined on the data class
963
	 * that you used to create this DataList.
964
	 *
965
	 * Example: Get members from all Groups:
966
	 *
967
	 *     DataList::Create("Group")->relation("Members")
968
	 *
969
	 * @param string $relationName
970
	 * @return HasManyList|ManyManyList
971
	 */
972
	public function relation($relationName) {
973
		$ids = $this->column('ID');
974
		return singleton($this->dataClass)->$relationName()->forForeignID($ids);
975
	}
976
977
	public function dbObject($fieldName) {
978
		return singleton($this->dataClass)->dbObject($fieldName);
979
	}
980
981
	/**
982
	 * Add a number of items to the component set.
983
	 *
984
	 * @param array $items Items to add, as either DataObjects or IDs.
985
	 * @return $this
986
	 */
987
	public function addMany($items) {
988
		foreach($items as $item) {
989
			$this->add($item);
990
		}
991
		return $this;
992
	}
993
994
	/**
995
	 * Remove the items from this list with the given IDs
996
	 *
997
	 * @param array $idList
998
	 * @return $this
999
	 */
1000
	public function removeMany($idList) {
1001
		foreach($idList as $id) {
1002
			$this->removeByID($id);
1003
		}
1004
		return $this;
1005
	}
1006
1007
	/**
1008
	 * Remove every element in this DataList matching the given $filter.
1009
	 *
1010
	 * @param string $filter - a sql type where filter
1011
	 * @return $this
1012
	 */
1013
	public function removeByFilter($filter) {
1014
		foreach($this->where($filter) as $item) {
1015
			$this->remove($item);
1016
		}
1017
		return $this;
1018
	}
1019
1020
	/**
1021
	 * Remove every element in this DataList.
1022
	 *
1023
	 * @return $this
1024
	 */
1025
	public function removeAll() {
1026
		foreach($this as $item) {
1027
			$this->remove($item);
1028
		}
1029
		return $this;
1030
	}
1031
1032
	/**
1033
	 * This method are overloaded by HasManyList and ManyMany list to perform more sophisticated
1034
	 * list manipulation
1035
	 *
1036
	 * @param mixed $item
1037
	 */
1038
	public function add($item) {
1039
		// Nothing needs to happen by default
1040
		// TO DO: If a filter is given to this data list then
1041
	}
1042
1043
	/**
1044
	 * Return a new item to add to this DataList.
1045
	 *
1046
	 * @todo This doesn't factor in filters.
1047
	 * @param array $initialFields
1048
	 * @return DataObject
1049
	 */
1050
	public function newObject($initialFields = null) {
1051
		$class = $this->dataClass;
1052
		return Injector::inst()->create($class, $initialFields, false, $this->model);
1053
	}
1054
1055
	/**
1056
	 * Remove this item by deleting it
1057
	 *
1058
	 * @param DataObject $item
1059
	 * @todo Allow for amendment of this behaviour - for example, we can remove an item from
1060
	 * an "ActiveItems" DataList by chaning the status to inactive.
1061
	 */
1062
	public function remove($item) {
1063
		// By default, we remove an item from a DataList by deleting it.
1064
		$this->removeByID($item->ID);
1065
	}
1066
1067
	/**
1068
	 * Remove an item from this DataList by ID
1069
	 *
1070
	 * @param int $itemID The primary ID
1071
	 */
1072
	public function removeByID($itemID) {
1073
		$item = $this->byID($itemID);
1074
1075
		if($item) {
1076
			$item->delete();
1077
		}
1078
	}
1079
1080
	/**
1081
	 * Reverses a list of items.
1082
	 *
1083
	 * @return static
1084
	 */
1085
	public function reverse() {
1086
		return $this->alterDataQuery(function(DataQuery $query){
1087
			$query->reverseSort();
1088
		});
1089
	}
1090
1091
	/**
1092
	 * Returns whether an item with $key exists
1093
	 *
1094
	 * @param mixed $key
1095
	 * @return bool
1096
	 */
1097
	public function offsetExists($key) {
1098
		return ($this->limit(1,$key)->first() != null);
1099
	}
1100
1101
	/**
1102
	 * Returns item stored in list with index $key
1103
	 *
1104
	 * @param mixed $key
1105
	 * @return DataObject
1106
	 */
1107
	public function offsetGet($key) {
1108
		return $this->limit(1, $key)->first();
1109
	}
1110
1111
	/**
1112
	 * Set an item with the key in $key
1113
	 *
1114
	 * @param mixed $key
1115
	 * @param mixed $value
1116
	 */
1117
	public function offsetSet($key, $value) {
1118
		user_error("Can't alter items in a DataList using array-access", E_USER_ERROR);
1119
	}
1120
1121
	/**
1122
	 * Unset an item with the key in $key
1123
	 *
1124
	 * @param mixed $key
1125
	 */
1126
	public function offsetUnset($key) {
1127
		user_error("Can't alter items in a DataList using array-access", E_USER_ERROR);
1128
	}
1129
1130
}
1131