Completed
Push — 3.1 ( 65a6f3...bd0716 )
by Damian
18:45 queued 06:39
created

DataList::merge()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 3
rs 10
cc 1
eloc 2
nc 1
nop 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 $callback
115
	 * @return DataList
116
	 */
117
	public function alterDataQuery($callback) {
118
		if ($this->inAlterDataQueryCall) {
119
			$list = $this;
120
121
			$res = call_user_func($callback, $list->dataQuery, $list);
122
			if ($res) $list->dataQuery = $res;
123
124
			return $list;
125
		}
126
		else {
127
			$list = clone $this;
128
			$list->inAlterDataQueryCall = true;
129
130
			try {
131
				$res = call_user_func($callback, $list->dataQuery, $list);
132
				if ($res) $list->dataQuery = $res;
133
			}
134
			catch (Exception $e) {
135
				$list->inAlterDataQueryCall = false;
136
				throw $e;
137
			}
138
139
			$list->inAlterDataQueryCall = false;
140
			return $list;
141
		}
142
	}
143
144
	/**
145
	 * Return a new DataList instance with the underlying {@link DataQuery} object changed
146
	 *
147
	 * @param DataQuery $dataQuery
148
	 * @return DataList
149
	 */
150
	public function setDataQuery(DataQuery $dataQuery) {
151
		$clone = clone $this;
152
		$clone->dataQuery = $dataQuery;
153
		return $clone;
154
	}
155
156
	public function setDataQueryParam($keyOrArray, $val = null) {
157
		$clone = clone $this;
158
159
		if(is_array($keyOrArray)) {
160
			foreach($keyOrArray as $key => $val) {
161
				$clone->dataQuery->setQueryParam($key, $val);
162
			}
163
		}
164
		else {
165
			$clone->dataQuery->setQueryParam($keyOrArray, $val);
166
		}
167
168
		return $clone;
169
	}
170
171
	/**
172
	 * Returns the SQL query that will be used to get this DataList's records.  Good for debugging. :-)
173
	 * 
174
	 * @return SQLQuery
175
	 */
176
	public function sql() {
177
		return $this->dataQuery->query()->sql();
178
	}
179
	
180
	/**
181
	 * Return a new DataList instance with a WHERE clause added to this list's query.
182
	 *
183
	 * @param string $filter Escaped SQL statement
184
	 * @return DataList
185
	 */
186
	public function where($filter) {
187
		return $this->alterDataQuery(function($query) use ($filter){
188
			$query->where($filter);
189
		});
190
	}
191
192
	/**
193
	 * Returns true if this DataList can be sorted by the given field.
194
	 * 
195
	 * @param string $fieldName
196
	 * @return boolean
197
	 */
198
	public function canSortBy($fieldName) {
199
		return $this->dataQuery()->query()->canSortBy($fieldName);
200
	}
201
	
202
	/**
203
	 *
204
	 * @param string $fieldName
205
	 * @return boolean
206
	 */
207
	public function canFilterBy($fieldName) {
208
		if($t = singleton($this->dataClass)->hasDatabaseField($fieldName)){
209
			return true;
210
		}
211
		return false;
212
	}
213
214
	/**
215
	 * Return a new DataList instance with the records returned in this query
216
	 * restricted by a limit clause.
217
	 * 
218
	 * @param int $limit
219
	 * @param int $offset
220
	 */
221
	public function limit($limit, $offset = 0) {
222
		return $this->alterDataQuery(function($query) use ($limit, $offset){
223
			$query->limit($limit, $offset);
224
		});
225
	}
226
227
	/**
228
	 * Return a new DataList instance with distinct records or not
229
	 *
230
	 * @param bool $value
231
	 */
232
	public function distinct($value) {
233
		return $this->alterDataQuery(function($query) use ($value){
234
			$query->distinct($value);
235
		});
236
	}
237
238
	/**
239
	 * Return a new DataList instance as a copy of this data list with the sort
240
	 * order set.
241
	 *
242
	 * @see SS_List::sort()
243
	 * @see SQLQuery::orderby
244
	 * @example $list = $list->sort('Name'); // default ASC sorting
245
	 * @example $list = $list->sort('Name DESC'); // DESC sorting
246
	 * @example $list = $list->sort('Name', 'ASC');
247
	 * @example $list = $list->sort(array('Name'=>'ASC', 'Age'=>'DESC'));
248
	 *
249
	 * @param String|array Escaped SQL statement. If passed as array, all keys and values are assumed to be escaped.
250
	 * @return DataList
251
	 */
