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

DataQuery   F

Complexity

Total Complexity 112

Size/Duplication

Total Lines 773
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 11
Metric Value
wmc 112
lcom 2
cbo 11
dl 0
loc 773
rs 3.9999

41 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A __clone() 0 3 1
A dataClass() 0 3 1
A query() 0 3 1
A removeFilterOn() 0 20 4
A setQueriedColumns() 0 3 1
A execute() 0 3 1
A sql() 0 3 1
A count() 0 4 1
A max() 0 3 1
A min() 0 3 1
A avg() 0 3 1
A sum() 0 3 1
A aggregate() 0 3 1
A firstRow() 0 3 1
A lastRow() 0 3 1
A groupby() 0 4 1
A having() 0 4 1
A disjunctiveGroup() 0 3 1
A conjunctiveGroup() 0 3 1
A reverseSort() 0 4 1
A limit() 0 4 1
A distinct() 0 4 1
A selectField() 0 3 1
A setQueryParam() 0 3 1
A getQueryParam() 0 4 2
A getQueryParams() 0 3 1
B initialiseQuery() 0 32 4
F getFinalisedQuery() 0 101 21
C ensureSelectContainsOrderbyColumns() 0 69 11
C selectColumnsFromTable() 0 23 13
A where() 0 6 2
A whereAny() 0 6 2
A sort() 0 9 2
A innerJoin() 0 6 2
A leftJoin() 0 6 2
C applyRelation() 0 74 16
A subtract() 0 10 1
A selectFromTable() 0 9 1
A column() 0 10 1
A expressionForField() 0 15 3

How to fix   Complexity   

Complex Class

Complex classes like DataQuery often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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

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

1
<?php
2
3
/**
4
 * An object representing a query of data from the DataObject's supporting database.
5
 * Acts as a wrapper over {@link SQLQuery} and performs all of the query generation.
6
 * Used extensively by {@link DataList}.
7
 *
8
 * Unlike DataList, modifiers on DataQuery modify the object rather than returning a clone.
9
 * DataList is immutable, DataQuery is mutable.
10
 *
11
 * @subpackage model
12
 * @package framework
13
 */
14
class DataQuery {
15
16
	/**
17
	 * @var string
18
	 */
19
	protected $dataClass;
20
21
	/**
22
	 * @var SQLQuery
23
	 */
24
	protected $query;
25
26
	/**
27
	 * @var array
28
	 */
29
	protected $collidingFields = array();
30
31
	private $queriedColumns = null;
32
33
	/**
34
	 * @var Boolean
35
	 */
36
	private $queryFinalised = false;
37
38
	// TODO: replace subclass_access with this
39
	protected $querySubclasses = true;
40
	// TODO: replace restrictclasses with this
41
	protected $filterByClassName = true;
42
43
	/**
44
	 * Create a new DataQuery.
45
	 *
46
	 * @param String The name of the DataObject class that you wish to query
47
	 */
48
	public function __construct($dataClass) {
49
		$this->dataClass = $dataClass;
50
		$this->initialiseQuery();
51
	}
52
53
	/**
54
	 * Clone this object
55
	 */
56
	public function __clone() {
57
		$this->query = clone $this->query;
58
	}
59
60
	/**
61
	 * Return the {@link DataObject} class that is being queried.
62
	 */
63
	public function dataClass() {
64
		return $this->dataClass;
65
	}
66
67
	/**
68
	 * Return the {@link SQLQuery} object that represents the current query; note that it will
69
	 * be a clone of the object.
70
	 */
71
	public function query() {
72
		return $this->getFinalisedQuery();
73
	}
74
75
76
	/**
77
	 * Remove a filter from the query
78
	 */
79
	public function removeFilterOn($fieldExpression) {
80
		$matched = false;
81
82
		$where = $this->query->getWhere();
83
		foreach($where as $i => $clause) {
84
			if(strpos($clause, $fieldExpression) !== false) {
85
				unset($where[$i]);
86
				$matched = true;
87
			}
88
		}
89
90
		// set the entire where clause back, but clear the original one first
91
		if($matched) {
92
			$this->query->setWhere($where);
93
		} else {
94
			throw new InvalidArgumentException("Couldn't find $fieldExpression in the query filter.");
95
		}
96
97
		return $this;
98
	}
99
100
	/**
101
	 * Set up the simplest initial query
102
	 */
103
	public function initialiseQuery() {
104
		// Get the tables to join to.
105
		// Don't get any subclass tables - let lazy loading do that.
106
		$tableClasses = ClassInfo::ancestry($this->dataClass, true);
107
108
		// Error checking
109
		if(!$tableClasses) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $tableClasses 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...
110
			if(!SS_ClassLoader::instance()->hasManifest()) {
111
				user_error("DataObjects have been requested before the manifest is loaded. Please ensure you are not"
112
					. " querying the database in _config.php.", E_USER_ERROR);
113
			} else {
114
				user_error("DataList::create Can't find data classes (classes linked to tables) for"
115
					. " $this->dataClass. Please ensure you run dev/build after creating a new DataObject.",
116
					E_USER_ERROR);
117
			}
118
		}
119
120
		$baseClass = array_shift($tableClasses);
121
122
		// Build our intial query
123
		$this->query = new SQLQuery(array());
124
		$this->query->setDistinct(true);
125
126
		if($sort = singleton($this->dataClass)->stat('default_sort')) {
127
			$this->sort($sort);
128
		}
129
130
		$this->query->setFrom("\"$baseClass\"");
131
132
		$obj = Injector::inst()->get($baseClass);
133
		$obj->extend('augmentDataQueryCreation', $this->query, $this);
134
	}
