Total Complexity | 137 |
Total Lines | 1235 |
Duplicated Lines | 0 % |
Changes | 0 |
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.
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 |
||
21 | class DataQuery |
||
22 | { |
||
23 | |||
24 | /** |
||
25 | * @var string |
||
26 | */ |
||
27 | protected $dataClass; |
||
28 | |||
29 | /** |
||
30 | * @var SQLSelect |
||
31 | */ |
||
32 | protected $query; |
||
33 | |||
34 | /** |
||
35 | * Map of all field names to an array of conflicting column SQL |
||
36 | * |
||
37 | * E.g. |
||
38 | * array( |
||
39 | * 'Title' => array( |
||
40 | * '"MyTable"."Title"', |
||
41 | * '"AnotherTable"."Title"', |
||
42 | * ) |
||
43 | * ) |
||
44 | * |
||
45 | * @var array |
||
46 | */ |
||
47 | protected $collidingFields = array(); |
||
48 | |||
49 | /** |
||
50 | * Allows custom callback to be registered before getFinalisedQuery is called. |
||
51 | * |
||
52 | * @var DataQueryManipulator[] |
||
53 | */ |
||
54 | protected $dataQueryManipulators = []; |
||
55 | |||
56 | private $queriedColumns = null; |
||
57 | |||
58 | /** |
||
59 | * @var bool |
||
60 | */ |
||
61 | private $queryFinalised = false; |
||
|
|||
62 | |||
63 | // TODO: replace subclass_access with this |
||
64 | protected $querySubclasses = true; |
||
65 | // TODO: replace restrictclasses with this |
||
66 | protected $filterByClassName = true; |
||
67 | |||
68 | /** |
||
69 | * Create a new DataQuery. |
||
70 | * |
||
71 | * @param string $dataClass The name of the DataObject class that you wish to query |
||
72 | */ |
||
73 | public function __construct($dataClass) |
||
74 | { |
||
75 | $this->dataClass = $dataClass; |
||
76 | $this->initialiseQuery(); |
||
77 | } |
||
78 | |||
79 | /** |
||
80 | * Clone this object |
||
81 | */ |
||
82 | public function __clone() |
||
83 | { |
||
84 | $this->query = clone $this->query; |
||
85 | } |
||
86 | |||
87 | /** |
||
88 | * Return the {@link DataObject} class that is being queried. |
||
89 | * |
||
90 | * @return string |
||
91 | */ |
||
92 | public function dataClass() |
||
93 | { |
||
94 | return $this->dataClass; |
||
95 | } |
||
96 | |||
97 | /** |
||
98 | * Return the {@link SQLSelect} object that represents the current query; note that it will |
||
99 | * be a clone of the object. |
||
100 | * |
||
101 | * @return SQLSelect |
||
102 | */ |
||
103 | public function query() |
||
104 | { |
||
105 | return $this->getFinalisedQuery(); |
||
106 | } |
||
107 | |||
108 | |||
109 | /** |
||
110 | * Remove a filter from the query |
||
111 | * |
||
112 | * @param string|array $fieldExpression The predicate of the condition to remove |
||
113 | * (ignoring parameters). The expression will be considered a match if it's |
||
114 | * contained within any other predicate. |
||
115 | * @return $this |
||
116 | */ |
||
117 | public function removeFilterOn($fieldExpression) |
||
118 | { |
||
119 | $matched = false; |
||
120 | |||
121 | // If given a parameterised condition extract only the condition |
||
122 | if (is_array($fieldExpression)) { |
||
123 | reset($fieldExpression); |
||
124 | $fieldExpression = key($fieldExpression); |
||
125 | } |
||
126 | |||
127 | $where = $this->query->getWhere(); |
||
128 | // Iterate through each condition |
||
129 | foreach ($where as $i => $condition) { |
||
130 | // Rewrite condition groups as plain conditions before comparison |
||
131 | if ($condition instanceof SQLConditionGroup) { |
||
132 | $predicate = $condition->conditionSQL($parameters); |
||
133 | $condition = array($predicate => $parameters); |
||
134 | } |
||
135 | |||
136 | // As each condition is a single length array, do a single |
||
137 | // iteration to extract the predicate and parameters |
||
138 | foreach ($condition as $predicate => $parameters) { |
||
139 | // @see SQLSelect::addWhere for why this is required here |
||
140 | if (strpos($predicate, $fieldExpression) !== false) { |
||
141 | unset($where[$i]); |
||
142 | $matched = true; |
||
143 | } |
||
144 | // Enforce single-item condition predicate => parameters structure |
||
145 | break; |
||
146 | } |
||
147 | } |
||
148 | |||
149 | // set the entire where clause back, but clear the original one first |
||
150 | if ($matched) { |
||
151 | $this->query->setWhere($where); |
||
152 | } else { |
||
153 | throw new InvalidArgumentException("Couldn't find $fieldExpression in the query filter."); |
||
154 | } |
||
155 | |||
156 | return $this; |
||
157 | } |
||
158 | |||
159 | /** |
||
160 | * Set up the simplest initial query |
||
161 | */ |
||
162 | protected function initialiseQuery() |
||
163 | { |
||
164 | // Join on base table and let lazy loading join subtables |
||
165 | $baseClass = DataObject::getSchema()->baseDataClass($this->dataClass()); |
||
166 | if (!$baseClass) { |
||
167 | throw new InvalidArgumentException("DataQuery::create() Can't find data classes for '{$this->dataClass}'"); |
||
168 | } |
||
169 | |||
170 | // Build our intial query |
||
171 | $this->query = new SQLSelect(array()); |
||
172 | $this->query->setDistinct(true); |
||
173 | |||
174 | if ($sort = singleton($this->dataClass)->config()->get('default_sort')) { |
||
175 | $this->sort($sort); |
||
176 | } |
||
177 | |||
178 | $baseTable = DataObject::getSchema()->tableName($baseClass); |
||
179 | $this->query->setFrom("\"{$baseTable}\""); |
||
180 | |||
181 | $obj = Injector::inst()->get($baseClass); |
||
182 | $obj->extend('augmentDataQueryCreation', $this->query, $this); |
||
183 | } |
||
184 | |||
185 | /** |
||
186 | * @param array $queriedColumns |
||
187 | * @return $this |
||
188 | */ |
||
189 | public function setQueriedColumns($queriedColumns) |
||
190 | { |
||
191 | $this->queriedColumns = $queriedColumns; |
||
192 | return $this; |
||
193 | } |
||
194 | |||
195 | /** |
||
196 | * Ensure that the query is ready to execute. |
||
197 | * |
||
198 | * @param array|null $queriedColumns Any columns to filter the query by |
||
199 | * @return SQLSelect The finalised sql query |
||
200 | */ |
||
201 | public function getFinalisedQuery($queriedColumns = null) |
||
202 | { |
||
203 | if (!$queriedColumns) { |
||
204 | $queriedColumns = $this->queriedColumns; |
||
205 | } |
||
206 | if ($queriedColumns) { |
||
207 | $queriedColumns = array_merge($queriedColumns, array('Created', 'LastEdited', 'ClassName')); |
||
208 | } |
||
209 | $query = clone $this->query; |
||
210 | |||
211 | // Apply manipulators before finalising query |
||
212 | foreach ($this->getDataQueryManipulators() as $manipulator) { |
||
213 | $manipulator->beforeGetFinalisedQuery($this, $queriedColumns, $query); |
||
214 | } |
||
215 | |||
216 | $schema = DataObject::getSchema(); |
||
217 | $baseDataClass = $schema->baseDataClass($this->dataClass()); |
||
218 | $baseIDColumn = $schema->sqlColumnForField($baseDataClass, 'ID'); |
||
219 | $ancestorClasses = ClassInfo::ancestry($this->dataClass(), true); |
||
220 | |||
221 | // Generate the list of tables to iterate over and the list of columns required |
||
222 | // by any existing where clauses. This second step is skipped if we're fetching |
||
223 | // the whole dataobject as any required columns will get selected regardless. |
||
224 | if ($queriedColumns) { |
||
225 | // Specifying certain columns allows joining of child tables |
||
226 | $tableClasses = ClassInfo::dataClassesFor($this->dataClass); |
||
227 | |||
228 | // Ensure that any filtered columns are included in the selected columns |
||
229 | foreach ($query->getWhereParameterised($parameters) as $where) { |
||
230 | // Check for any columns in the form '"Column" = ?' or '"Table"."Column"' = ? |
||
231 | if (preg_match_all( |
||
232 | '/(?:"(?<table>[^"]+)"\.)?"(?<column>[^"]+)"(?:[^\.]|$)/', |
||
233 | $where, |
||
234 | $matches, |
||
235 | PREG_SET_ORDER |
||
236 | )) { |
||
237 | foreach ($matches as $match) { |
||
238 | $column = $match['column']; |
||
239 | if (!in_array($column, $queriedColumns)) { |
||
240 | $queriedColumns[] = $column; |
||
241 | } |
||
242 | } |
||
243 | } |
||
244 | } |
||
245 | } else { |
||
246 | $tableClasses = $ancestorClasses; |
||
247 | } |
||
248 | |||
249 | // Iterate over the tables and check what we need to select from them. If any selects are made (or the table is |
||
250 | // required for a select) |
||
251 | foreach ($tableClasses as $tableClass) { |
||
252 | // Determine explicit columns to select |
||
253 | $selectColumns = null; |
||
254 | if ($queriedColumns) { |
||
255 | // Restrict queried columns to that on the selected table |
||
256 | $tableFields = $schema->databaseFields($tableClass, false); |
||
257 | unset($tableFields['ID']); |
||
258 | $selectColumns = array_intersect($queriedColumns, array_keys($tableFields)); |
||
259 | } |
||
260 | |||
261 | // If this is a subclass without any explicitly requested columns, omit this from the query |
||
262 | if (!in_array($tableClass, $ancestorClasses) && empty($selectColumns)) { |
||
263 | continue; |
||
264 | } |
||
265 | |||
266 | // Select necessary columns (unless an explicitly empty array) |
||
267 | if ($selectColumns !== array()) { |
||
268 | $this->selectColumnsFromTable($query, $tableClass, $selectColumns); |
||
269 | } |
||
270 | |||
271 | // Join if not the base table |
||
272 | if ($tableClass !== $baseDataClass) { |
||
273 | $tableName = $schema->tableName($tableClass); |
||
274 | $query->addLeftJoin( |
||
275 | $tableName, |
||
276 | "\"{$tableName}\".\"ID\" = {$baseIDColumn}", |
||
277 | $tableName, |
||
278 | 10 |
||
279 | ); |
||
280 | } |
||
281 | } |
||
282 | |||
283 | // Resolve colliding fields |
||
284 | if ($this->collidingFields) { |
||
285 | foreach ($this->collidingFields as $collisionField => $collisions) { |
||
286 | $caseClauses = array(); |
||
287 | foreach ($collisions as $collision) { |
||
288 | if (preg_match('/^"(?<table>[^"]+)"\./', $collision, $matches)) { |
||
289 | $collisionTable = $matches['table']; |
||
290 | $collisionClass = $schema->tableClass($collisionTable); |
||
291 | if ($collisionClass) { |
||
292 | $collisionClassColumn = $schema->sqlColumnForField($collisionClass, 'ClassName'); |
||
293 | $collisionClasses = ClassInfo::subclassesFor($collisionClass); |
||
294 | $collisionClassesSQL = implode(', ', Convert::raw2sql($collisionClasses, true)); |
||
295 | $caseClauses[] = "WHEN {$collisionClassColumn} IN ({$collisionClassesSQL}) THEN $collision"; |
||
296 | } |
||
297 | } else { |
||
298 | user_error("Bad collision item '$collision'", E_USER_WARNING); |
||
299 | } |
||
300 | } |
||
301 | $query->selectField("CASE " . implode(" ", $caseClauses) . " ELSE NULL END", $collisionField); |
||
302 | } |
||
303 | } |
||
304 | |||
305 | |||
306 | if ($this->filterByClassName) { |
||
307 | // If querying the base class, don't bother filtering on class name |
||
308 | if ($this->dataClass != $baseDataClass) { |
||
309 | // Get the ClassName values to filter to |
||
310 | $classNames = ClassInfo::subclassesFor($this->dataClass); |
||
311 | $classNamesPlaceholders = DB::placeholders($classNames); |
||
312 | $baseClassColumn = $schema->sqlColumnForField($baseDataClass, 'ClassName'); |
||
313 | $query->addWhere(array( |
||
314 | "{$baseClassColumn} IN ($classNamesPlaceholders)" => $classNames |
||
315 | )); |
||
316 | } |
||
317 | } |
||
318 | |||
319 | // Select ID |
||
320 | $query->selectField($baseIDColumn, "ID"); |
||
321 | |||
322 | // Select RecordClassName |
||
323 | $baseClassColumn = $schema->sqlColumnForField($baseDataClass, 'ClassName'); |
||
324 | $query->selectField( |
||
325 | " |
||
326 | CASE WHEN {$baseClassColumn} IS NOT NULL THEN {$baseClassColumn} |
||
327 | ELSE ".Convert::raw2sql($baseDataClass, true)." END", |
||
328 | "RecordClassName" |
||
329 | ); |
||
330 | |||
331 | // TODO: Versioned, Translatable, SiteTreeSubsites, etc, could probably be better implemented as subclasses |
||
332 | // of DataQuery |
||
333 | |||
334 | $obj = Injector::inst()->get($this->dataClass); |
||
335 | $obj->extend('augmentSQL', $query, $this); |
||
336 | |||
337 | $this->ensureSelectContainsOrderbyColumns($query); |
||
338 | |||
339 | // Apply post-finalisation manipulations |
||
340 | foreach ($this->getDataQueryManipulators() as $manipulator) { |
||
341 | $manipulator->afterGetFinalisedQuery($this, $queriedColumns, $query); |
||
342 | } |
||
343 | |||
344 | return $query; |
||
345 | } |
||
346 | |||
347 | /** |
||
348 | * Ensure that if a query has an order by clause, those columns are present in the select. |
||
349 | * |
||
350 | * @param SQLSelect $query |
||
351 | * @param array $originalSelect |
||
352 | */ |
||
353 | protected function ensureSelectContainsOrderbyColumns($query, $originalSelect = array()) |
||
354 | { |
||
355 | if ($orderby = $query->getOrderBy()) { |
||
356 | $newOrderby = array(); |
||
357 | $i = 0; |
||
358 | foreach ($orderby as $k => $dir) { |
||
359 | $newOrderby[$k] = $dir; |
||
360 | |||
361 | // don't touch functions in the ORDER BY or public function calls |
||
362 | // selected as fields |
||
363 | if (strpos($k, '(') !== false) { |
||
364 | continue; |
||
365 | } |
||
366 | |||
367 | $col = str_replace('"', '', trim($k)); |
||
368 | $parts = explode('.', $col); |
||
369 | |||
370 | // Pull through SortColumn references from the originalSelect variables |
||
371 | if (preg_match('/_SortColumn/', $col)) { |
||
372 | if (isset($originalSelect[$col])) { |
||
373 | $query->selectField($originalSelect[$col], $col); |
||
374 | } |
||
375 | |||
376 | continue; |
||
377 | } |
||
378 | |||
379 | if (count($parts) == 1) { |
||
380 | // Get expression for sort value |
||
381 | $qualCol = "\"{$parts[0]}\""; |
||
382 | $table = DataObject::getSchema()->tableForField($this->dataClass(), $parts[0]); |
||
383 | if ($table) { |
||
384 | $qualCol = "\"{$table}\".{$qualCol}"; |
||
385 | } |
||
386 | |||
387 | // remove original sort |
||
388 | unset($newOrderby[$k]); |
||
389 | |||
390 | // add new columns sort |
||
391 | $newOrderby[$qualCol] = $dir; |
||
392 | |||
393 | // To-do: Remove this if block once SQLSelect::$select has been refactored to store getSelect() |
||
394 | // format internally; then this check can be part of selectField() |
||
395 | $selects = $query->getSelect(); |
||
396 | if (!isset($selects[$col]) && !in_array($qualCol, $selects)) { |
||
397 | $query->selectField($qualCol); |
||
398 | } |
||
399 | } else { |
||
400 | $qualCol = '"' . implode('"."', $parts) . '"'; |
||
401 | |||
402 | if (!in_array($qualCol, $query->getSelect())) { |
||
403 | unset($newOrderby[$k]); |
||
404 | |||
405 | $newOrderby["\"_SortColumn$i\""] = $dir; |
||
406 | $query->selectField($qualCol, "_SortColumn$i"); |
||
407 | |||
408 | $i++; |
||
409 | } |
||
410 | } |
||
411 | } |
||
412 | |||
413 | $query->setOrderBy($newOrderby); |
||
414 | } |
||
415 | } |
||
416 | |||
417 | /** |
||
418 | * Execute the query and return the result as {@link SS_Query} object. |
||
419 | * |
||
420 | * @return Query |
||
421 | */ |
||
422 | public function execute() |
||
423 | { |
||
424 | return $this->getFinalisedQuery()->execute(); |
||
425 | } |
||
426 | |||
427 | /** |
||
428 | * Return this query's SQL |
||
429 | * |
||
430 | * @param array $parameters Out variable for parameters required for this query |
||
431 | * @return string The resulting SQL query (may be paramaterised) |
||
432 | */ |
||
433 | public function sql(&$parameters = array()) |
||
434 | { |
||
435 | return $this->getFinalisedQuery()->sql($parameters); |
||
436 | } |
||
437 | |||
438 | /** |
||
439 | * Return the number of records in this query. |
||
440 | * Note that this will issue a separate SELECT COUNT() query. |
||
441 | * |
||
442 | * @return int |
||
443 | */ |
||
444 | public function count() |
||
445 | { |
||
446 | $quotedColumn = DataObject::getSchema()->sqlColumnForField($this->dataClass(), 'ID'); |
||
447 | return $this->getFinalisedQuery()->count("DISTINCT {$quotedColumn}"); |
||
448 | } |
||
449 | |||
450 | /** |
||
451 | * Return the maximum value of the given field in this DataList |
||
452 | * |
||
453 | * @param string $field Unquoted database column name. Will be ANSI quoted |
||
454 | * automatically so must not contain double quotes. |
||
455 | * @return string |
||
456 | */ |
||
457 | public function max($field) |
||
458 | { |
||
459 | $table = DataObject::getSchema()->tableForField($this->dataClass, $field); |
||
460 | if (!$table) { |
||
461 | return $this->aggregate("MAX(\"$field\")"); |
||
462 | } |
||
463 | return $this->aggregate("MAX(\"$table\".\"$field\")"); |
||
464 | } |
||
465 | |||
466 | /** |
||
467 | * Return the minimum value of the given field in this DataList |
||
468 | * |
||
469 | * @param string $field Unquoted database column name. Will be ANSI quoted |
||
470 | * automatically so must not contain double quotes. |
||
471 | * @return string |
||
472 | */ |
||
473 | public function min($field) |
||
474 | { |
||
475 | $table = DataObject::getSchema()->tableForField($this->dataClass, $field); |
||
476 | if (!$table) { |
||
477 | return $this->aggregate("MIN(\"$field\")"); |
||
478 | } |
||
479 | return $this->aggregate("MIN(\"$table\".\"$field\")"); |
||
480 | } |
||
481 | |||
482 | /** |
||
483 | * Return the average value of the given field in this DataList |
||
484 | * |
||
485 | * @param string $field Unquoted database column name. Will be ANSI quoted |
||
486 | * automatically so must not contain double quotes. |
||
487 | * @return string |
||
488 | */ |
||
489 | public function avg($field) |
||
490 | { |
||
491 | $table = DataObject::getSchema()->tableForField($this->dataClass, $field); |
||
492 | if (!$table) { |
||
493 | return $this->aggregate("AVG(\"$field\")"); |
||
494 | } |
||
495 | return $this->aggregate("AVG(\"$table\".\"$field\")"); |
||
496 | } |
||
497 | |||
498 | /** |
||
499 | * Return the sum of the values of the given field in this DataList |
||
500 | * |
||
501 | * @param string $field Unquoted database column name. Will be ANSI quoted |
||
502 | * automatically so must not contain double quotes. |
||
503 | * @return string |
||
504 | */ |
||
505 | public function sum($field) |
||
506 | { |
||
507 | $table = DataObject::getSchema()->tableForField($this->dataClass, $field); |
||
508 | if (!$table) { |
||
509 | return $this->aggregate("SUM(\"$field\")"); |
||
510 | } |
||
511 | return $this->aggregate("SUM(\"$table\".\"$field\")"); |
||
512 | } |
||
513 | |||
514 | /** |
||
515 | * Runs a raw aggregate expression. Please handle escaping yourself |
||
516 | * |
||
517 | * @param string $expression An aggregate expression, such as 'MAX("Balance")', or a set of them |
||
518 | * (as an escaped SQL statement) |
||
519 | * @return string |
||
520 | */ |
||
521 | public function aggregate($expression) |
||
522 | { |
||
523 | return $this->getFinalisedQuery()->aggregate($expression)->execute()->value(); |
||
524 | } |
||
525 | |||
526 | /** |
||
527 | * Return the first row that would be returned by this full DataQuery |
||
528 | * Note that this will issue a separate SELECT ... LIMIT 1 query. |
||
529 | * |
||
530 | * @return SQLSelect |
||
531 | */ |
||
532 | public function firstRow() |
||
533 | { |
||
534 | return $this->getFinalisedQuery()->firstRow(); |
||
535 | } |
||
536 | |||
537 | /** |
||
538 | * Return the last row that would be returned by this full DataQuery |
||
539 | * Note that this will issue a separate SELECT ... LIMIT query. |
||
540 | * |
||
541 | * @return SQLSelect |
||
542 | */ |
||
543 | public function lastRow() |
||
544 | { |
||
545 | return $this->getFinalisedQuery()->lastRow(); |
||
546 | } |
||
547 | |||
548 | /** |
||
549 | * Update the SELECT clause of the query with the columns from the given table |
||
550 | * |
||
551 | * @param SQLSelect $query |
||
552 | * @param string $tableClass Class to select from |
||
553 | * @param array $columns |
||
554 | */ |
||
555 | protected function selectColumnsFromTable(SQLSelect &$query, $tableClass, $columns = null) |
||
556 | { |
||
557 | // Add SQL for multi-value fields |
||
558 | $schema = DataObject::getSchema(); |
||
559 | $databaseFields = $schema->databaseFields($tableClass, false); |
||
560 | $compositeFields = $schema->compositeFields($tableClass, false); |
||
561 | unset($databaseFields['ID']); |
||
562 | foreach ($databaseFields as $k => $v) { |
||
563 | if ((is_null($columns) || in_array($k, $columns)) && !isset($compositeFields[$k])) { |
||
564 | // Update $collidingFields if necessary |
||
565 | $expressionForField = $query->expressionForField($k); |
||
566 | $quotedField = $schema->sqlColumnForField($tableClass, $k); |
||
567 | if ($expressionForField) { |
||
568 | if (!isset($this->collidingFields[$k])) { |
||
569 | $this->collidingFields[$k] = array($expressionForField); |
||
570 | } |
||
571 | $this->collidingFields[$k][] = $quotedField; |
||
572 | } else { |
||
573 | $query->selectField($quotedField, $k); |
||
574 | } |
||
575 | } |
||
576 | } |
||
577 | foreach ($compositeFields as $k => $v) { |
||
578 | if ((is_null($columns) || in_array($k, $columns)) && $v) { |
||
579 | $tableName = $schema->tableName($tableClass); |
||
580 | $dbO = Injector::inst()->create($v, $k); |
||
581 | $dbO->setTable($tableName); |
||
582 | $dbO->addToQuery($query); |
||
583 | } |
||
584 | } |
||
585 | } |
||
586 | |||
587 | /** |
||
588 | * Append a GROUP BY clause to this query. |
||
589 | * |
||
590 | * @param string $groupby Escaped SQL statement |
||
591 | * @return $this |
||
592 | */ |
||
593 | public function groupby($groupby) |
||
594 | { |
||
595 | $this->query->addGroupBy($groupby); |
||
596 | return $this; |
||
597 | } |
||
598 | |||
599 | /** |
||
600 | * Append a HAVING clause to this query. |
||
601 | * |
||
602 | * @param string $having Escaped SQL statement |
||
603 | * @return $this |
||
604 | */ |
||
605 | public function having($having) |
||
606 | { |
||
607 | $this->query->addHaving($having); |
||
608 | return $this; |
||
609 | } |
||
610 | |||
611 | /** |
||
612 | * Create a disjunctive subgroup. |
||
613 | * |
||
614 | * That is a subgroup joined by OR |
||
615 | * |
||
616 | * @return DataQuery_SubGroup |
||
617 | */ |
||
618 | public function disjunctiveGroup() |
||
619 | { |
||
620 | return new DataQuery_SubGroup($this, 'OR'); |
||
621 | } |
||
622 | |||
623 | /** |
||
624 | * Create a conjunctive subgroup |
||
625 | * |
||
626 | * That is a subgroup joined by AND |
||
627 | * |
||
628 | * @return DataQuery_SubGroup |
||
629 | */ |
||
630 | public function conjunctiveGroup() |
||
631 | { |
||
632 | return new DataQuery_SubGroup($this, 'AND'); |
||
633 | } |
||
634 | |||
635 | /** |
||
636 | * Adds a WHERE clause. |
||
637 | * |
||
638 | * @see SQLSelect::addWhere() for syntax examples, although DataQuery |
||
639 | * won't expand multiple arguments as SQLSelect does. |
||
640 | * |
||
641 | * @param string|array|SQLConditionGroup $filter Predicate(s) to set, as escaped SQL statements or |
||
642 | * paramaterised queries |
||
643 | * @return $this |
||
644 | */ |
||
645 | public function where($filter) |
||
646 | { |
||
647 | if ($filter) { |
||
648 | $this->query->addWhere($filter); |
||
649 | } |
||
650 | return $this; |
||
651 | } |
||
652 | |||
653 | /** |
||
654 | * Append a WHERE with OR. |
||
655 | * |
||
656 | * @see SQLSelect::addWhere() for syntax examples, although DataQuery |
||
657 | * won't expand multiple method arguments as SQLSelect does. |
||
658 | * |
||
659 | * @param string|array|SQLConditionGroup $filter Predicate(s) to set, as escaped SQL statements or |
||
660 | * paramaterised queries |
||
661 | * @return $this |
||
662 | */ |
||
663 | public function whereAny($filter) |
||
664 | { |
||
665 | if ($filter) { |
||
666 | $this->query->addWhereAny($filter); |
||
667 | } |
||
668 | return $this; |
||
669 | } |
||
670 | |||
671 | /** |
||
672 | * Set the ORDER BY clause of this query |
||
673 | * |
||
674 | * @see SQLSelect::orderby() |
||
675 | * |
||
676 | * @param string $sort Column to sort on (escaped SQL statement) |
||
677 | * @param string $direction Direction ("ASC" or "DESC", escaped SQL statement) |
||
678 | * @param bool $clear Clear existing values |
||
679 | * @return $this |
||
680 | */ |
||
681 | public function sort($sort = null, $direction = null, $clear = true) |
||
682 | { |
||
683 | if ($clear) { |
||
684 | $this->query->setOrderBy($sort, $direction); |
||
685 | } else { |
||
686 | $this->query->addOrderBy($sort, $direction); |
||
687 | } |
||
688 | |||
689 | return $this; |
||
690 | } |
||
691 | |||
692 | /** |
||
693 | * Reverse order by clause |
||
694 | * |
||
695 | * @return $this |
||
696 | */ |
||
697 | public function reverseSort() |
||
698 | { |
||
699 | $this->query->reverseOrderBy(); |
||
700 | return $this; |
||
701 | } |
||
702 | |||
703 | /** |
||
704 | * Set the limit of this query. |
||
705 | * |
||
706 | * @param int $limit |
||
707 | * @param int $offset |
||
708 | * @return $this |
||
709 | */ |
||
710 | public function limit($limit, $offset = 0) |
||
711 | { |
||
712 | $this->query->setLimit($limit, $offset); |
||
713 | return $this; |
||
714 | } |
||
715 | |||
716 | /** |
||
717 | * Set whether this query should be distinct or not. |
||
718 | * |
||
719 | * @param bool $value |
||
720 | * @return $this |
||
721 | */ |
||
722 | public function distinct($value) |
||
723 | { |
||
724 | $this->query->setDistinct($value); |
||
725 | return $this; |
||
726 | } |
||
727 | |||
728 | /** |
||
729 | * Add an INNER JOIN clause to this query. |
||
730 | * |
||
731 | * @param string $table The unquoted table name to join to. |
||
732 | * @param string $onClause The filter for the join (escaped SQL statement) |
||
733 | * @param string $alias An optional alias name (unquoted) |
||
734 | * @param int $order A numerical index to control the order that joins are added to the query; lower order values |
||
735 | * will cause the query to appear first. The default is 20, and joins created automatically by the |
||
736 | * ORM have a value of 10. |
||
737 | * @param array $parameters Any additional parameters if the join is a parameterised subquery |
||
738 | * @return $this |
||
739 | */ |
||
740 | public function innerJoin($table, $onClause, $alias = null, $order = 20, $parameters = array()) |
||
741 | { |
||
742 | if ($table) { |
||
743 | $this->query->addInnerJoin($table, $onClause, $alias, $order, $parameters); |
||
744 | } |
||
745 | return $this; |
||
746 | } |
||
747 | |||
748 | /** |
||
749 | * Add a LEFT JOIN clause to this query. |
||
750 | * |
||
751 | * @param string $table The unquoted table to join to. |
||
752 | * @param string $onClause The filter for the join (escaped SQL statement). |
||
753 | * @param string $alias An optional alias name (unquoted) |
||
754 | * @param int $order A numerical index to control the order that joins are added to the query; lower order values |
||
755 | * will cause the query to appear first. The default is 20, and joins created automatically by the |
||
756 | * ORM have a value of 10. |
||
757 | * @param array $parameters Any additional parameters if the join is a parameterised subquery |
||
758 | * @return $this |
||
759 | */ |
||
760 | public function leftJoin($table, $onClause, $alias = null, $order = 20, $parameters = array()) |
||
761 | { |
||
762 | if ($table) { |
||
763 | $this->query->addLeftJoin($table, $onClause, $alias, $order, $parameters); |
||
764 | } |
||
765 | return $this; |
||
766 | } |
||
767 | |||
768 | /** |
||
769 | * Prefix of all joined table aliases. E.g. ->filter('Banner.Image.Title)' |
||
770 | * Will join the Banner, and then Image relations |
||
771 | * `$relationPrefx` will be `banner_image_` |
||
772 | * Each table in the Image chain will be suffixed to this prefix. E.g. |
||
773 | * `banner_image_File` and `banner_image_Image` |
||
774 | * |
||
775 | * This will be null if no relation is joined. |
||
776 | * E.g. `->filter('Title')` |
||
777 | * |
||
778 | * @param string|array $relation Relation in '.' delimited string, or array of parts |
||
779 | * @return string Table prefix |
||
780 | */ |
||
781 | public static function applyRelationPrefix($relation) |
||
782 | { |
||
783 | if (!$relation) { |
||
784 | return null; |
||
785 | } |
||
786 | if (is_string($relation)) { |
||
787 | $relation = explode(".", $relation); |
||
788 | } |
||
789 | return strtolower(implode('_', $relation)) . '_'; |
||
790 | } |
||
791 | |||
792 | /** |
||
793 | * Traverse the relationship fields, and add the table |
||
794 | * mappings to the query object state. This has to be called |
||
795 | * in any overloaded {@link SearchFilter->apply()} methods manually. |
||
796 | * |
||
797 | * Note, that in order to filter against the joined relation user code must |
||
798 | * use {@see tablePrefix()} to get the table alias used for this relation. |
||
799 | * |
||
800 | * @param string|array $relation The array/dot-syntax relation to follow |
||
801 | * @param bool $linearOnly Set to true to restrict to linear relations only. Set this |
||
802 | * if this relation will be used for sorting, and should not include duplicate rows. |
||
803 | * @return string The model class of the related item |
||
804 | */ |
||
805 | public function applyRelation($relation, $linearOnly = false) |
||
806 | { |
||
807 | // NO-OP |
||
808 | if (!$relation) { |
||
809 | return $this->dataClass; |
||
810 | } |
||
811 | |||
812 | if (is_string($relation)) { |
||
813 | $relation = explode(".", $relation); |
||
814 | } |
||
815 | |||
816 | $modelClass = $this->dataClass; |
||
817 | |||
818 | $schema = DataObject::getSchema(); |
||
819 | $currentRelation = []; |
||
820 | foreach ($relation as $rel) { |
||
821 | // Get prefix for join for this table (and parent to join on) |
||
822 | $parentPrefix = $this->applyRelationPrefix($currentRelation); |
||
823 | $currentRelation[] = $rel; |
||
824 | $tablePrefix = $this->applyRelationPrefix($currentRelation); |
||
825 | |||
826 | // Check has_one |
||
827 | if ($component = $schema->hasOneComponent($modelClass, $rel)) { |
||
828 | // Join via has_one |
||
829 | $this->joinHasOneRelation($modelClass, $rel, $component, $parentPrefix, $tablePrefix); |
||
830 | $modelClass = $component; |
||
831 | continue; |
||
832 | } |
||
833 | |||
834 | // Check has_many |
||
835 | if ($component = $schema->hasManyComponent($modelClass, $rel)) { |
||
836 | // Fail on non-linear relations |
||
837 | if ($linearOnly) { |
||
838 | throw new InvalidArgumentException("$rel is not a linear relation on model $modelClass"); |
||
839 | } |
||
840 | // Join via has_many |
||
841 | $this->joinHasManyRelation($modelClass, $rel, $component, $parentPrefix, $tablePrefix, 'has_many'); |
||
842 | $modelClass = $component; |
||
843 | continue; |
||
844 | } |
||
845 | |||
846 | // check belongs_to (like has_many but linear safe) |
||
847 | if ($component = $schema->belongsToComponent($modelClass, $rel)) { |
||
848 | // Piggy back off has_many logic |
||
849 | $this->joinHasManyRelation($modelClass, $rel, $component, $parentPrefix, $tablePrefix, 'belongs_to'); |
||
850 | $modelClass = $component; |
||
851 | continue; |
||
852 | } |
||
853 | |||
854 | // Check many_many |
||
855 | if ($component = $schema->manyManyComponent($modelClass, $rel)) { |
||
856 | // Fail on non-linear relations |
||
857 | if ($linearOnly) { |
||
858 | throw new InvalidArgumentException("$rel is not a linear relation on model $modelClass"); |
||
859 | } |
||
860 | $this->joinManyManyRelationship( |
||
861 | $component['relationClass'], |
||
862 | $component['parentClass'], |
||
863 | $component['childClass'], |
||
864 | $component['parentField'], |
||
865 | $component['childField'], |
||
866 | $component['join'], |
||
867 | $parentPrefix, |
||
868 | $tablePrefix |
||
869 | ); |
||
870 | $modelClass = $component['childClass']; |
||
871 | continue; |
||
872 | } |
||
873 | |||
874 | // no relation |
||
875 | throw new InvalidArgumentException("$rel is not a relation on model $modelClass"); |
||
876 | } |
||
877 | |||
878 | return $modelClass; |
||
879 | } |
||
880 | |||
881 | /** |
||
882 | * Join the given has_many relation to this query. |
||
883 | * Also works with belongs_to |
||
884 | * |
||
885 | * Doesn't work with polymorphic relationships |
||
886 | * |
||
887 | * @param string $localClass Name of class that has the has_many to the joined class |
||
888 | * @param string $localField Name of the has_many relationship to join |
||
889 | * @param string $foreignClass Class to join |
||
890 | * @param string $localPrefix Table prefix for parent class |
||
891 | * @param string $foreignPrefix Table prefix to use |
||
892 | * @param string $type 'has_many' or 'belongs_to' |
||
893 | */ |
||
894 | protected function joinHasManyRelation( |
||
895 | $localClass, |
||
896 | $localField, |
||
897 | $foreignClass, |
||
898 | $localPrefix = null, |
||
899 | $foreignPrefix = null, |
||
900 | $type = 'has_many' |
||
901 | ) { |
||
902 | if (!$foreignClass || $foreignClass === DataObject::class) { |
||
903 | throw new InvalidArgumentException("Could not find a has_many relationship {$localField} on {$localClass}"); |
||
904 | } |
||
905 | $schema = DataObject::getSchema(); |
||
906 | |||
907 | // Skip if already joined |
||
908 | // Note: don't just check base class, since we need to join on the table with the actual relation key |
||
909 | $foreignTable = $schema->tableName($foreignClass); |
||
910 | $foreignTableAliased = $foreignPrefix . $foreignTable; |
||
911 | if ($this->query->isJoinedTo($foreignTableAliased)) { |
||
912 | return; |
||
913 | } |
||
914 | |||
915 | // Join table with associated has_one |
||
916 | /** @var DataObject $model */ |
||
917 | $foreignKey = $schema->getRemoteJoinField($localClass, $localField, $type, $polymorphic); |
||
918 | $localIDColumn = $schema->sqlColumnForField($localClass, 'ID', $localPrefix); |
||
919 | if ($polymorphic) { |
||
920 | $foreignKeyIDColumn = $schema->sqlColumnForField($foreignClass, "{$foreignKey}ID", $foreignPrefix); |
||
921 | $foreignKeyClassColumn = $schema->sqlColumnForField($foreignClass, "{$foreignKey}Class", $foreignPrefix); |
||
922 | $localClassColumn = $schema->sqlColumnForField($localClass, 'ClassName', $localPrefix); |
||
923 | $joinExpression = |
||
924 | "{$foreignKeyIDColumn} = {$localIDColumn} AND {$foreignKeyClassColumn} = {$localClassColumn}"; |
||
925 | } else { |
||
926 | $foreignKeyIDColumn = $schema->sqlColumnForField($foreignClass, $foreignKey, $foreignPrefix); |
||
927 | $joinExpression = "{$foreignKeyIDColumn} = {$localIDColumn}"; |
||
928 | } |
||
929 | $this->query->addLeftJoin( |
||
930 | $foreignTable, |
||
931 | $joinExpression, |
||
932 | $foreignTableAliased |
||
933 | ); |
||
934 | |||
935 | // Add join clause to the component's ancestry classes so that the search filter could search on |
||
936 | // its ancestor fields. |
||
937 | $ancestry = ClassInfo::ancestry($foreignClass, true); |
||
938 | $ancestry = array_reverse($ancestry); |
||
939 | foreach ($ancestry as $ancestor) { |
||
940 | $ancestorTable = $schema->tableName($ancestor); |
||
941 | if ($ancestorTable !== $foreignTable) { |
||
942 | $ancestorTableAliased = $foreignPrefix.$ancestorTable; |
||
943 | $this->query->addLeftJoin( |
||
944 | $ancestorTable, |
||
945 | "\"{$foreignTableAliased}\".\"ID\" = \"{$ancestorTableAliased}\".\"ID\"", |
||
946 | $ancestorTableAliased |
||
947 | ); |
||
948 | } |
||
949 | } |
||
950 | } |
||
951 | |||
952 | /** |
||
953 | * Join the given class to this query with the given key |
||
954 | * |
||
955 | * @param string $localClass Name of class that has the has_one to the joined class |
||
956 | * @param string $localField Name of the has_one relationship to joi |
||
957 | * @param string $foreignClass Class to join |
||
958 | * @param string $localPrefix Table prefix to use for local class |
||
959 | * @param string $foreignPrefix Table prefix to use for joined table |
||
960 | */ |
||
961 | protected function joinHasOneRelation( |
||
962 | $localClass, |
||
963 | $localField, |
||
964 | $foreignClass, |
||
965 | $localPrefix = null, |
||
966 | $foreignPrefix = null |
||
967 | ) { |
||
968 | if (!$foreignClass) { |
||
969 | throw new InvalidArgumentException("Could not find a has_one relationship {$localField} on {$localClass}"); |
||
970 | } |
||
971 | |||
972 | if ($foreignClass === DataObject::class) { |
||
973 | throw new InvalidArgumentException( |
||
974 | "Could not join polymorphic has_one relationship {$localField} on {$localClass}" |
||
975 | ); |
||
976 | } |
||
977 | $schema = DataObject::getSchema(); |
||
978 | |||
979 | // Skip if already joined |
||
980 | $foreignBaseClass = $schema->baseDataClass($foreignClass); |
||
981 | $foreignBaseTable = $schema->tableName($foreignBaseClass); |
||
982 | if ($this->query->isJoinedTo($foreignPrefix.$foreignBaseTable)) { |
||
983 | return; |
||
984 | } |
||
985 | |||
986 | // Join base table |
||
987 | $foreignIDColumn = $schema->sqlColumnForField($foreignBaseClass, 'ID', $foreignPrefix); |
||
988 | $localColumn = $schema->sqlColumnForField($localClass, "{$localField}ID", $localPrefix); |
||
989 | $this->query->addLeftJoin( |
||
990 | $foreignBaseTable, |
||
991 | "{$foreignIDColumn} = {$localColumn}", |
||
992 | $foreignPrefix.$foreignBaseTable |
||
993 | ); |
||
994 | |||
995 | // Add join clause to the component's ancestry classes so that the search filter could search on |
||
996 | // its ancestor fields. |
||
997 | $ancestry = ClassInfo::ancestry($foreignClass, true); |
||
998 | if (!empty($ancestry)) { |
||
999 | $ancestry = array_reverse($ancestry); |
||
1000 | foreach ($ancestry as $ancestor) { |
||
1001 | $ancestorTable = $schema->tableName($ancestor); |
||
1002 | if ($ancestorTable !== $foreignBaseTable) { |
||
1003 | $ancestorTableAliased = $foreignPrefix.$ancestorTable; |
||
1004 | $this->query->addLeftJoin( |
||
1005 | $ancestorTable, |
||
1006 | "{$foreignIDColumn} = \"{$ancestorTableAliased}\".\"ID\"", |
||
1007 | $ancestorTableAliased |
||
1008 | ); |
||
1009 | } |
||
1010 | } |
||
1011 | } |
||
1012 | } |
||
1013 | |||
1014 | /** |
||
1015 | * Join table via many_many relationship |
||
1016 | * |
||
1017 | * @param string $relationClass |
||
1018 | * @param string $parentClass |
||
1019 | * @param string $componentClass |
||
1020 | * @param string $parentField |
||
1021 | * @param string $componentField |
||
1022 | * @param string $relationClassOrTable Name of relation table |
||
1023 | * @param string $parentPrefix Table prefix for parent class |
||
1024 | * @param string $componentPrefix Table prefix to use for both joined and mapping table |
||
1025 | */ |
||
1026 | protected function joinManyManyRelationship( |
||
1027 | $relationClass, |
||
1028 | $parentClass, |
||
1029 | $componentClass, |
||
1030 | $parentField, |
||
1031 | $componentField, |
||
1032 | $relationClassOrTable, |
||
1033 | $parentPrefix = null, |
||
1034 | $componentPrefix = null |
||
1035 | ) { |
||
1036 | $schema = DataObject::getSchema(); |
||
1037 | |||
1038 | if (class_exists($relationClassOrTable)) { |
||
1039 | $relationClassOrTable = $schema->tableName($relationClassOrTable); |
||
1040 | } |
||
1041 | |||
1042 | // Check if already joined to component alias (skip join table for the check) |
||
1043 | $componentBaseClass = $schema->baseDataClass($componentClass); |
||
1044 | $componentBaseTable = $schema->tableName($componentBaseClass); |
||
1045 | $componentAliasedTable = $componentPrefix . $componentBaseTable; |
||
1046 | if ($this->query->isJoinedTo($componentAliasedTable)) { |
||
1047 | return; |
||
1048 | } |
||
1049 | |||
1050 | // Join parent class to join table |
||
1051 | $relationAliasedTable = $componentPrefix.$relationClassOrTable; |
||
1052 | $parentIDColumn = $schema->sqlColumnForField($parentClass, 'ID', $parentPrefix); |
||
1053 | $this->query->addLeftJoin( |
||
1054 | $relationClassOrTable, |
||
1055 | "\"{$relationAliasedTable}\".\"{$parentField}\" = {$parentIDColumn}", |
||
1056 | $relationAliasedTable |
||
1057 | ); |
||
1058 | |||
1059 | // Join on base table of component class |
||
1060 | $componentIDColumn = $schema->sqlColumnForField($componentBaseClass, 'ID', $componentPrefix); |
||
1061 | $this->query->addLeftJoin( |
||
1062 | $componentBaseTable, |
||
1063 | "\"{$relationAliasedTable}\".\"{$componentField}\" = {$componentIDColumn}", |
||
1064 | $componentAliasedTable |
||
1065 | ); |
||
1066 | |||
1067 | // Add join clause to the component's ancestry classes so that the search filter could search on |
||
1068 | // its ancestor fields. |
||
1069 | $ancestry = ClassInfo::ancestry($componentClass, true); |
||
1070 | $ancestry = array_reverse($ancestry); |
||
1071 | foreach ($ancestry as $ancestor) { |
||
1072 | $ancestorTable = $schema->tableName($ancestor); |
||
1073 | if ($ancestorTable !== $componentBaseTable) { |
||
1074 | $ancestorTableAliased = $componentPrefix.$ancestorTable; |
||
1075 | $this->query->addLeftJoin( |
||
1076 | $ancestorTable, |
||
1077 | "{$componentIDColumn} = \"{$ancestorTableAliased}\".\"ID\"", |
||
1078 | $ancestorTableAliased |
||
1079 | ); |
||
1080 | } |
||
1081 | } |
||
1082 | } |
||
1083 | |||
1084 | /** |
||
1085 | * Removes the result of query from this query. |
||
1086 | * |
||
1087 | * @param DataQuery $subtractQuery |
||
1088 | * @param string $field |
||
1089 | * @return $this |
||
1090 | */ |
||
1091 | public function subtract(DataQuery $subtractQuery, $field = 'ID') |
||
1102 | } |
||
1103 | |||
1104 | /** |
||
1105 | * Select the only given fields from the given table. |
||
1106 | * |
||
1107 | * @param string $table Unquoted table name (will be escaped automatically) |
||
1108 | * @param array $fields Database column names (will be escaped automatically) |
||
1109 | * @return $this |
||
1110 | */ |
||
1111 | public function selectFromTable($table, $fields) |
||
1112 | { |
||
1113 | $fieldExpressions = array_map(function ($item) use ($table) { |
||
1114 | return Convert::symbol2sql("{$table}.{$item}"); |
||
1115 | }, $fields); |
||
1116 | |||
1117 | $this->query->setSelect($fieldExpressions); |
||
1118 | |||
1119 | return $this; |
||
1120 | } |
||
1121 | |||
1122 | /** |
||
1123 | * Add the given fields from the given table to the select statement. |
||
1124 | * |
||
1125 | * @param string $table Unquoted table name (will be escaped automatically) |
||
1126 | * @param array $fields Database column names (will be escaped automatically) |
||
1127 | * @return $this |
||
1128 | */ |
||
1129 | public function addSelectFromTable($table, $fields) |
||
1130 | { |
||
1131 | $fieldExpressions = array_map(function ($item) use ($table) { |
||
1132 | return Convert::symbol2sql("{$table}.{$item}"); |
||
1133 | }, $fields); |
||
1134 | |||
1135 | $this->query->addSelect($fieldExpressions); |
||
1136 | |||
1137 | return $this; |
||
1138 | } |
||
1139 | |||
1140 | /** |
||
1141 | * Query the given field column from the database and return as an array. |
||
1142 | * |
||
1143 | * @param string $field See {@link expressionForField()}. |
||
1144 | * @return array List of column values for the specified column |
||
1145 | */ |
||
1146 | public function column($field = 'ID') |
||
1147 | { |
||
1148 | $fieldExpression = $this->expressionForField($field); |
||
1149 | $query = $this->getFinalisedQuery(array($field)); |
||
1150 | $originalSelect = $query->getSelect(); |
||
1151 | $query->setSelect(array()); |
||
1152 | $query->selectField($fieldExpression, $field); |
||
1153 | $this->ensureSelectContainsOrderbyColumns($query, $originalSelect); |
||
1154 | |||
1155 | return $query->execute()->column($field); |
||
1156 | } |
||
1157 | |||
1158 | /** |
||
1159 | * @param string $field Select statement identifier, either the unquoted column name, |
||
1160 | * the full composite SQL statement, or the alias set through {@link SQLSelect->selectField()}. |
||
1161 | * @return string The expression used to query this field via this DataQuery |
||
1162 | */ |
||
1163 | protected function expressionForField($field) |
||
1164 | { |
||
1165 | // Prepare query object for selecting this field |
||
1166 | $query = $this->getFinalisedQuery(array($field)); |
||
1167 | |||
1168 | // Allow query to define the expression for this field |
||
1169 | $expression = $query->expressionForField($field); |
||
1170 | if (!empty($expression)) { |
||
1171 | return $expression; |
||
1172 | } |
||
1173 | |||
1174 | // Special case for ID, if not provided |
||
1175 | if ($field === 'ID') { |
||
1176 | return DataObject::getSchema()->sqlColumnForField($this->dataClass, 'ID'); |
||
1177 | } |
||
1178 | return null; |
||
1179 | } |
||
1180 | |||
1181 | /** |
||
1182 | * Select the given field expressions. |
||
1183 | * |
||
1184 | * @param string $fieldExpression String The field to select (escaped SQL statement) |
||
1185 | * @param string $alias String The alias of that field (escaped SQL statement) |
||
1186 | */ |
||
1187 | public function selectField($fieldExpression, $alias = null) |
||
1188 | { |
||
1189 | $this->query->selectField($fieldExpression, $alias); |
||
1190 | } |
||
1191 | |||
1192 | //// QUERY PARAMS |
||
1193 | |||
1194 | /** |
||
1195 | * An arbitrary store of query parameters that can be used by decorators. |
||
1196 | */ |
||
1197 | private $queryParams; |
||
1198 | |||
1199 | /** |
||
1200 | * Set an arbitrary query parameter, that can be used by decorators to add additional meta-data to the query. |
||
1201 | * It's expected that the $key will be namespaced, e.g, 'Versioned.stage' instead of just 'stage'. |
||
1202 | * |
||
1203 | * @param string $key |
||
1204 | * @param string|array $value |
||
1205 | * @return $this |
||
1206 | */ |
||
1207 | public function setQueryParam($key, $value) |
||
1208 | { |
||
1209 | $this->queryParams[$key] = $value; |
||
1210 | return $this; |
||
1211 | } |
||
1212 | |||
1213 | /** |
||
1214 | * Set an arbitrary query parameter, that can be used by decorators to add additional meta-data to the query. |
||
1215 | * |
||
1216 | * @param string $key |
||
1217 | * @return string |
||
1218 | */ |
||
1219 | public function getQueryParam($key) |
||
1220 | { |
||
1221 | if (isset($this->queryParams[$key])) { |
||
1222 | return $this->queryParams[$key]; |
||
1223 | } |
||
1224 | return null; |
||
1225 | } |
||
1226 | |||
1227 | /** |
||
1228 | * Returns all query parameters |
||
1229 | * @return array query parameters array |
||
1230 | */ |
||
1231 | public function getQueryParams() |
||
1234 | } |
||
1235 | |||
1236 | /** |
||
1237 | * Get query manipulators |
||
1238 | * |
||
1239 | * @return DataQueryManipulator[] |
||
1240 | */ |
||
1241 | public function getDataQueryManipulators() |
||
1242 | { |
||
1244 | } |
||
1245 | |||
1246 | /** |
||
1247 | * Assign callback to be invoked in getFinalisedQuery() |
||
1248 | * |
||
1249 | * @param DataQueryManipulator $manipulator |
||
1250 | * @return $this |
||
1251 | */ |
||
1252 | public function pushQueryManipulator(DataQueryManipulator $manipulator) |
||
1253 | { |
||
1254 | $this->dataQueryManipulators[] = $manipulator; |
||
1255 | return $this; |
||
1256 | } |
||
1257 | } |
||
1258 |