252
	public function sort() {
253
		$count = func_num_args();
254
255
		if($count == 0) {
256
			return $this;
257
		}
258
		
259
		if($count > 2) {
260
			throw new InvalidArgumentException('This method takes zero, one or two arguments');
261
		}
262
263
		$sort = $col = $dir = null;
264
265
		if ($count == 2) {
266
			list($col, $dir) = func_get_args();
267
		}
268
		else {
269
			$sort = func_get_arg(0);
270
		}
271
272
		return $this->alterDataQuery(function($query, $list) use ($sort, $col, $dir){
273
274
			if ($col) {
275
				// sort('Name','Desc')
0 ignored issues
show
Unused Code Comprehensibility introduced by
72% 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...
276
				if(!in_array(strtolower($dir),array('desc','asc'))){
277
					user_error('Second argument to sort must be either ASC or DESC');
278
				}
279
280
				$query->sort($col, $dir);
281
			}
282
283
			else if(is_string($sort) && $sort){
284
				// sort('Name ASC')
285
				if(stristr($sort, ' asc') || stristr($sort, ' desc')) {
286
					$query->sort($sort);
287
				} else {
288
					$query->sort($sort, 'ASC');
289
				}
290
			}
291
292
			else if(is_array($sort)) {
293
				// 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...
294
				$query->sort(null, null); // wipe the sort
295
296
				foreach($sort as $col => $dir) {
297
					// Convert column expressions to SQL fragment, while still allowing the passing of raw SQL
298
					// fragments.
299
					try {
300
						$relCol = $list->getRelationName($col);
301
					} catch(InvalidArgumentException $e) {
302
						$relCol = $col;
303
					}
304
					$query->sort($relCol, $dir, false);
305
				}
306
			}
307
		});
308
	}
309
310
	/**
311
	 * Return a copy of this list which only includes items with these charactaristics
312
	 *
313
	 * @see SS_List::filter()
314
	 *
315
	 * @example $list = $list->filter('Name', 'bob'); // only bob in the list
316
	 * @example $list = $list->filter('Name', array('aziz', 'bob'); // aziz and bob in list
317
	 * @example $list = $list->filter(array('Name'=>'bob, 'Age'=>21)); // bob with the age 21
318
	 * @example $list = $list->filter(array('Name'=>'bob, 'Age'=>array(21, 43))); // bob with the Age 21 or 43
319
	 * @example $list = $list->filter(array('Name'=>array('aziz','bob'), 'Age'=>array(21, 43)));
320
	 *          // aziz with the age 21 or 43 and bob with the Age 21 or 43
321
	 *
322
	 * @todo extract the sql from $customQuery into a SQLGenerator class
323
	 *
324
	 * @param string|array Key and Value pairs, the array values are automatically sanitised for the DB query
325
	 * @return DataList
326
	 */
327
	public function filter() {
328
		// Validate and process arguments
329
		$arguments = func_get_args();
330
		switch(sizeof($arguments)) {
331
			case 1: $filters = $arguments[0]; break;
332
			case 2: $filters = array($arguments[0] => $arguments[1]); break;
333
			default:
334
				throw new InvalidArgumentException('Incorrect number of arguments passed to filter()');
335
		}
336
		
337
		return $this->addFilter($filters);
338
	}
339
340
	/**
341
	 * Return a new instance of the list with an added filter
342
	 */
343
	public function addFilter($filterArray) {
344
		$list = $this;
345
346
		foreach($filterArray as $field => $value) {
347
			$fieldArgs = explode(':', $field);
348
			$field = array_shift($fieldArgs);
349
			$filterType = array_shift($fieldArgs);
350
			$modifiers = $fieldArgs;
351
			$list = $list->applyFilterContext($field, $filterType, $modifiers, $value);
352
		}
353
354
		return $list;
355
	}