135
136
	public function setQueriedColumns($queriedColumns) {
137
		$this->queriedColumns = $queriedColumns;
138
	}
139
140
	/**
141
	 * Ensure that the query is ready to execute.
142
	 *
143
	 * @return SQLQuery
144
	 */
145
	public function getFinalisedQuery($queriedColumns = null) {
146
		if(!$queriedColumns) $queriedColumns = $this->queriedColumns;
147
		if($queriedColumns) {
148
			$queriedColumns = array_merge($queriedColumns, array('Created', 'LastEdited', 'ClassName'));
149
		}
150
151
		$query = clone $this->query;
152
		$ancestorTables = ClassInfo::ancestry($this->dataClass, true);
153
154
		// Generate the list of tables to iterate over and the list of columns required
155
		// by any existing where clauses. This second step is skipped if we're fetching
156
		// the whole dataobject as any required columns will get selected regardless.
157
		if($queriedColumns) {
158
			// Specifying certain columns allows joining of child tables
159
			$tableClasses = ClassInfo::dataClassesFor($this->dataClass);
160
161
			foreach ($query->getWhere() as $where) {
162
				// Check for just the column, in the form '"Column" = ?' and the form '"Table"."Column"' = ?
163
				if (preg_match('/^"([^"]+)"/', $where, $matches) ||
164
					preg_match('/^"([^"]+)"\."[^"]+"/', $where, $matches)) {
165
					if (!in_array($matches[1], $queriedColumns)) $queriedColumns[] = $matches[1];
166
				}
167
			}
168
		} else {
169
			$tableClasses = $ancestorTables;
170
		}
171
172
		$tableNames = array_values($tableClasses);
173
		$baseClass = $tableNames[0];
174
175
		// Iterate over the tables and check what we need to select from them. If any selects are made (or the table is
176
		// required for a select)
177
		foreach($tableClasses as $tableClass) {
178
179
			// Determine explicit columns to select
180
			$selectColumns = null;
181
			if ($queriedColumns) {
182
				// Restrict queried columns to that on the selected table
183
				$tableFields = DataObject::database_fields($tableClass);
184
				$selectColumns = array_intersect($queriedColumns, array_keys($tableFields));
185
			}
186
187
			// If this is a subclass without any explicitly requested columns, omit this from the query
188
			if(!in_array($tableClass, $ancestorTables) && empty($selectColumns)) continue;
189
190
			// Select necessary columns (unless an explicitly empty array)
191
			if($selectColumns !== array()) {
192
				$this->selectColumnsFromTable($query, $tableClass, $selectColumns);
193
			}
194
195
			// Join if not the base table
196
			if($tableClass !== $baseClass) {
197
				$query->addLeftJoin($tableClass, "\"$tableClass\".\"ID\" = \"$baseClass\".\"ID\"", $tableClass, 10);
198
			}
199
		}
200
201
		// Resolve colliding fields
202
		if($this->collidingFields) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->collidingFields 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...
203
			foreach($this->collidingFields as $k => $collisions) {
204
				$caseClauses = array();
205
				foreach($collisions as $collision) {
206
					if(preg_match('/^"([^"]+)"/', $collision, $matches)) {
207
						$collisionBase = $matches[1];
208
						$collisionClasses = ClassInfo::subclassesFor($collisionBase);
209
						$collisionClasses = array_map(array(DB::getConn(), 'prepStringForDB'), $collisionClasses);
210
						$caseClauses[] = "WHEN \"$baseClass\".\"ClassName\" IN ("
211
							. implode(", ", $collisionClasses) . ") THEN $collision";
212
					} else {
213
						user_error("Bad collision item '$collision'", E_USER_WARNING);
214
					}
215
				}
216
				$query->selectField("CASE " . implode( " ", $caseClauses) . " ELSE NULL END", $k);
217
			}
218
		}
