Completed
Push — master ( 9e3f76...51d53f )
by Hamish
10:45
created

DataList::removeDuplicates()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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