356
357
	/**
358
	 * Return a copy of this list which contains items matching any of these charactaristics.
359
	 *
360
	 * @example // only bob in the list
361
	 *          $list = $list->filterAny('Name', 'bob'); 
362
	 *          // SQL: WHERE "Name" = 'bob'
363
	 * @example // azis or bob in the list
364
	 *          $list = $list->filterAny('Name', array('aziz', 'bob'); 
365
	 *          // SQL: WHERE ("Name" IN ('aziz','bob'))
366
	 * @example // bob or anyone aged 21 in the list
367
	 *          $list = $list->filterAny(array('Name'=>'bob, 'Age'=>21)); 
368
	 *          // SQL: WHERE ("Name" = 'bob' OR "Age" = '21')
369
	 * @example // bob or anyone aged 21 or 43 in the list
370
	 *          $list = $list->filterAny(array('Name'=>'bob, 'Age'=>array(21, 43))); 
371
	 *          // SQL: WHERE ("Name" = 'bob' OR ("Age" IN ('21', '43'))
372
	 * @example // all bobs, phils or anyone aged 21 or 43 in the list
373
	 *          $list = $list->filterAny(array('Name'=>array('bob','phil'), 'Age'=>array(21, 43)));
374
	 *          // SQL: WHERE (("Name" IN ('bob', 'phil')) OR ("Age" IN ('21', '43'))
375
	 *
376
	 * @todo extract the sql from this method into a SQLGenerator class
377
	 *
378
	 * @param string|array See {@link filter()}
379
	 * @return DataList
380
	 */
381
	public function filterAny() {
382
		$numberFuncArgs = count(func_get_args());
383
		$whereArguments = array();
384
385
		if($numberFuncArgs == 1 && is_array(func_get_arg(0))) {
386
			$whereArguments = func_get_arg(0);
387
		} elseif($numberFuncArgs == 2) {
388
			$whereArguments[func_get_arg(0)] = func_get_arg(1);
389
			} else {
390
			throw new InvalidArgumentException('Incorrect number of arguments passed to exclude()');
391
			}
392
			
393
		return $this->alterDataQuery(function($query, $list) use ($whereArguments) {
394
			$subquery = $query->disjunctiveGroup();
395
396
			foreach($whereArguments as $field => $value) {
397
				$fieldArgs = explode(':',$field);
398
				$field = array_shift($fieldArgs);
399
				$filterType = array_shift($fieldArgs);
400
				$modifiers = $fieldArgs;
401
402
				// This is here since PHP 5.3 can't call protected/private methods in a closure.
403
				$t = singleton($list->dataClass())->dbObject($field);
404
				if($filterType) {
405
					$className = "{$filterType}Filter";
406
			} else {
407
					$className = 'ExactMatchFilter';
408
				}
409
				if(!class_exists($className)){
410
					$className = 'ExactMatchFilter';
411
					array_unshift($modifiers, $filterType);
412
			}
413
				$t = new $className($field, $value, $modifiers);
414
				$t->apply($subquery);
415
		}
416
		});
417
	}
418
419
	/**
420
	 * Note that, in the current implementation, the filtered list will be an ArrayList, but this may change in a
421
	 * future implementation.
422
	 * @see SS_Filterable::filterByCallback()
423
	 *
424
	 * @example $list = $list->filterByCallback(function($item, $list) { return $item->Age == 9; })
425
	 * @param callable $callback
426
	 * @return ArrayList (this may change in future implementations)
427
	 */
428
	public function filterByCallback($callback) {
429
		if(!is_callable($callback)) {
430
			throw new LogicException(sprintf(
431
				"SS_Filterable::filterByCallback() passed callback must be callable, '%s' given",
432
				gettype($callback)
433
			));
434
		}
435
		$output = ArrayList::create();
436
		foreach($this as $item) {
437
			if(call_user_func($callback, $item, $this)) $output->push($item);
438
		}
439
		return $output;
440
	}
441
442
	/**
443
	 * Translates a {@link Object} relation name to a Database name and apply
444
	 * the relation join to the query.  Throws an InvalidArgumentException if
445
	 * the $field doesn't correspond to a relation.
446
	 *
447
	 * @throws InvalidArgumentException
448
	 * @param string $field
449
	 *
450
	 * @return string
451
	 */
452
	public function getRelationName($field) {
453
		if(!preg_match('/^[A-Z0-9._]+$/i', $field)) {
454
			throw new InvalidArgumentException("Bad field expression $field");
455
		}
456
457
		if (!$this->inAlterDataQueryCall) {
458
			Deprecation::notice('3.1',
459
				'getRelationName is mutating, and must be called inside an alterDataQuery block');
460
		}
461
462
		if(strpos($field,'.') === false) {
463
			return '"'.$field.'"';
464
		}
465
466
		$relations = explode('.', $field);
467
		$fieldName = array_pop($relations);
468
		$relationModelName = $this->dataQuery->applyRelation($field);
469
470
		return '"'.$relationModelName.'"."'.$fieldName.'"';
471
	}