219
220
221
		if($this->filterByClassName) {
222
			// If querying the base class, don't bother filtering on class name
223
			if($this->dataClass != $baseClass) {
224
				// Get the ClassName values to filter to
225
				$classNames = ClassInfo::subclassesFor($this->dataClass);
226
				if(!$classNames) user_error("DataList::create() Can't find data sub-classes for '{$this->dataClass}'");
0 ignored issues
show
Bug Best Practice introduced by
The expression $classNames 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...
227
				$classNames = array_map(array(DB::getConn(), 'prepStringForDB'), $classNames);
228
				$query->addWhere("\"$baseClass\".\"ClassName\" IN (" . implode(",", $classNames) . ")");
229
			}
230
		}
231
232
		$query->selectField("\"$baseClass\".\"ID\"", "ID");
233
		$query->selectField("CASE WHEN \"$baseClass\".\"ClassName\" IS NOT NULL THEN \"$baseClass\".\"ClassName\""
234
			. " ELSE ".DB::getConn()->prepStringForDB($baseClass)." END", "RecordClassName");
235
236
		// TODO: Versioned, Translatable, SiteTreeSubsites, etc, could probably be better implemented as subclasses
237
		// of DataQuery
238
239
		$obj = Injector::inst()->get($this->dataClass);
240
		$obj->extend('augmentSQL', $query, $this);
241
242
		$this->ensureSelectContainsOrderbyColumns($query);
243
244
		return $query;
245
	}
246
247
	/**
248
	 * Ensure that if a query has an order by clause, those columns are present in the select.
249
	 *
250
	 * @param SQLQuery $query
251
	 * @return null
252
	 */
253
	protected function ensureSelectContainsOrderbyColumns($query, $originalSelect = array()) {
254
		$tableClasses = ClassInfo::dataClassesFor($this->dataClass);
255
		$baseClass = array_shift($tableClasses);
256
257
		if($orderby = $query->getOrderBy()) {
258
			$newOrderby = array();
259
			$i = 0;
260
			foreach($orderby as $k => $dir) {
261
				$newOrderby[$k] = $dir;
262
263
				// don't touch functions in the ORDER BY or public function calls
264
				// selected as fields
265
				if(strpos($k, '(') !== false) continue;
266
267
				$col = str_replace('"', '', trim($k));
268
				$parts = explode('.', $col);
269
270
				// Pull through SortColumn references from the originalSelect variables
271
				if(preg_match('/_SortColumn/', $col)) {
272
					if(isset($originalSelect[$col])) {
273
						$query->selectField($originalSelect[$col], $col);
274
					}
275
276
					continue;
277
				}
278
279
				if(count($parts) == 1) {
280
					$databaseFields = DataObject::database_fields($baseClass);
281
282
					// database_fields() doesn't return ID, so we need to
283
					// manually add it here
284
					$databaseFields['ID'] = true;
285
286
					if(isset($databaseFields[$parts[0]])) {
287
						$qualCol = "\"$baseClass\".\"{$parts[0]}\"";
288
					} else {
289
						$qualCol = "\"$parts[0]\"";
290
					}
291
292
					// remove original sort
293
					unset($newOrderby[$k]);
294
295
					// add new columns sort
296
					$newOrderby[$qualCol] = $dir;
297
298
					// To-do: Remove this if block once SQLQuery::$select has been refactored to store getSelect()
299
					// format internally; then this check can be part of selectField()
300
					$selects = $query->getSelect();
301
					if(!isset($selects[$col]) && !in_array($qualCol, $selects)) {
302
						$query->selectField($qualCol);
303
					}
304
				} else {
305
					$qualCol = '"' . implode('"."', $parts) . '"';
306
307
					if(!in_array($qualCol, $query->getSelect())) {
308
						unset($newOrderby[$k]);
309
310
						$newOrderby["\"_SortColumn$i\""] = $dir;
311
						$query->selectField($qualCol, "_SortColumn$i");
312
313
						$i++;
314
					}
315
				}
316
317
			}
318
319
			$query->setOrderBy($newOrderby);
320
		}
321
	}
