Total Complexity | 138 |
Total Lines | 1238 |
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) |
||
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) |
||
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 | foreach ($orderby as $k => $dir) { |
||
358 | $newOrderby[$k] = $dir; |
||
359 | |||
360 | // don't touch functions in the ORDER BY or public function calls |
||
361 | // selected as fields |
||
362 | if (strpos($k, '(') !== false) { |
||
363 | continue; |
||
364 | } |
||
365 | |||
366 | $col = str_replace('"', '', trim($k)); |
||
367 | $parts = explode('.', $col); |
||
368 | |||
369 | // Pull through SortColumn references from the originalSelect variables |
||
370 | if (preg_match('/_SortColumn/', $col)) { |
||
371 | if (isset($originalSelect[$col])) { |
||
372 | $query->selectField($originalSelect[$col], $col); |
||
373 | } |
||
374 | continue; |
||
375 | } |
||
376 | |||
377 | if (count($parts) == 1) { |
||
378 | // Get expression for sort value |
||
379 | $qualCol = "\"{$parts[0]}\""; |
||
380 | $table = DataObject::getSchema()->tableForField($this->dataClass(), $parts[0]); |
||
381 | if ($table) { |
||
382 | $qualCol = "\"{$table}\".{$qualCol}"; |
||
383 | } |
||
384 | |||
385 | // remove original sort |
||
386 | unset($newOrderby[$k]); |
||
387 | |||
388 | // add new columns sort |
||
389 | $newOrderby[$qualCol] = $dir; |
||
390 | |||
391 | // To-do: Remove this if block once SQLSelect::$select has been refactored to store getSelect() |
||
392 | // format internally; then this check can be part of selectField() |
||
393 | $selects = $query->getSelect(); |
||
394 | if (!isset($selects[$col]) && !in_array($qualCol, $selects)) { |
||
395 | $query->selectField($qualCol); |
||
396 | } |
||
397 | } else { |
||
398 | $qualCol = '"' . implode('"."', $parts) . '"'; |
||
399 | |||
400 | if (!in_array($qualCol, $query->getSelect())) { |
||
401 | unset($newOrderby[$k]); |
||
402 | |||
403 | // Find the first free "_SortColumnX" slot |
||
404 | // and assign it to $key |
||
405 | $i = 0; |
||
406 | while (isset($orderby[$key = "\"_SortColumn$i\""])) { |
||
407 | ++$i; |
||
408 | } |
||
409 | |||
410 | $newOrderby[$key] = $dir; |
||
411 | $query->selectField($qualCol, "_SortColumn$i"); |
||
412 | } |
||
413 | } |
||
414 | } |
||
415 | |||
416 | $query->setOrderBy($newOrderby); |
||
417 | } |
||
418 | } |
||
419 | |||
420 | /** |
||
421 | * Execute the query and return the result as {@link SS_Query} object. |
||
422 | * |
||
423 | * @return Query |
||
424 | */ |
||
425 | public function execute() |
||
426 | { |
||
427 | return $this->getFinalisedQuery()->execute(); |
||
428 | } |
||
429 | |||
430 | /** |
||
431 | * Return this query's SQL |
||
432 | * |
||
433 | * @param array $parameters Out variable for parameters required for this query |
||
434 | * @return string The resulting SQL query (may be paramaterised) |
||
435 | */ |
||
436 | public function sql(&$parameters = array()) |
||
437 | { |
||
438 | return $this->getFinalisedQuery()->sql($parameters); |
||
439 | } |
||
440 | |||
441 | /** |
||
442 | * Return the number of records in this query. |
||
443 | * Note that this will issue a separate SELECT COUNT() query. |
||
444 | * |
||
445 | * @return int |
||
446 | */ |
||
447 | public function count() |
||
448 | { |
||
449 | $quotedColumn = DataObject::getSchema()->sqlColumnForField($this->dataClass(), 'ID'); |
||
450 | return $this->getFinalisedQuery()->count("DISTINCT {$quotedColumn}"); |
||
451 | } |
||
452 | |||
453 | /** |
||
454 | * Return the maximum value of the given field in this DataList |
||
455 | * |
||
456 | * @param string $field Unquoted database column name. Will be ANSI quoted |
||
457 | * automatically so must not contain double quotes. |
||
458 | * @return string |
||
459 | */ |
||
460 | public function max($field) |
||
461 | { |
||
462 | $table = DataObject::getSchema()->tableForField($this->dataClass, $field); |
||
463 | if (!$table) { |
||
464 | return $this->aggregate("MAX(\"$field\")"); |
||
465 | } |
||
466 | return $this->aggregate("MAX(\"$table\".\"$field\")"); |
||
467 | } |
||
468 | |||
469 | /** |
||
470 | * Return the minimum value of the given field in this DataList |
||
471 | * |
||
472 | * @param string $field Unquoted database column name. Will be ANSI quoted |
||
473 | * automatically so must not contain double quotes. |
||
474 | * @return string |
||
475 | */ |
||
476 | public function min($field) |
||
477 | { |
||
478 | $table = DataObject::getSchema()->tableForField($this->dataClass, $field); |
||
479 | if (!$table) { |
||
480 | return $this->aggregate("MIN(\"$field\")"); |
||
481 | } |
||
482 | return $this->aggregate("MIN(\"$table\".\"$field\")"); |
||
483 | } |
||
484 | |||
485 | /** |
||
486 | * Return the average value of the given field in this DataList |
||
487 | * |
||
488 | * @param string $field Unquoted database column name. Will be ANSI quoted |
||
489 | * automatically so must not contain double quotes. |
||
490 | * @return string |
||
491 | */ |
||
492 | public function avg($field) |
||
493 | { |
||
494 | $table = DataObject::getSchema()->tableForField($this->dataClass, $field); |
||
495 | if (!$table) { |
||
496 | return $this->aggregate("AVG(\"$field\")"); |
||
497 | } |
||
498 | return $this->aggregate("AVG(\"$table\".\"$field\")"); |
||
499 | } |
||
500 | |||
501 | /** |
||
502 | * Return the sum of the values of the given field in this DataList |
||
503 | * |
||
504 | * @param string $field Unquoted database column name. Will be ANSI quoted |
||
505 | * automatically so must not contain double quotes. |
||
506 | * @return string |
||
507 | */ |
||
508 | public function sum($field) |
||
509 | { |
||
510 | $table = DataObject::getSchema()->tableForField($this->dataClass, $field); |
||
511 | if (!$table) { |
||
512 | return $this->aggregate("SUM(\"$field\")"); |
||
513 | } |
||
514 | return $this->aggregate("SUM(\"$table\".\"$field\")"); |
||
515 | } |
||
516 | |||
517 | /** |
||
518 | * Runs a raw aggregate expression. Please handle escaping yourself |
||
519 | * |
||
520 | * @param string $expression An aggregate expression, such as 'MAX("Balance")', or a set of them |
||
521 | * (as an escaped SQL statement) |
||
522 | * @return string |
||
523 | */ |
||
524 | public function aggregate($expression) |
||
525 | { |
||
526 | return $this->getFinalisedQuery()->aggregate($expression)->execute()->value(); |
||
527 | } |
||
528 | |||
529 | /** |
||
530 | * Return the first row that would be returned by this full DataQuery |
||
531 | * Note that this will issue a separate SELECT ... LIMIT 1 query. |
||
532 | * |
||
533 | * @return SQLSelect |
||
534 | */ |
||
535 | public function firstRow() |
||
536 | { |
||
537 | return $this->getFinalisedQuery()->firstRow(); |
||
538 | } |
||
539 | |||
540 | /** |
||
541 | * Return the last row that would be returned by this full DataQuery |
||
542 | * Note that this will issue a separate SELECT ... LIMIT query. |
||
543 | * |
||
544 | * @return SQLSelect |
||
545 | */ |
||
546 | public function lastRow() |
||
547 | { |
||
548 | return $this->getFinalisedQuery()->lastRow(); |
||
549 | } |
||
550 | |||
551 | /** |
||
552 | * Update the SELECT clause of the query with the columns from the given table |
||
553 | * |
||
554 | * @param SQLSelect $query |
||
555 | * @param string $tableClass Class to select from |
||
556 | * @param array $columns |
||
557 | */ |
||
558 | protected function selectColumnsFromTable(SQLSelect &$query, $tableClass, $columns = null) |
||
559 | { |
||
560 | // Add SQL for multi-value fields |
||
561 | $schema = DataObject::getSchema(); |
||
562 | $databaseFields = $schema->databaseFields($tableClass, false); |
||
563 | $compositeFields = $schema->compositeFields($tableClass, false); |
||
564 | unset($databaseFields['ID']); |
||
565 | foreach ($databaseFields as $k => $v) { |
||
566 | if ((is_null($columns) || in_array($k, $columns)) && !isset($compositeFields[$k])) { |
||
567 | // Update $collidingFields if necessary |
||
568 | $expressionForField = $query->expressionForField($k); |
||
569 | $quotedField = $schema->sqlColumnForField($tableClass, $k); |
||
570 | if ($expressionForField) { |
||
571 | if (!isset($this->collidingFields[$k])) { |
||
572 | $this->collidingFields[$k] = array($expressionForField); |
||
573 | } |
||
574 | $this->collidingFields[$k][] = $quotedField; |
||
575 | } else { |
||
576 | $query->selectField($quotedField, $k); |
||
577 | } |
||
578 | } |
||
579 | } |
||
580 | foreach ($compositeFields as $k => $v) { |
||
581 | if ((is_null($columns) || in_array($k, $columns)) && $v) { |
||
582 | $tableName = $schema->tableName($tableClass); |
||
583 | $dbO = Injector::inst()->create($v, $k); |
||
584 | $dbO->setTable($tableName); |
||
585 | $dbO->addToQuery($query); |
||
586 | } |
||
587 | } |
||
588 | } |
||
589 | |||
590 | /** |
||
591 | * Append a GROUP BY clause to this query. |
||
592 | * |
||
593 | * @param string $groupby Escaped SQL statement |
||
594 | * @return $this |
||
595 | */ |
||
596 | public function groupby($groupby) |
||
597 | { |
||
598 | $this->query->addGroupBy($groupby); |
||
599 | return $this; |
||
600 | } |
||
601 | |||
602 | /** |
||
603 | * Append a HAVING clause to this query. |
||
604 | * |
||
605 | * @param mixed $having Predicate(s) to set, as escaped SQL statements or parameterised queries |
||
606 | * @return $this |
||
607 | */ |
||
608 | public function having($having) |
||
609 | { |
||
610 | $this->query->addHaving($having); |
||
611 | return $this; |
||
612 | } |
||
613 | |||
614 | /** |
||
615 | * Create a disjunctive subgroup. |
||
616 | * |
||
617 | * That is a subgroup joined by OR |
||
618 | * |
||
619 | * @return DataQuery_SubGroup |
||
620 | */ |
||
621 | public function disjunctiveGroup() |
||
622 | { |
||
623 | return new DataQuery_SubGroup($this, 'OR'); |
||
624 | } |
||
625 | |||
626 | /** |
||
627 | * Create a conjunctive subgroup |
||
628 | * |
||
629 | * That is a subgroup joined by AND |
||
630 | * |
||
631 | * @return DataQuery_SubGroup |
||
632 | */ |
||
633 | public function conjunctiveGroup() |
||
634 | { |
||
635 | return new DataQuery_SubGroup($this, 'AND'); |
||
636 | } |
||
637 | |||
638 | /** |
||
639 | * Adds a WHERE clause. |
||
640 | * |
||
641 | * @see SQLSelect::addWhere() for syntax examples, although DataQuery |
||
642 | * won't expand multiple arguments as SQLSelect does. |
||
643 | * |
||
644 | * @param string|array|SQLConditionGroup $filter Predicate(s) to set, as escaped SQL statements or |
||
645 | * paramaterised queries |
||
646 | * @return $this |
||
647 | */ |
||
648 | public function where($filter) |
||
649 | { |
||
650 | if ($filter) { |
||
651 | $this->query->addWhere($filter); |
||
652 | } |
||
653 | return $this; |
||
654 | } |
||
655 | |||
656 | /** |
||
657 | * Append a WHERE with OR. |
||
658 | * |
||
659 | * @see SQLSelect::addWhere() for syntax examples, although DataQuery |
||
660 | * won't expand multiple method arguments as SQLSelect does. |
||
661 | * |
||
662 | * @param string|array|SQLConditionGroup $filter Predicate(s) to set, as escaped SQL statements or |
||
663 | * paramaterised queries |
||
664 | * @return $this |
||
665 | */ |
||
666 | public function whereAny($filter) |
||
667 | { |
||
668 | if ($filter) { |
||
669 | $this->query->addWhereAny($filter); |
||
670 | } |
||
671 | return $this; |
||
672 | } |
||
673 | |||
674 | /** |
||
675 | * Set the ORDER BY clause of this query |
||
676 | * |
||
677 | * @see SQLSelect::orderby() |
||
678 | * |
||
679 | * @param string $sort Column to sort on (escaped SQL statement) |
||
680 | * @param string $direction Direction ("ASC" or "DESC", escaped SQL statement) |
||
681 | * @param bool $clear Clear existing values |
||
682 | * @return $this |
||
683 | */ |
||
684 | public function sort($sort = null, $direction = null, $clear = true) |
||
685 | { |
||
686 | if ($clear) { |
||
687 | $this->query->setOrderBy($sort, $direction); |
||
688 | } else { |
||
689 | $this->query->addOrderBy($sort, $direction); |
||
690 | } |
||
691 | |||
692 | return $this; |
||
693 | } |
||
694 | |||
695 | /** |
||
696 | * Reverse order by clause |
||
697 | * |
||
698 | * @return $this |
||
699 | */ |
||
700 | public function reverseSort() |
||
701 | { |
||
702 | $this->query->reverseOrderBy(); |
||
703 | return $this; |
||
704 | } |
||
705 | |||
706 | /** |
||
707 | * Set the limit of this query. |
||
708 | * |
||
709 | * @param int $limit |
||
710 | * @param int $offset |
||
711 | * @return $this |
||
712 | */ |
||
713 | public function limit($limit, $offset = 0) |
||
714 | { |
||
715 | $this->query->setLimit($limit, $offset); |
||
716 | return $this; |
||
717 | } |
||
718 | |||
719 | /** |
||
720 | * Set whether this query should be distinct or not. |
||
721 | * |
||
722 | * @param bool $value |
||
723 | * @return $this |
||
724 | */ |
||
725 | public function distinct($value) |
||
726 | { |
||
727 | $this->query->setDistinct($value); |
||
728 | return $this; |
||
729 | } |
||
730 | |||
731 | /** |
||
732 | * Add an INNER JOIN clause to this query. |
||
733 | * |
||
734 | * @param string $table The unquoted table name to join to. |
||
735 | * @param string $onClause The filter for the join (escaped SQL statement) |
||
736 | * @param string $alias An optional alias name (unquoted) |
||
737 | * @param int $order A numerical index to control the order that joins are added to the query; lower order values |
||
738 | * will cause the query to appear first. The default is 20, and joins created automatically by the |
||
739 | * ORM have a value of 10. |
||
740 | * @param array $parameters Any additional parameters if the join is a parameterised subquery |
||
741 | * @return $this |
||
742 | */ |
||
743 | public function innerJoin($table, $onClause, $alias = null, $order = 20, $parameters = array()) |
||
744 | { |
||
745 | if ($table) { |
||
746 | $this->query->addInnerJoin($table, $onClause, $alias, $order, $parameters); |
||
747 | } |
||
748 | return $this; |
||
749 | } |
||
750 | |||
751 | /** |
||
752 | * Add a LEFT JOIN clause to this query. |
||
753 | * |
||
754 | * @param string $table The unquoted table to join to. |
||
755 | * @param string $onClause The filter for the join (escaped SQL statement). |
||
756 | * @param string $alias An optional alias name (unquoted) |
||
757 | * @param int $order A numerical index to control the order that joins are added to the query; lower order values |
||
758 | * will cause the query to appear first. The default is 20, and joins created automatically by the |
||
759 | * ORM have a value of 10. |
||
760 | * @param array $parameters Any additional parameters if the join is a parameterised subquery |
||
761 | * @return $this |
||
762 | */ |
||
763 | public function leftJoin($table, $onClause, $alias = null, $order = 20, $parameters = array()) |
||
764 | { |
||
765 | if ($table) { |
||
766 | $this->query->addLeftJoin($table, $onClause, $alias, $order, $parameters); |
||
767 | } |
||
768 | return $this; |
||
769 | } |
||
770 | |||
771 | /** |
||
772 | * Prefix of all joined table aliases. E.g. ->filter('Banner.Image.Title)' |
||
773 | * Will join the Banner, and then Image relations |
||
774 | * `$relationPrefx` will be `banner_image_` |
||
775 | * Each table in the Image chain will be suffixed to this prefix. E.g. |
||
776 | * `banner_image_File` and `banner_image_Image` |
||
777 | * |
||
778 | * This will be null if no relation is joined. |
||
779 | * E.g. `->filter('Title')` |
||
780 | * |
||
781 | * @param string|array $relation Relation in '.' delimited string, or array of parts |
||
782 | * @return string Table prefix |
||
783 | */ |
||
784 | public static function applyRelationPrefix($relation) |
||
785 | { |
||
786 | if (!$relation) { |
||
787 | return null; |
||
788 | } |
||
789 | if (is_string($relation)) { |
||
790 | $relation = explode(".", $relation); |
||
791 | } |
||
792 | return strtolower(implode('_', $relation)) . '_'; |
||
793 | } |
||
794 | |||
795 | /** |
||
796 | * Traverse the relationship fields, and add the table |
||
797 | * mappings to the query object state. This has to be called |
||
798 | * in any overloaded {@link SearchFilter->apply()} methods manually. |
||
799 | * |
||
800 | * Note, that in order to filter against the joined relation user code must |
||
801 | * use {@see tablePrefix()} to get the table alias used for this relation. |
||
802 | * |
||
803 | * @param string|array $relation The array/dot-syntax relation to follow |
||
804 | * @param bool $linearOnly Set to true to restrict to linear relations only. Set this |
||
805 | * if this relation will be used for sorting, and should not include duplicate rows. |
||
806 | * @return string The model class of the related item |
||
807 | */ |
||
808 | public function applyRelation($relation, $linearOnly = false) |
||
809 | { |
||
810 | // NO-OP |
||
811 | if (!$relation) { |
||
812 | return $this->dataClass; |
||
813 | } |
||
814 | |||
815 | if (is_string($relation)) { |
||
816 | $relation = explode(".", $relation); |
||
817 | } |
||
818 | |||
819 | $modelClass = $this->dataClass; |
||
820 | |||
821 | $schema = DataObject::getSchema(); |
||
822 | $currentRelation = []; |
||
823 | foreach ($relation as $rel) { |
||
824 | // Get prefix for join for this table (and parent to join on) |
||
825 | $parentPrefix = $this->applyRelationPrefix($currentRelation); |
||
826 | $currentRelation[] = $rel; |
||
827 | $tablePrefix = $this->applyRelationPrefix($currentRelation); |
||
828 | |||
829 | // Check has_one |
||
830 | if ($component = $schema->hasOneComponent($modelClass, $rel)) { |
||
831 | // Join via has_one |
||
832 | $this->joinHasOneRelation($modelClass, $rel, $component, $parentPrefix, $tablePrefix); |
||
833 | $modelClass = $component; |
||
834 | continue; |
||
835 | } |
||
836 | |||
837 | // Check has_many |
||
838 | if ($component = $schema->hasManyComponent($modelClass, $rel)) { |
||
839 | // Fail on non-linear relations |
||
840 | if ($linearOnly) { |
||
841 | throw new InvalidArgumentException("$rel is not a linear relation on model $modelClass"); |
||
842 | } |
||
843 | // Join via has_many |
||
844 | $this->joinHasManyRelation($modelClass, $rel, $component, $parentPrefix, $tablePrefix, 'has_many'); |
||
845 | $modelClass = $component; |
||
846 | continue; |
||
847 | } |
||
848 | |||
849 | // check belongs_to (like has_many but linear safe) |
||
850 | if ($component = $schema->belongsToComponent($modelClass, $rel)) { |
||
851 | // Piggy back off has_many logic |
||
852 | $this->joinHasManyRelation($modelClass, $rel, $component, $parentPrefix, $tablePrefix, 'belongs_to'); |
||
853 | $modelClass = $component; |
||
854 | continue; |
||
855 | } |
||
856 | |||
857 | // Check many_many |
||
858 | if ($component = $schema->manyManyComponent($modelClass, $rel)) { |
||
859 | // Fail on non-linear relations |
||
860 | if ($linearOnly) { |
||
861 | throw new InvalidArgumentException("$rel is not a linear relation on model $modelClass"); |
||
862 | } |
||
863 | $this->joinManyManyRelationship( |
||
864 | $component['relationClass'], |
||
865 | $component['parentClass'], |
||
866 | $component['childClass'], |
||
867 | $component['parentField'], |
||
868 | $component['childField'], |
||
869 | $component['join'], |
||
870 | $parentPrefix, |
||
871 | $tablePrefix |
||
872 | ); |
||
873 | $modelClass = $component['childClass']; |
||
874 | continue; |
||
875 | } |
||
876 | |||
877 | // no relation |
||
878 | throw new InvalidArgumentException("$rel is not a relation on model $modelClass"); |
||
879 | } |
||
880 | |||
881 | return $modelClass; |
||
882 | } |
||
883 | |||
884 | /** |
||
885 | * Join the given has_many relation to this query. |
||
886 | * Also works with belongs_to |
||
887 | * |
||
888 | * Doesn't work with polymorphic relationships |
||
889 | * |
||
890 | * @param string $localClass Name of class that has the has_many to the joined class |
||
891 | * @param string $localField Name of the has_many relationship to join |
||
892 | * @param string $foreignClass Class to join |
||
893 | * @param string $localPrefix Table prefix for parent class |
||
894 | * @param string $foreignPrefix Table prefix to use |
||
895 | * @param string $type 'has_many' or 'belongs_to' |
||
896 | */ |
||
897 | protected function joinHasManyRelation( |
||
898 | $localClass, |
||
899 | $localField, |
||
900 | $foreignClass, |
||
901 | $localPrefix = null, |
||
902 | $foreignPrefix = null, |
||
903 | $type = 'has_many' |
||
904 | ) { |
||
905 | if (!$foreignClass || $foreignClass === DataObject::class) { |
||
906 | throw new InvalidArgumentException("Could not find a has_many relationship {$localField} on {$localClass}"); |
||
907 | } |
||
908 | $schema = DataObject::getSchema(); |
||
909 | |||
910 | // Skip if already joined |
||
911 | // Note: don't just check base class, since we need to join on the table with the actual relation key |
||
912 | $foreignTable = $schema->tableName($foreignClass); |
||
913 | $foreignTableAliased = $foreignPrefix . $foreignTable; |
||
914 | if ($this->query->isJoinedTo($foreignTableAliased)) { |
||
915 | return; |
||
916 | } |
||
917 | |||
918 | // Join table with associated has_one |
||
919 | /** @var DataObject $model */ |
||
920 | $foreignKey = $schema->getRemoteJoinField($localClass, $localField, $type, $polymorphic); |
||
921 | $localIDColumn = $schema->sqlColumnForField($localClass, 'ID', $localPrefix); |
||
922 | if ($polymorphic) { |
||
923 | $foreignKeyIDColumn = $schema->sqlColumnForField($foreignClass, "{$foreignKey}ID", $foreignPrefix); |
||
924 | $foreignKeyClassColumn = $schema->sqlColumnForField($foreignClass, "{$foreignKey}Class", $foreignPrefix); |
||
925 | $localClassColumn = $schema->sqlColumnForField($localClass, 'ClassName', $localPrefix); |
||
926 | $joinExpression = |
||
927 | "{$foreignKeyIDColumn} = {$localIDColumn} AND {$foreignKeyClassColumn} = {$localClassColumn}"; |
||
928 | } else { |
||
929 | $foreignKeyIDColumn = $schema->sqlColumnForField($foreignClass, $foreignKey, $foreignPrefix); |
||
930 | $joinExpression = "{$foreignKeyIDColumn} = {$localIDColumn}"; |
||
931 | } |
||
932 | $this->query->addLeftJoin( |
||
933 | $foreignTable, |
||
934 | $joinExpression, |
||
935 | $foreignTableAliased |
||
936 | ); |
||
937 | |||
938 | // Add join clause to the component's ancestry classes so that the search filter could search on |
||
939 | // its ancestor fields. |
||
940 | $ancestry = ClassInfo::ancestry($foreignClass, true); |
||
941 | $ancestry = array_reverse($ancestry); |
||
942 | foreach ($ancestry as $ancestor) { |
||
943 | $ancestorTable = $schema->tableName($ancestor); |
||
944 | if ($ancestorTable !== $foreignTable) { |
||
945 | $ancestorTableAliased = $foreignPrefix . $ancestorTable; |
||
946 | $this->query->addLeftJoin( |
||
947 | $ancestorTable, |
||
948 | "\"{$foreignTableAliased}\".\"ID\" = \"{$ancestorTableAliased}\".\"ID\"", |
||
949 | $ancestorTableAliased |
||
950 | ); |
||
951 | } |
||
952 | } |
||
953 | } |
||
954 | |||
955 | /** |
||
956 | * Join the given class to this query with the given key |
||
957 | * |
||
958 | * @param string $localClass Name of class that has the has_one to the joined class |
||
959 | * @param string $localField Name of the has_one relationship to joi |
||
960 | * @param string $foreignClass Class to join |
||
961 | * @param string $localPrefix Table prefix to use for local class |
||
962 | * @param string $foreignPrefix Table prefix to use for joined table |
||
963 | */ |
||
964 | protected function joinHasOneRelation( |
||
965 | $localClass, |
||
966 | $localField, |
||
967 | $foreignClass, |
||
968 | $localPrefix = null, |
||
969 | $foreignPrefix = null |
||
970 | ) { |
||
971 | if (!$foreignClass) { |
||
972 | throw new InvalidArgumentException("Could not find a has_one relationship {$localField} on {$localClass}"); |
||
973 | } |
||
974 | |||
975 | if ($foreignClass === DataObject::class) { |
||
976 | throw new InvalidArgumentException( |
||
977 | "Could not join polymorphic has_one relationship {$localField} on {$localClass}" |
||
978 | ); |
||
979 | } |
||
980 | $schema = DataObject::getSchema(); |
||
981 | |||
982 | // Skip if already joined |
||
983 | $foreignBaseClass = $schema->baseDataClass($foreignClass); |
||
984 | $foreignBaseTable = $schema->tableName($foreignBaseClass); |
||
985 | if ($this->query->isJoinedTo($foreignPrefix . $foreignBaseTable)) { |
||
986 | return; |
||
987 | } |
||
988 | |||
989 | // Join base table |
||
990 | $foreignIDColumn = $schema->sqlColumnForField($foreignBaseClass, 'ID', $foreignPrefix); |
||
991 | $localColumn = $schema->sqlColumnForField($localClass, "{$localField}ID", $localPrefix); |
||
992 | $this->query->addLeftJoin( |
||
993 | $foreignBaseTable, |
||
994 | "{$foreignIDColumn} = {$localColumn}", |
||
995 | $foreignPrefix . $foreignBaseTable |
||
996 | ); |
||
997 | |||
998 | // Add join clause to the component's ancestry classes so that the search filter could search on |
||
999 | // its ancestor fields. |
||
1000 | $ancestry = ClassInfo::ancestry($foreignClass, true); |
||
1001 | if (!empty($ancestry)) { |
||
1002 | $ancestry = array_reverse($ancestry); |
||
1003 | foreach ($ancestry as $ancestor) { |
||
1004 | $ancestorTable = $schema->tableName($ancestor); |
||
1005 | if ($ancestorTable !== $foreignBaseTable) { |
||
1006 | $ancestorTableAliased = $foreignPrefix . $ancestorTable; |
||
1007 | $this->query->addLeftJoin( |
||
1008 | $ancestorTable, |
||
1009 | "{$foreignIDColumn} = \"{$ancestorTableAliased}\".\"ID\"", |
||
1010 | $ancestorTableAliased |
||
1011 | ); |
||
1012 | } |
||
1013 | } |
||
1014 | } |
||
1015 | } |
||
1016 | |||
1017 | /** |
||
1018 | * Join table via many_many relationship |
||
1019 | * |
||
1020 | * @param string $relationClass |
||
1021 | * @param string $parentClass |
||
1022 | * @param string $componentClass |
||
1023 | * @param string $parentField |
||
1024 | * @param string $componentField |
||
1025 | * @param string $relationClassOrTable Name of relation table |
||
1026 | * @param string $parentPrefix Table prefix for parent class |
||
1027 | * @param string $componentPrefix Table prefix to use for both joined and mapping table |
||
1028 | */ |
||
1029 | protected function joinManyManyRelationship( |
||
1030 | $relationClass, |
||
1031 | $parentClass, |
||
1032 | $componentClass, |
||
1033 | $parentField, |
||
1034 | $componentField, |
||
1035 | $relationClassOrTable, |
||
1036 | $parentPrefix = null, |
||
1037 | $componentPrefix = null |
||
1038 | ) { |
||
1039 | $schema = DataObject::getSchema(); |
||
1040 | |||
1041 | if (class_exists($relationClassOrTable)) { |
||
1042 | $relationClassOrTable = $schema->tableName($relationClassOrTable); |
||
1043 | } |
||
1044 | |||
1045 | // Check if already joined to component alias (skip join table for the check) |
||
1046 | $componentBaseClass = $schema->baseDataClass($componentClass); |
||
1047 | $componentBaseTable = $schema->tableName($componentBaseClass); |
||
1048 | $componentAliasedTable = $componentPrefix . $componentBaseTable; |
||
1049 | if ($this->query->isJoinedTo($componentAliasedTable)) { |
||
1050 | return; |
||
1051 | } |
||
1052 | |||
1053 | // Join parent class to join table |
||
1054 | $relationAliasedTable = $componentPrefix . $relationClassOrTable; |
||
1055 | $parentIDColumn = $schema->sqlColumnForField($parentClass, 'ID', $parentPrefix); |
||
1056 | $this->query->addLeftJoin( |
||
1057 | $relationClassOrTable, |
||
1058 | "\"{$relationAliasedTable}\".\"{$parentField}\" = {$parentIDColumn}", |
||
1059 | $relationAliasedTable |
||
1060 | ); |
||
1061 | |||
1062 | // Join on base table of component class |
||
1063 | $componentIDColumn = $schema->sqlColumnForField($componentBaseClass, 'ID', $componentPrefix); |
||
1064 | $this->query->addLeftJoin( |
||
1065 | $componentBaseTable, |
||
1066 | "\"{$relationAliasedTable}\".\"{$componentField}\" = {$componentIDColumn}", |
||
1067 | $componentAliasedTable |
||
1068 | ); |
||
1069 | |||
1070 | // Add join clause to the component's ancestry classes so that the search filter could search on |
||
1071 | // its ancestor fields. |
||
1072 | $ancestry = ClassInfo::ancestry($componentClass, true); |
||
1073 | $ancestry = array_reverse($ancestry); |
||
1074 | foreach ($ancestry as $ancestor) { |
||
1075 | $ancestorTable = $schema->tableName($ancestor); |
||
1076 | if ($ancestorTable !== $componentBaseTable) { |
||
1077 | $ancestorTableAliased = $componentPrefix . $ancestorTable; |
||
1078 | $this->query->addLeftJoin( |
||
1079 | $ancestorTable, |
||
1080 | "{$componentIDColumn} = \"{$ancestorTableAliased}\".\"ID\"", |
||
1081 | $ancestorTableAliased |
||
1082 | ); |
||
1083 | } |
||
1084 | } |
||
1085 | } |
||
1086 | |||
1087 | /** |
||
1088 | * Removes the result of query from this query. |
||
1089 | * |
||
1090 | * @param DataQuery $subtractQuery |
||
1091 | * @param string $field |
||
1092 | * @return $this |
||
1093 | */ |
||
1094 | public function subtract(DataQuery $subtractQuery, $field = 'ID') |
||
1105 | } |
||
1106 | |||
1107 | /** |
||
1108 | * Select the only given fields from the given table. |
||
1109 | * |
||
1110 | * @param string $table Unquoted table name (will be escaped automatically) |
||
1111 | * @param array $fields Database column names (will be escaped automatically) |
||
1112 | * @return $this |
||
1113 | */ |
||
1114 | public function selectFromTable($table, $fields) |
||
1115 | { |
||
1116 | $fieldExpressions = array_map(function ($item) use ($table) { |
||
1117 | return Convert::symbol2sql("{$table}.{$item}"); |
||
1118 | }, $fields); |
||
1119 | |||
1120 | $this->query->setSelect($fieldExpressions); |
||
1121 | |||
1122 | return $this; |
||
1123 | } |
||
1124 | |||
1125 | /** |
||
1126 | * Add the given fields from the given table to the select statement. |
||
1127 | * |
||
1128 | * @param string $table Unquoted table name (will be escaped automatically) |
||
1129 | * @param array $fields Database column names (will be escaped automatically) |
||
1130 | * @return $this |
||
1131 | */ |
||
1132 | public function addSelectFromTable($table, $fields) |
||
1133 | { |
||
1134 | $fieldExpressions = array_map(function ($item) use ($table) { |
||
1135 | return Convert::symbol2sql("{$table}.{$item}"); |
||
1136 | }, $fields); |
||
1137 | |||
1138 | $this->query->addSelect($fieldExpressions); |
||
1139 | |||
1140 | return $this; |
||
1141 | } |
||
1142 | |||
1143 | /** |
||
1144 | * Query the given field column from the database and return as an array. |
||
1145 | * |
||
1146 | * @param string $field See {@link expressionForField()}. |
||
1147 | * @return array List of column values for the specified column |
||
1148 | */ |
||
1149 | public function column($field = 'ID') |
||
1150 | { |
||
1151 | $fieldExpression = $this->expressionForField($field); |
||
1152 | $query = $this->getFinalisedQuery(array($field)); |
||
1153 | $originalSelect = $query->getSelect(); |
||
1154 | $query->setSelect(array()); |
||
1155 | $query->selectField($fieldExpression, $field); |
||
1156 | $this->ensureSelectContainsOrderbyColumns($query, $originalSelect); |
||
1157 | |||
1158 | return $query->execute()->column($field); |
||
1159 | } |
||
1160 | |||
1161 | /** |
||
1162 | * @param string $field Select statement identifier, either the unquoted column name, |
||
1163 | * the full composite SQL statement, or the alias set through {@link SQLSelect->selectField()}. |
||
1164 | * @return string The expression used to query this field via this DataQuery |
||
1165 | */ |
||
1166 | protected function expressionForField($field) |
||
1167 | { |
||
1168 | // Prepare query object for selecting this field |
||
1169 | $query = $this->getFinalisedQuery(array($field)); |
||
1170 | |||
1171 | // Allow query to define the expression for this field |
||
1172 | $expression = $query->expressionForField($field); |
||
1173 | if (!empty($expression)) { |
||
1174 | return $expression; |
||
1175 | } |
||
1176 | |||
1177 | // Special case for ID, if not provided |
||
1178 | if ($field === 'ID') { |
||
1179 | return DataObject::getSchema()->sqlColumnForField($this->dataClass, 'ID'); |
||
1180 | } |
||
1181 | return null; |
||
1182 | } |
||
1183 | |||
1184 | /** |
||
1185 | * Select the given field expressions. |
||
1186 | * |
||
1187 | * @param string $fieldExpression String The field to select (escaped SQL statement) |
||
1188 | * @param string $alias String The alias of that field (escaped SQL statement) |
||
1189 | */ |
||
1190 | public function selectField($fieldExpression, $alias = null) |
||
1191 | { |
||
1192 | $this->query->selectField($fieldExpression, $alias); |
||
1193 | } |
||
1194 | |||
1195 | //// QUERY PARAMS |
||
1196 | |||
1197 | /** |
||
1198 | * An arbitrary store of query parameters that can be used by decorators. |
||
1199 | */ |
||
1200 | private $queryParams; |
||
1201 | |||
1202 | /** |
||
1203 | * Set an arbitrary query parameter, that can be used by decorators to add additional meta-data to the query. |
||
1204 | * It's expected that the $key will be namespaced, e.g, 'Versioned.stage' instead of just 'stage'. |
||
1205 | * |
||
1206 | * @param string $key |
||
1207 | * @param string|array $value |
||
1208 | * @return $this |
||
1209 | */ |
||
1210 | public function setQueryParam($key, $value) |
||
1211 | { |
||
1212 | $this->queryParams[$key] = $value; |
||
1213 | return $this; |
||
1214 | } |
||
1215 | |||
1216 | /** |
||
1217 | * Set an arbitrary query parameter, that can be used by decorators to add additional meta-data to the query. |
||
1218 | * |
||
1219 | * @param string $key |
||
1220 | * @return string |
||
1221 | */ |
||
1222 | public function getQueryParam($key) |
||
1223 | { |
||
1224 | if (isset($this->queryParams[$key])) { |
||
1225 | return $this->queryParams[$key]; |
||
1226 | } |
||
1227 | return null; |
||
1228 | } |
||
1229 | |||
1230 | /** |
||
1231 | * Returns all query parameters |
||
1232 | * @return array query parameters array |
||
1233 | */ |
||
1234 | public function getQueryParams() |
||
1237 | } |
||
1238 | |||
1239 | /** |
||
1240 | * Get query manipulators |
||
1241 | * |
||
1242 | * @return DataQueryManipulator[] |
||
1243 | */ |
||
1244 | public function getDataQueryManipulators() |
||
1245 | { |
||
1247 | } |
||
1248 | |||
1249 | /** |
||
1250 | * Assign callback to be invoked in getFinalisedQuery() |
||
1251 | * |
||
1252 | * @param DataQueryManipulator $manipulator |
||
1253 | * @return $this |
||
1254 | */ |
||
1255 | public function pushQueryManipulator(DataQueryManipulator $manipulator) |
||
1256 | { |
||
1257 | $this->dataQueryManipulators[] = $manipulator; |
||
1258 | return $this; |
||
1259 | } |
||
1260 | } |
||
1261 |