472
473
	/**
474
	 * Translates a filter type to a SQL query.
475
	 *
476
	 * @param string $field - the fieldname in the db
477
	 * @param string $filter - example StartsWith, relates to a filtercontext
478
	 * @param array $modifiers - Modifiers to pass to the filter, ie not,nocase
479
	 * @param string $value - the value that the filtercontext will use for matching
480
	 * @todo Deprecated SearchContexts and pull their functionality into the core of the ORM
481
	 */
482
	private function applyFilterContext($field, $filter, $modifiers, $value) {
483
		if($filter) {
484
			$className = "{$filter}Filter";
485
		} else {
486
			$className = 'ExactMatchFilter';
487
		}
488
489
		if(!class_exists($className)) {
490
			$className = 'ExactMatchFilter';
491
492
			array_unshift($modifiers, $filter);
493
		}
494
495
		$t = new $className($field, $value, $modifiers);
496
497
		return $this->alterDataQuery(array($t, 'apply'));
498
	}
499
500
	/**
501
	 * Return a copy of this list which does not contain any items with these charactaristics
502
	 *
503
	 * @see SS_List::exclude()
504
	 * @example $list = $list->exclude('Name', 'bob'); // exclude bob from list
505
	 * @example $list = $list->exclude('Name', array('aziz', 'bob'); // exclude aziz and bob from list
506
	 * @example $list = $list->exclude(array('Name'=>'bob, 'Age'=>21)); // exclude bob that has Age 21
507
	 * @example $list = $list->exclude(array('Name'=>'bob, 'Age'=>array(21, 43))); // exclude bob with Age 21 or 43
508
	 * @example $list = $list->exclude(array('Name'=>array('bob','phil'), 'Age'=>array(21, 43)));
509
	 *          // bob age 21 or 43, phil age 21 or 43 would be excluded
510
	 *
511
	 * @todo extract the sql from this method into a SQLGenerator class
512
	 *
513
	 * @param string|array Escaped SQL statement. If passed as array, all keys and values are assumed to be escaped.
514
	 * @return DataList
515
	 */
516
	public function exclude() {
517
		$numberFuncArgs = count(func_get_args());
518
		$whereArguments = array();
519
520
		if($numberFuncArgs == 1 && is_array(func_get_arg(0))) {
521
			$whereArguments = func_get_arg(0);
522
		} elseif($numberFuncArgs == 2) {
523
			$whereArguments[func_get_arg(0)] = func_get_arg(1);
524
		} else {
525
			throw new InvalidArgumentException('Incorrect number of arguments passed to exclude()');
526
		}
527
528
		return $this->alterDataQuery(function($query, $list) use ($whereArguments) {
529
			$subquery = $query->disjunctiveGroup();
530
531
			foreach($whereArguments as $field => $value) {
532
				$fieldArgs = explode(':', $field);
533
				$field = array_shift($fieldArgs);
534
				$filterType = array_shift($fieldArgs);
535
				$modifiers = $fieldArgs;
536
537
				// This is here since PHP 5.3 can't call protected/private methods in a closure.
538
				$t = singleton($list->dataClass())->dbObject($field);
539
				if($filterType) {
540
					$className = "{$filterType}Filter";
541
			} else {
542
					$className = 'ExactMatchFilter';
543
			}
544
				if(!class_exists($className)){
545
					$className = 'ExactMatchFilter';
546
					array_unshift($modifiers, $filterType);
547
		}
548
				$t = new $className($field, $value, $modifiers);
549
				$t->exclude($subquery);
550
			}
551
		});
552
	}
553
	
554
	/**
555
	 * This method returns a copy of this list that does not contain any DataObjects that exists in $list
556
	 * 
557
	 * The $list passed needs to contain the same dataclass as $this
558
	 *
559
	 * @param SS_List $list
560
	 * @return DataList 
561
	 * @throws BadMethodCallException
562
	 */
563
	public function subtract(SS_List $list) {
564
		if($this->dataclass() != $list->dataclass()) {
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface SS_List as the method dataclass() does only exist in the following implementations of said interface: ArrayList, DataList, FieldList, HasManyList, ManyManyList, Member_GroupSet, RelationList, UnsavedRelationList.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
565
			throw new InvalidArgumentException('The list passed must have the same dataclass as this class');
566
		}
567
568
		return $this->alterDataQuery(function($query) use ($list){
569
			$query->subtract($list->dataQuery());
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface SS_List as the method dataQuery() does only exist in the following implementations of said interface: DataList, HasManyList, ManyManyList, Member_GroupSet, RelationList, UnsavedRelationList.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
570
		});
571
	}