322
323
	/**
324
	 * Execute the query and return the result as {@link Query} object.
325
	 */
326
	public function execute() {
327
		return $this->getFinalisedQuery()->execute();
328
	}
329
330
	/**
331
	 * Return this query's SQL
332
	 */
333
	public function sql() {
334
		return $this->getFinalisedQuery()->sql();
335
	}
336
337
	/**
338
	 * Return the number of records in this query.
339
	 * Note that this will issue a separate SELECT COUNT() query.
340
	 */
341
	public function count() {
342
		$baseClass = ClassInfo::baseDataClass($this->dataClass);
343
		return $this->getFinalisedQuery()->count("DISTINCT \"$baseClass\".\"ID\"");
344
	}
345
346
	/**
347
	 * Return the maximum value of the given field in this DataList
348
	 *
349
	 * @param String $field Unquoted database column name (will be escaped automatically)
350
	 */
351
	public function max($field) {
352
		return $this->aggregate(sprintf('MAX("%s")', Convert::raw2sql($field)));
353
	}
354
355
	/**
356
	 * Return the minimum value of the given field in this DataList
357
	 *
358
	 * @param String $field Unquoted database column name (will be escaped automatically)
359
	 */
360
	public function min($field) {
361
		return $this->aggregate(sprintf('MIN("%s")', Convert::raw2sql($field)));
362
	}
363
364
	/**
365
	 * Return the average value of the given field in this DataList
366
	 *
367
	 * @param String $field Unquoted database column name (will be escaped automatically)
368
	 */
369
	public function avg($field) {
370
		return $this->aggregate(sprintf('AVG("%s")', Convert::raw2sql($field)));
371
	}
372
373
	/**
374
	 * Return the sum of the values of the given field in this DataList
375
	 *
376
	 * @param String $field Unquoted database column name (will be escaped automatically)
377
	 */
378
	public function sum($field) {
379
		return $this->aggregate(sprintf('SUM("%s")', Convert::raw2sql($field)));
380
	}
381
382
	/**
383
	 * Runs a raw aggregate expression.  Please handle escaping yourself
384
	 */
385
	public function aggregate($expression) {
386
		return $this->getFinalisedQuery()->aggregate($expression)->execute()->value();
387
	}
388
389
	/**
390
	 * Return the first row that would be returned by this full DataQuery
391
	 * Note that this will issue a separate SELECT ... LIMIT 1 query.
392
	 */
393
	public function firstRow() {
394
		return $this->getFinalisedQuery()->firstRow();
395
	}
396
397
	/**
398
	 * Return the last row that would be returned by this full DataQuery
399
	 * Note that this will issue a separate SELECT ... LIMIT query.
400
	 */
401
	public function lastRow() {
402
		return $this->getFinalisedQuery()->lastRow();
403
	}
404
405
	/**
406
	 * Update the SELECT clause of the query with the columns from the given table
407
	 */