572
	
573
	/**
574
	 * Return a new DataList instance with an inner join clause added to this list's query.
575
	 *
576
	 * @param string $table Table name (unquoted and as escaped SQL)
577
	 * @param string $onClause Escaped SQL statement, e.g. '"Table1"."ID" = "Table2"."ID"'
578
	 * @param string $alias - if you want this table to be aliased under another name
579
	 * @return DataList 
580
	 */
581
	public function innerJoin($table, $onClause, $alias = null) {
582
		return $this->alterDataQuery(function($query) use ($table, $onClause, $alias){
583
			$query->innerJoin($table, $onClause, $alias);
584
		});
585
	}
586
587
	/**
588
	 * Return a new DataList instance with a left join clause added to this list's query.
589
	 *
590
	 * @param string $table Table name (unquoted and as escaped SQL)
591
	 * @param string $onClause Escaped SQL statement, e.g. '"Table1"."ID" = "Table2"."ID"'
592
	 * @param string $alias - if you want this table to be aliased under another name
593
	 * @return DataList 
594
	 */
595
	public function leftJoin($table, $onClause, $alias = null) {
596
		return $this->alterDataQuery(function($query) use ($table, $onClause, $alias){
597
			$query->leftJoin($table, $onClause, $alias);
598
		});
599
	}
600
601
	/**
602
	 * Return an array of the actual items that this DataList contains at this stage.
603
	 * This is when the query is actually executed.
604
	 *
605
	 * @return array
606
	 */
607
	public function toArray() {
608
		$query = $this->dataQuery->query();
609
		$rows = $query->execute();
610
		$results = array();
611
		
612
		foreach($rows as $row) {
613
			$results[] = $this->createDataObject($row);
614
		}
615
		
616
		return $results;
617
	}
618
619
	/**
620
	 * Return this list as an array and every object it as an sub array as well
621
	 *
622
	 * @return type 
623
	 */
624
	public function toNestedArray() {
625
		$result = array();
626
		
627
		foreach($this as $item) {
628
			$result[] = $item->toMap();
629
		}
630
631
		return $result;
632
	}
633
634
	/**
635
	 * Walks the list using the specified callback
636
	 *
637
	 * @param callable $callback
638
	 * @return DataList
639
	 */
640
	public function each($callback) {
641
		foreach($this as $row) {
642
			$callback($row);
643
		}
644
		
645
		return $this;
646
	}
647
648
	public function debug() {
649
		$val = "<h2>" . $this->class . "</h2><ul>";
650
		
651
		foreach($this->toNestedArray() as $item) {
652
			$val .= "<li style=\"list-style-type: disc; margin-left: 20px\">" . Debug::text($item) . "</li>";
653
		}
654
		$val .= "</ul>";
655
		return $val;
656
	}
657
658
	/**
659
	 * Returns a map of this list
660
	 *
661
	 * @param string $keyField - the 'key' field of the result array
662
	 * @param string $titleField - the value field of the result array
663
	 * @return SS_Map 
664
	 */
665
	public function map($keyField = 'ID', $titleField = 'Title') {
666
		return new SS_Map($this, $keyField, $titleField);
667
	}
668
669
	/**
670
	 * Create a DataObject from the given SQL row
671
	 * 
672
	 * @param array $row
673
	 * @return DataObject
674
	 */
675
	protected function createDataObject($row) {
676
		$defaultClass = $this->dataClass;
677
678
		// Failover from RecordClassName to ClassName
679
		if(empty($row['RecordClassName'])) {
680
			$row['RecordClassName'] = $row['ClassName'];
681
		}
682
		
683
		// Instantiate the class mentioned in RecordClassName only if it exists, otherwise default to $this->dataClass
684
		if(class_exists($row['RecordClassName'])) {
685
			$item = Injector::inst()->create($row['RecordClassName'], $row, false, $this->model);
686
		} else {
687
			$item = Injector::inst()->create($defaultClass, $row, false, $this->model);
688
		}
689
690
		//set query params on the DataObject to tell the lazy loading mechanism the context the object creation context
691
		$item->setSourceQueryParams($this->dataQuery()->getQueryParams());
692
693
		return $item;
694
	}
695
	
696
	/**
697
	 * Returns an Iterator for this DataList.
698
	 * This function allows you to use DataLists in foreach loops
699
	 * 
700
	 * @return ArrayIterator
701
	 */
702
	public function getIterator() {
703
		return new ArrayIterator($this->toArray());
704
	}
705
706
	/**
707
	 * Return the number of items in this DataList
708
	 * 
709
	 * @return int
710
	 */
711
	public function count() {
712
		return $this->dataQuery->count();
713
	}
714
	
715
	/**
716
	 * Return the maximum value of the given field in this DataList
717
	 *
718
	 * @param string $fieldName
719
	 * @return mixed
720
	 */
721
	public function max($fieldName) {
722
		return $this->dataQuery->max($fieldName);
723
	}
724
725
	/**
726
	 * Return the minimum value of the given field in this DataList
727
	 *
728
	 * @param string $fieldName
729
	 * @return mixed
730
	 */
731
	public function min($fieldName) {
732
		return $this->dataQuery->min($fieldName);
733
	}
734
	
735
	/**
736
	 * Return the average value of the given field in this DataList
737
	 * 
738
	 * @param string $fieldName
739
	 * @return mixed
740
	 */
741
	public function avg($fieldName) {
742
		return $this->dataQuery->avg($fieldName);
743
	}
744
745
	/**
746
	 * Return the sum of the values of the given field in this DataList
747
	 * 
748
	 * @param string $fieldName
749
	 * @return mixed
750
	 */
751
	public function sum($fieldName) {
752
		return $this->dataQuery->sum($fieldName);
753
	}
754
	
755
	
756
	/**
757
	 * Returns the first item in this DataList
758
	 * 
759
	 * @return DataObject
760
	 */
761
	public function first() {
762
		foreach($this->dataQuery->firstRow()->execute() as $row) {
763
			return $this->createDataObject($row);
764
		}
765
	}
766
767
	/**
768
	 * Returns the last item in this DataList
769
	 *
770
	 *  @return DataObject
771
	 */
772
	public function last() {
773
		foreach($this->dataQuery->lastRow()->execute() as $row) {
774
			return $this->createDataObject($row);
775
		}
776
	}
777
	
778
	/**
779
	 * Returns true if this DataList has items
780
	 * 
781
	 * @return bool
782
	 */
783
	public function exists() {
784
		return $this->count() > 0;
785
	}
786
787
	/**
788
	 * Get a sub-range of this dataobjectset as an array
789
	 *
790
	 * @param int $offset
791
	 * @param int $length
792
	 * @return DataList
793
	 */
794
	public function getRange($offset, $length) {
795
		Deprecation::notice("3.0", 'Use limit($length, $offset) instead.  Note the new argument order.');
796
		return $this->limit($length, $offset);
797
	}
798
	
799
	/**
800
	 * Find the first DataObject of this DataList where the given key = value
801
	 *
802
	 * @param string $key
803
	 * @param string $value
804
	 * @return DataObject|null
805
	 */
806
	public function find($key, $value) {
807
		if($key == 'ID') {
808
			$baseClass = ClassInfo::baseDataClass($this->dataClass);
809
			$SQL_col = sprintf('"%s"."%s"', $baseClass, Convert::raw2sql($key));
810
		} else {
811
			$SQL_col = sprintf('"%s"', Convert::raw2sql($key));
812
		}
813
814
		return $this->where("$SQL_col = '" . Convert::raw2sql($value) . "'")->First();
815
	}
816
	
817
	/**
818
	 * Restrict the columns to fetch into this DataList
819
	 *
820
	 * @param array $queriedColumns
821
	 * @return DataList
822
	 */
823
	public function setQueriedColumns($queriedColumns) {
824
		return $this->alterDataQuery(function($query) use ($queriedColumns){
825
			$query->setQueriedColumns($queriedColumns);
826
		});
827
	}
828
829
	/**
830
	 * Filter this list to only contain the given Primary IDs
831
	 *
832
	 * @param array $ids Array of integers, will be automatically cast/escaped.
833
	 * @return DataList
834
	 */
835
	public function byIDs(array $ids) {
836
		$ids = array_map('intval', $ids); // sanitize
837
		$baseClass = ClassInfo::baseDataClass($this->dataClass);
838
		return $this->where("\"$baseClass\".\"ID\" IN (" . implode(',', $ids) .")");
839
	}