408
	protected function selectColumnsFromTable(SQLQuery &$query, $tableClass, $columns = null) {
409
		// Add SQL for multi-value fields
410
		$databaseFields = DataObject::database_fields($tableClass);
411
		$compositeFields = DataObject::composite_fields($tableClass, false);
412
		if($databaseFields) foreach($databaseFields as $k => $v) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $databaseFields 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...
413
			if((is_null($columns) || in_array($k, $columns)) && !isset($compositeFields[$k])) {
414
				// Update $collidingFields if necessary
415
				if($expressionForField = $query->expressionForField($k)) {
416
					if(!isset($this->collidingFields[$k])) $this->collidingFields[$k] = array($expressionForField);
417
					$this->collidingFields[$k][] = "\"$tableClass\".\"$k\"";
418
419
				} else {
420
					$query->selectField("\"$tableClass\".\"$k\"", $k);
421
				}
422
			}
423
		}
424
		if($compositeFields) foreach($compositeFields as $k => $v) {
425
			if((is_null($columns) || in_array($k, $columns)) && $v) {
426
				$dbO = Object::create_from_string($v, $k);
427
				$dbO->addToQuery($query);
428
			}
429
		}
430
	}
431
432
	/**
433
	 * Append a GROUP BY clause to this query.
434
	 *
435
	 * @param String $groupby Escaped SQL statement
436
	 */
437
	public function groupby($groupby) {
438
		$this->query->addGroupBy($groupby);
439
		return $this;
440
	}
441
442
	/**
443
	 * Append a HAVING clause to this query.
444
	 *
445
	 * @param String $having Escaped SQL statement
446
	 */
447
	public function having($having) {
448
		$this->query->addHaving($having);
449
		return $this;
450
	}
451
452
	/**
453
	 * Create a disjunctive subgroup.
454
	 *
455
	 * That is a subgroup joined by OR
456
	 *
457
	 * @return DataQuery_SubGroup
458
	 */
459
	public function disjunctiveGroup() {
460
		return new DataQuery_SubGroup($this, 'OR');
461
	}
462
463
	/**
464
	 * Create a conjunctive subgroup
465
	 *
466
	 * That is a subgroup joined by AND
467
	 *
468
	 * @return DataQuery_SubGroup
469
	 */
470
	public function conjunctiveGroup() {
471
		return new DataQuery_SubGroup($this, 'AND');
472
	}
473
474
	/**
475
	 * Append a WHERE clause to this query.
476
	 * There are two different ways of doing this:
477
	 *
478
	 * <code>
479
	 *  // the entire predicate as a single string
480
	 *  $query->where("\"Column\" = 'Value'");
481
	 *
482
	 *  // multiple predicates as an array
483
	 *  $query->where(array("\"Column\" = 'Value'", "\"Column\" != 'Value'"));
484
	 * </code>
485
	 *
486
	 * @param string|array $where Predicate(s) to set, as escaped SQL statements.
0 ignored issues
show
Bug introduced by
There is no parameter named $where. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
487
	 */
488
	public function where($filter) {
489
		if($filter) {
490
			$this->query->addWhere($filter);
491
		}
492
		return $this;
493
	}
494
495
	/**
496
	 * Append a WHERE with OR.
497
	 *
498
	 * @example $dataQuery->whereAny(array("\"Monkey\" = 'Chimp'", "\"Color\" = 'Brown'"));
499
	 * @see where()
500
	 *
501
	 * @param array $filter Escaped SQL statement.
502
	 * @return DataQuery
503
	 */
504
	public function whereAny($filter) {
505
		if($filter) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $filter 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...
506
			$this->query->addWhereAny($filter);
507
		}
508
		return $this;
509
	}
510
511
	/**
512
	 * Set the ORDER BY clause of this query
513
	 *
514
	 * @see SQLQuery::orderby()
515
	 *
516
	 * @param String $sort Column to sort on (escaped SQL statement)
517
	 * @param String $direction Direction ("ASC" or "DESC", escaped SQL statement)
518
	 * @param Boolean $clear Clear existing values
519
	 * @return DataQuery
520
	 */
521
	public function sort($sort = null, $direction = null, $clear = true) {
522
		if($clear) {
523
			$this->query->setOrderBy($sort, $direction);
524
		} else {
525
			$this->query->addOrderBy($sort, $direction);
526
		}
527
528
		return $this;
529
	}
530
531
	/**
532
	 * Reverse order by clause
533
	 *
534
	 * @return DataQuery
535
	 */