840
841
	/**
842
	 * Return the first DataObject with the given ID
843
	 * 
844
	 * @param int $id
845
	 * @return DataObject
846
	 */
847
	public function byID($id) {
848
		$baseClass = ClassInfo::baseDataClass($this->dataClass);
849
		return $this->where("\"$baseClass\".\"ID\" = " . (int)$id)->First();
850
	}
851
	
852
	/**
853
	 * Returns an array of a single field value for all items in the list.
854
	 *
855
	 * @param string $colName
856
	 * @return array
857
	 */
858
	public function column($colName = "ID") {
859
		return $this->dataQuery->column($colName);
860
	}
861
	
862
	// Member altering methods
863
	
864
	/**
865
	 * Sets the ComponentSet to be the given ID list.
866
	 * Records will be added and deleted as appropriate.
867
	 * 
868
	 * @param array $idList List of IDs.
869
	 */
870
	public function setByIDList($idList) {
871
		$has = array();
872
		
873
		// Index current data
874
		foreach($this->column() as $id) {
875
			$has[$id] = true;
876
		}
877
		
878
		// Keep track of items to delete
879
		$itemsToDelete = $has;
880
		
881
		// add items in the list
882
		// $id is the database ID of the record
883
		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...
884
			unset($itemsToDelete[$id]);
885
			if($id && !isset($has[$id])) {
886
				$this->add($id);
887
			}
888
		}
889
890
		// Remove any items that haven't been mentioned
891
		$this->removeMany(array_keys($itemsToDelete));
892
	}
893
	
894
	/**
895
	 * Returns an array with both the keys and values set to the IDs of the records in this list.
896
	 * 
897
	 */
898
	public function getIDList() {
899
		$ids = $this->column("ID");
900
		return $ids ? array_combine($ids, $ids) : array();
901
	}
902
	
903
	/**
904
	 * Returns a HasManyList or ManyMany list representing the querying of a relation across all
905
	 * objects in this data list.  For it to work, the relation must be defined on the data class
906
	 * that you used to create this DataList.
907
	 * 
908
	 * Example: Get members from all Groups:
909
	 * 
910
	 *     DataList::Create("Group")->relation("Members")
911
	 * 
912
	 * @param string $relationName
913
	 * @return HasManyList|ManyManyList
914
	 */
915
	public function relation($relationName) {
916
		$ids = $this->column('ID');
917
		return singleton($this->dataClass)->$relationName()->forForeignID($ids);
918
	}
919
920
	public function dbObject($fieldName) {
921
		return singleton($this->dataClass)->dbObject($fieldName);
922
	}
923
924
	/**
925
	 * Add a number of items to the component set.
926
	 * 
927
	 * @param array $items Items to add, as either DataObjects or IDs.
928
	 * @return DataList
929
	 */
930
	public function addMany($items) {
931
		foreach($items as $item) {
932
			$this->add($item);
933
		}
934
		return $this;
935
	}
936
937
	/**
938
	 * Remove the items from this list with the given IDs
939
	 * 
940
	 * @param array $idList
941
	 * @return DataList
942
	 */
943
	public function removeMany($idList) {
944
		foreach($idList as $id) {
945
			$this->removeByID($id);
946
		}
947
		return $this;
948
	}
949
950
	/**
951
	 * Remove every element in this DataList matching the given $filter.
952
	 * 
953
	 * @param string $filter - a sql type where filter
954
	 * @return DataList
955
	 */
956
	public function removeByFilter($filter) {
957
		foreach($this->where($filter) as $item) {
958
			$this->remove($item);
959
		}
960
		return $this;
961
	}
962
963
	/**
964
	 * Remove every element in this DataList.
965
	 *
966
	 * @return DataList
967
	 */
968
	public function removeAll() {
969
		foreach($this as $item) {
970
			$this->remove($item);
971
		}
972
		return $this;
973
	}
974
975
	/**
976
	 * This method are overloaded by HasManyList and ManyMany list to perform more sophisticated 
977
	 * list manipulation
978
	 *
979
	 * @param type $item 
980
	 */
981
	public function add($item) {
982
		// Nothing needs to happen by default
983
		// TO DO: If a filter is given to this data list then
984
	}
985
986
	/**
987
	 * Return a new item to add to this DataList.
988
	 * 
989
	 * @todo This doesn't factor in filters.
990
	 */