536
	public function reverseSort() {
537
		$this->query->reverseOrderBy();
538
		return $this;
539
	}
540
541
	/**
542
	 * Set the limit of this query.
543
	 *
544
	 * @param int $limit
545
	 * @param int $offset
546
	 */
547
	public function limit($limit, $offset = 0) {
548
		$this->query->setLimit($limit, $offset);
549
		return $this;
550
	}
551
552
	/**
553
	 * Set whether this query should be distinct or not.
554
	 *
555
	 * @param bool $value
556
	 * @return DataQuery
557
	 */
558
	public function distinct($value) {
559
		$this->query->setDistinct($value);
560
		return $this;
561
	}
562
563
	/**
564
	 * Add an INNER JOIN clause to this query.
565
	 *
566
	 * @param String $table The unquoted table name to join to.
567
	 * @param String $onClause The filter for the join (escaped SQL statement)
568
	 * @param String $alias An optional alias name (unquoted)
569
	 */
570
	public function innerJoin($table, $onClause, $alias = null) {
571
		if($table) {
572
			$this->query->addInnerJoin($table, $onClause, $alias);
573
		}
574
		return $this;
575
	}
576
577
	/**
578
	 * Add a LEFT JOIN clause to this query.
579
	 *
580
	 * @param String $table The unquoted table to join to.
581
	 * @param String $onClause The filter for the join (escaped SQL statement).
582
	 * @param String $alias An optional alias name (unquoted)
583
	 */
584
	public function leftJoin($table, $onClause, $alias = null) {
585
		if($table) {
586
			$this->query->addLeftJoin($table, $onClause, $alias);
587
		}
588
		return $this;
589
	}
590
591
	/**
592
	 * Traverse the relationship fields, and add the table
593
	 * mappings to the query object state. This has to be called
594
	 * in any overloaded {@link SearchFilter->apply()} methods manually.
595
	 *
596
	 * @param String|array $relation The array/dot-syntax relation to follow
597
	 * @return The model class of the related item
598
	 */
599
	public function applyRelation($relation) {
600
		// NO-OP
601
		if(!$relation) return $this->dataClass;
602
603
		if(is_string($relation)) $relation = explode(".", $relation);
604
605
		$modelClass = $this->dataClass;
606
607
		foreach($relation as $rel) {
608
			$model = singleton($modelClass);
609
			if ($component = $model->has_one($rel)) {
610
				if(!$this->query->isJoinedTo($component)) {
611
					$foreignKey = $rel;
612
					$realModelClass = ClassInfo::table_for_object_field($modelClass, "{$foreignKey}ID");
613
					$this->query->addLeftJoin($component,
614
						"\"$component\".\"ID\" = \"{$realModelClass}\".\"{$foreignKey}ID\"");
615
616
					/**
617
					 * add join clause to the component's ancestry classes so that the search filter could search on
618
					 * its ancestor fields.
619
					 */
620
					$ancestry = ClassInfo::ancestry($component, true);
621
					if(!empty($ancestry)){
622
						$ancestry = array_reverse($ancestry);
623
						foreach($ancestry as $ancestor){
624
							if($ancestor != $component){
625
								$this->query->addInnerJoin($ancestor, "\"$component\".\"ID\" = \"$ancestor\".\"ID\"");
626
							}
627
						}
628
					}
629
				}
630
				$modelClass = $component;
631
632
			} elseif ($component = $model->has_many($rel)) {
633
				if(!$this->query->isJoinedTo($component)) {
634
					$ancestry = $model->getClassAncestry();
635
					$foreignKey = $model->getRemoteJoinField($rel);
636
					$this->query->addLeftJoin($component,
637
						"\"$component\".\"{$foreignKey}\" = \"{$ancestry[0]}\".\"ID\"");
638
					/**
639
					 * add join clause to the component's ancestry classes so that the search filter could search on
640
					 * its ancestor fields.
641
					 */
642
					$ancestry = ClassInfo::ancestry($component, true);
643
					if(!empty($ancestry)){
644
						$ancestry = array_reverse($ancestry);
645
						foreach($ancestry as $ancestor){
646
							if($ancestor != $component){
647
								$this->query->addInnerJoin($ancestor, "\"$component\".\"ID\" = \"$ancestor\".\"ID\"");
648
							}
649
						}
650
					}
651
				}
652
				$modelClass = $component;
653
654
			} elseif ($component = $model->many_many($rel)) {
655
				list($parentClass, $componentClass, $parentField, $componentField, $relationTable) = $component;
656
				$parentBaseClass = ClassInfo::baseDataClass($parentClass);
657
				$componentBaseClass = ClassInfo::baseDataClass($componentClass);
658
				$this->query->addInnerJoin($relationTable,
659
					"\"$relationTable\".\"$parentField\" = \"$parentBaseClass\".\"ID\"");
660
				$this->query->addLeftJoin($componentBaseClass,
661
					"\"$relationTable\".\"$componentField\" = \"$componentBaseClass\".\"ID\"");
662
				if(ClassInfo::hasTable($componentClass)) {
663
					$this->query->addLeftJoin($componentClass,
664
						"\"$relationTable\".\"$componentField\" = \"$componentClass\".\"ID\"");
665
				}
666
				$modelClass = $componentClass;
667
668
			}
669
		}
670
671
		return $modelClass;
672
	}