991
	public function newObject($initialFields = null) {
992
		$class = $this->dataClass;
993
		return Injector::inst()->create($class, $initialFields, false, $this->model);
994
	}
995
	
996
	/**
997
	 * Remove this item by deleting it
998
	 * 
999
	 * @param DataClass $item 
1000
	 * @todo Allow for amendment of this behaviour - for example, we can remove an item from
1001
	 * an "ActiveItems" DataList by chaning the status to inactive.
1002
	 */
1003
	public function remove($item) {
1004
		// By default, we remove an item from a DataList by deleting it.
1005
		$this->removeByID($item->ID);
1006
	}
1007
1008
	/**
1009
	 * Remove an item from this DataList by ID
1010
	 * 
1011
	 * @param int $itemID - The primary ID
1012
	 */
1013
	public function removeByID($itemID) {
1014
		$item = $this->byID($itemID);
1015
1016
		if($item) {
1017
			return $item->delete();
1018
		}
1019
	}
1020
	
1021
	/**
1022
	 * Reverses a list of items.
1023
	 *
1024
	 * @return DataList
1025
	 */
1026
	public function reverse() {
1027
		return $this->alterDataQuery(function($query){
1028
			$query->reverseSort();
1029
		});
1030
	}
1031
	
1032
	/**
1033
	 * This method won't function on DataLists due to the specific query that it represent
1034
	 * 
1035
	 * @param mixed $item
1036
	 */
1037
	public function push($item) {
0 ignored issues
show
Unused Code introduced by
The parameter $item is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1038
		user_error("Can't call DataList::push() because its data comes from a specific query.", E_USER_ERROR);
1039
	}
1040
	
1041
	/**
1042
	 * This method won't function on DataLists due to the specific query that it represent
1043
	 *
1044
	 * @param mixed $item 
1045
	 */
1046
	public function insertFirst($item) {
0 ignored issues
show
Unused Code introduced by
The parameter $item is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1047
		user_error("Can't call DataList::insertFirst() because its data comes from a specific query.", E_USER_ERROR);
1048
	}
1049
	
1050
	/**
1051
	 * This method won't function on DataLists due to the specific query that it represent
1052
	 * 
1053
	 */
1054
	public function shift() {
1055
		user_error("Can't call DataList::shift() because its data comes from a specific query.", E_USER_ERROR);
1056
	}
1057
	
1058
	/**
1059
	 * This method won't function on DataLists due to the specific query that it represent
1060
	 * 
1061
	 */
1062
	public function replace() {
1063
		user_error("Can't call DataList::replace() because its data comes from a specific query.", E_USER_ERROR);
1064
	}
1065
	
1066
	/**
1067
	 * This method won't function on DataLists due to the specific query that it represent
1068
	 *
1069
	 */
1070
	public function merge() {
1071
		user_error("Can't call DataList::merge() because its data comes from a specific query.", E_USER_ERROR);
1072
	}
1073
	
1074
	/**
1075
	 * This method won't function on DataLists due to the specific query that it represent
1076
	 * 
1077
	 */
1078
	public function removeDuplicates() {
1079
		user_error("Can't call DataList::removeDuplicates() because its data comes from a specific query.",
1080
			E_USER_ERROR);
1081
	}
1082
	
1083
	/**
1084
	 * Returns whether an item with $key exists
1085
	 * 
1086
	 * @param mixed $key
1087
	 * @return bool
1088
	 */
1089
	public function offsetExists($key) {
1090
		return ($this->limit(1,$key)->First() != null);
1091
	}
1092
1093
	/**
1094
	 * Returns item stored in list with index $key
1095
	 * 
1096
	 * @param mixed $key
1097
	 * @return DataObject
1098
	 */
1099
	public function offsetGet($key) {
1100
		return $this->limit(1, $key)->First();
1101
	}
1102
	
1103
	/**
1104
	 * Set an item with the key in $key
1105
	 * 
1106
	 * @param mixed $key
1107
	 * @param mixed $value
1108
	 */
1109
	public function offsetSet($key, $value) {
1110
		user_error("Can't alter items in a DataList using array-access", E_USER_ERROR);
1111
	}
1112
1113
	/**
1114
	 * Unset an item with the key in $key
1115
	 * 
1116
	 * @param mixed $key
1117
	 */
1118
	public function offsetUnset($key) {
1119
		user_error("Can't alter items in a DataList using array-access", E_USER_ERROR);
1120
	}
1121
1122
}
1123