673
674
	/**
675
	 * Removes the result of query from this query.
676
	 *
677
	 * @param DataQuery $subtractQuery
678
	 * @param string $field
679
	 */
680
	public function subtract(DataQuery $subtractQuery, $field='ID') {
681
		$fieldExpression = $subtractQuery->expressionForField($field);
682
		$subSelect = $subtractQuery->getFinalisedQuery();
683
		$subSelect->setSelect(array());
684
		$subSelect->selectField($fieldExpression, $field);
685
		$subSelect->setOrderBy(null);
686
		$this->where($this->expressionForField($field).' NOT IN ('.$subSelect->sql().')');
687
688
		return $this;
689
	}
690
691
	/**
692
	 * Select the given fields from the given table.
693
	 *
694
	 * @param String $table Unquoted table name (will be escaped automatically)
695
	 * @param Array $fields Database column names (will be escaped automatically)
696
	 */
697
	public function selectFromTable($table, $fields) {
698
		$table = Convert::raw2sql($table);
699
		$fieldExpressions = array_map(create_function('$item',
0 ignored issues
show
Security Best Practice introduced by
The use of create_function is highly discouraged, better use a closure.

create_function can pose a great security vulnerability as it is similar to eval, and could be used for arbitrary code execution. We highly recommend to use a closure instead.

// Instead of
$function = create_function('$a, $b', 'return $a + $b');

// Better use
$function = function($a, $b) { return $a + $b; }
Loading history...
700
			"return '\"$table\".\"' . Convert::raw2sql(\$item) . '\"';"), $fields);
701
702
		$this->query->setSelect($fieldExpressions);
703
704
		return $this;
705
	}
706
707
	/**
708
	 * Query the given field column from the database and return as an array.
709
	 *
710
	 * @param string $field See {@link expressionForField()}.
711
	 * @return array List of column values for the specified column
712
	 */
713
	public function column($field = 'ID') {
714
		$fieldExpression = $this->expressionForField($field);
715
		$query = $this->getFinalisedQuery(array($field));
716
		$originalSelect = $query->getSelect();
717
		$query->setSelect(array());
718
		$query->selectField($fieldExpression, $field);
719
		$this->ensureSelectContainsOrderbyColumns($query, $originalSelect);
720
721
		return $query->execute()->column($field);
722
	}
723
724
	/**
725
	 * @param  String $field Select statement identifier, either the unquoted column name,
726
	 * the full composite SQL statement, or the alias set through {@link SQLQuery->selectField()}.
727
	 * @return String The expression used to query this field via this DataQuery
728
	 */
729
	protected function expressionForField($field) {
730
731
		// Prepare query object for selecting this field
732
		$query = $this->getFinalisedQuery(array($field));
733
734
		// Allow query to define the expression for this field
735
		$expression = $query->expressionForField($field);
736
		if(!empty($expression)) return $expression;
737
738
		// Special case for ID, if not provided
739
		if($field === 'ID') {
740
			$baseClass = ClassInfo::baseDataClass($this->dataClass);
741
			return "\"$baseClass\".\"ID\"";
742
		}
743
	}
744
745
	/**
746
	 * Select the given field expressions.
747
	 *
748
	 * @param $fieldExpression String The field to select (escaped SQL statement)
749
	 * @param $alias String The alias of that field (escaped SQL statement)
750
	 */
751
	protected function selectField($fieldExpression, $alias = null) {
752
		$this->query->selectField($fieldExpression, $alias);
753
	}
754
755
	//// QUERY PARAMS
756
757
	/**
758
	 * An arbitrary store of query parameters that can be used by decorators.
759
	 * @todo This will probably be made obsolete if we have subclasses of DataList and/or DataQuery.
760
	 */
761
	private $queryParams;
762
763
	/**
764
	 * Set an arbitrary query parameter, that can be used by decorators to add additional meta-data to the query.
765
	 * It's expected that the $key will be namespaced, e.g, 'Versioned.stage' instead of just 'stage'.
766
	 */
767
	public function setQueryParam($key, $value) {
768
		$this->queryParams[$key] = $value;
769
	}
770
771
	/**
772
	 * Set an arbitrary query parameter, that can be used by decorators to add additional meta-data to the query.
773
	 */
774
	public function getQueryParam($key) {
775
		if(isset($this->queryParams[$key])) return $this->queryParams[$key];
776
		else return null;
777
	}
778
779
	/**
780
	 * Returns all query parameters
781
	 * @return array query parameters array
782
	 */
783
	public function getQueryParams() {
784
		return $this->queryParams;
785
	}
786
}
787
788
/**
789
 * Represents a subgroup inside a WHERE clause in a {@link DataQuery}
790
 *
791
 * Stores the clauses for the subgroup inside a specific {@link SQLQuery}
792
 * object.
793
 *
794
 * All non-where methods call their DataQuery versions, which uses the base
795
 * query object.
796
 *
797
 * @package framework
798
 */
799
class DataQuery_SubGroup extends DataQuery {
800
801
	protected $whereQuery;
802
803
	public function __construct(DataQuery $base, $connective) {
804
		$this->dataClass = $base->dataClass;
805
		$this->query = $base->query;
806
		$this->whereQuery = new SQLQuery;
807
		$this->whereQuery->setConnective($connective);
808
809
		$base->where($this);
810
	}
811
812
	/**
813
	 * Set the WHERE clause of this query.
814
	 * There are two different ways of doing this:
815
	 *
816
	 * <code>
817
	 *  // the entire predicate as a single string
818
	 *  $query->where("\"Column\" = 'Value'");
819
	 *
820
	 *  // multiple predicates as an array
821
	 *  $query->where(array("\"Column\" = 'Value'", "\"Column\" != 'Value'"));
822
	 * </code>
823
	 *
824
	 * @param string|array $where Predicate(s) to set, as escaped SQL statements.
0 ignored issues
show
Bug introduced by
There is no parameter named $where. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
825
	 */
826
	public function where($filter) {
827
		if($filter) {
828
			$this->whereQuery->addWhere($filter);
829
		}
830
831
		return $this;
832
	}
833
834
	/**
835
	 * Set a WHERE with OR.
836
	 *
837
	 * @example $dataQuery->whereAny(array("\"Monkey\" = 'Chimp'", "\"Color\" = 'Brown'"));
838
	 * @see where()
839
	 *
840
	 * @param array $filter Escaped SQL statement.
841
	 * @return DataQuery
842
	 */
843
	public function whereAny($filter) {
844
		if($filter) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $filter 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...
845
			$this->whereQuery->addWhereAny($filter);
846
		}
847
848
		return $this;
849
	}
850
851
	public function __toString() {
852
		if(!$this->whereQuery->getWhere()) {
853
			// We always need to have something so we don't end up with something like '... AND () AND ...'
854
			return '1=1';
855
		}
856
857
		$sql = DB::getConn()->sqlWhereToString(
858
			$this->whereQuery->getWhere(),
859
			$this->whereQuery->getConnective()
860
		);
861
862
		$sql = preg_replace('[^\s*WHERE\s*]', '', $sql);
863
864
		return $sql;
865
	}
866
}
867