Passed
Pull Request — 4 (#10222)
by Steve
07:01
created

DataQuery::joinManyManyRelationship()   B

Complexity

Conditions 6
Paths 8

Size

Total Lines 59
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 33
nc 8
nop 8
dl 0
loc 59
rs 8.7697
c 0
b 0
f 0

How to fix   Long Method    Many Parameters   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

1072
        /** @scrutinizer ignore-unused */ $relationClass,

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

Loading history...
1073
        $parentClass,
1074
        $componentClass,
1075
        $parentField,
1076
        $componentField,
1077
        $relationClassOrTable,
1078
        $parentPrefix = null,
1079
        $componentPrefix = null
1080
    ) {
1081
        $schema = DataObject::getSchema();
1082
1083
        if (class_exists((string) $relationClassOrTable)) {
1084
            // class is provided
1085
            $relationTable = $schema->tableName($relationClassOrTable);
1086
            $relationTableUpdated = $this->getJoinTableName($relationClassOrTable, $relationTable);
1087
        } else {
1088
            // table is provided
1089
            $relationTable = $relationClassOrTable;
1090
            $relationTableUpdated = $relationClassOrTable;
1091
        }
1092
1093
        // Check if already joined to component alias (skip join table for the check)
1094
        $componentBaseClass = $schema->baseDataClass($componentClass);
1095
        $componentBaseTable = $schema->tableName($componentBaseClass);
1096
        $componentAliasedTable = $componentPrefix . $componentBaseTable;
1097
        if ($this->query->isJoinedTo($componentAliasedTable)) {
1098
            return;
1099
        }
1100
1101
        // Join parent class to join table
1102
        $relationAliasedTable = $componentPrefix . $relationTable;
1103
        $parentIDColumn = $schema->sqlColumnForField($parentClass, 'ID', $parentPrefix);
1104
        $this->query->addLeftJoin(
1105
            $relationTableUpdated,
1106
            "\"{$relationAliasedTable}\".\"{$parentField}\" = {$parentIDColumn}",
1107
            $relationAliasedTable
1108
        );
1109
1110
        // Join on base table of component class
1111
        $componentIDColumn = $schema->sqlColumnForField($componentBaseClass, 'ID', $componentPrefix);
1112
            $this->query->addLeftJoin(
1113
                $this->getJoinTableName($componentBaseClass, $componentBaseTable),
1114
                "\"{$relationAliasedTable}\".\"{$componentField}\" = {$componentIDColumn}",
1115
                $componentAliasedTable
1116
            );
1117
1118
        // Add join clause to the component's ancestry classes so that the search filter could search on
1119
        // its ancestor fields.
1120
        $ancestry = ClassInfo::ancestry($componentClass, true);
1121
        $ancestry = array_reverse($ancestry ?: []);
1122
        foreach ($ancestry as $ancestor) {
1123
            $ancestorTable = $schema->tableName($ancestor);
1124
            if ($ancestorTable !== $componentBaseTable) {
1125
                $ancestorTableAliased = $componentPrefix . $ancestorTable;
1126
                $this->query->addLeftJoin(
1127
                    $this->getJoinTableName($ancestor, $ancestorTable),
1128
                    "{$componentIDColumn} = \"{$ancestorTableAliased}\".\"ID\"",
1129
                    $ancestorTableAliased
1130
                );
1131
            }
1132
        }
1133
    }
1134
1135
    /**
1136
     * Removes the result of query from this query.
1137
     *
1138
     * @param DataQuery $subtractQuery
1139
     * @param string $field
1140
     * @return $this
1141
     */
1142
    public function subtract(DataQuery $subtractQuery, $field = 'ID')
1143
    {
1144
        $fieldExpression = $subtractQuery->expressionForField($field);
1145
        $subSelect = $subtractQuery->getFinalisedQuery();
1146
        $subSelect->setSelect([]);
1147
        $subSelect->selectField($fieldExpression, $field);
1148
        $subSelect->setOrderBy(null);
1149
        $subSelectSQL = $subSelect->sql($subSelectParameters);
1150
        $this->where([$this->expressionForField($field) . " NOT IN ($subSelectSQL)" => $subSelectParameters]);
1151
1152
        return $this;
1153
    }
1154
1155
    /**
1156
     * Select the only given fields from the given table.
1157
     *
1158
     * @param string $table Unquoted table name (will be escaped automatically)
1159
     * @param array $fields Database column names (will be escaped automatically)
1160
     * @return $this
1161
     */
1162
    public function selectFromTable($table, $fields)
1163
    {
1164
        $fieldExpressions = array_map(function ($item) use ($table) {
1165
            return Convert::symbol2sql("{$table}.{$item}");
1166
        }, $fields ?: []);
1167
1168
        $this->query->setSelect($fieldExpressions);
1169
1170
        return $this;
1171
    }
1172
1173
    /**
1174
     * Add the given fields from the given table to the select statement.
1175
     *
1176
     * @param string $table Unquoted table name (will be escaped automatically)
1177
     * @param array $fields Database column names (will be escaped automatically)
1178
     * @return $this
1179
     */
1180
    public function addSelectFromTable($table, $fields)
1181
    {
1182
        $fieldExpressions = array_map(function ($item) use ($table) {
1183
            return Convert::symbol2sql("{$table}.{$item}");
1184
        }, $fields ?: []);
1185
1186
        $this->query->addSelect($fieldExpressions);
1187
1188
        return $this;
1189
    }
1190
1191
    /**
1192
     * Query the given field column from the database and return as an array.
1193
     * querying DB columns of related tables is supported but you need to make sure that the related table
1194
     * is already available in join
1195
     *
1196
     * @see DataList::applyRelation()
1197
     *
1198
     * example use:
1199
     *
1200
     * <code>
1201
     *  column("MyTable"."Title")
1202
     *
1203
     *  or
1204
     *
1205
     *  $columnName = null;
1206
     *  Category::get()
1207
     *    ->applyRelation('Products.Title', $columnName)
1208
     *    ->column($columnName);
1209
     * </code>
1210
     *
1211
     * @param string $field See {@link expressionForField()}.
1212
     * @return array List of column values for the specified column
1213
     * @throws InvalidArgumentException
1214
     */
1215
    public function column($field = 'ID')
1216
    {
1217
        $fieldExpression = $this->expressionForField($field);
1218
        $query = $this->getFinalisedQuery([$field]);
1219
        $originalSelect = $query->getSelect();
1220
        $query->setSelect([]);
1221
1222
        // field wasn't recognised as a valid field from the table class hierarchy
1223
        // check if the field is in format "<table_name>"."<column_name>"
1224
        // if that's the case we may want to query related table
1225
        if (!$fieldExpression) {
1226
            if (!$this->validateColumnField($field, $query)) {
1227
                throw new InvalidArgumentException('Invalid column name ' . $field);
1228
            }
1229
1230
            $fieldExpression = $field;
1231
            $field = null;
1232
        }
1233
1234
        $query->selectField($fieldExpression, $field);
1235
        $this->ensureSelectContainsOrderbyColumns($query, $originalSelect);
1236
1237
        return $query->execute()->column($field);
1238
    }
1239
1240
    /**
1241
     * @param string $field Select statement identifier, either the unquoted column name,
1242
     * the full composite SQL statement, or the alias set through {@link SQLSelect->selectField()}.
1243
     * @return string The expression used to query this field via this DataQuery
1244
     */
1245
    protected function expressionForField($field)
1246
    {
1247
        // Prepare query object for selecting this field
1248
        $query = $this->getFinalisedQuery([$field]);
1249
1250
        // Allow query to define the expression for this field
1251
        $expression = $query->expressionForField($field);
1252
        if (!empty($expression)) {
1253
            return $expression;
1254
        }
1255
1256
        // Special case for ID, if not provided
1257
        if ($field === 'ID') {
1258
            return DataObject::getSchema()->sqlColumnForField($this->dataClass, 'ID');
1259
        }
1260
        return null;
1261
    }
1262
1263
    /**
1264
     * Select the given field expressions.
1265
     *
1266
     * @param string $fieldExpression String The field to select (escaped SQL statement)
1267
     * @param string $alias String The alias of that field (escaped SQL statement)
1268
     */
1269
    public function selectField($fieldExpression, $alias = null)
1270
    {
1271
        $this->query->selectField($fieldExpression, $alias);
1272
    }
1273
1274
    //// QUERY PARAMS
1275
1276
    /**
1277
     * An arbitrary store of query parameters that can be used by decorators.
1278
     */
1279
    private $queryParams;
1280
1281
    /**
1282
     * Set an arbitrary query parameter, that can be used by decorators to add additional meta-data to the query.
1283
     * It's expected that the $key will be namespaced, e.g, 'Versioned.stage' instead of just 'stage'.
1284
     *
1285
     * @param string $key
1286
     * @param string|array $value
1287
     * @return $this
1288
     */
1289
    public function setQueryParam($key, $value)
1290
    {
1291
        $this->queryParams[$key] = $value;
1292
        return $this;
1293
    }
1294
1295
    /**
1296
     * Set an arbitrary query parameter, that can be used by decorators to add additional meta-data to the query.
1297
     *
1298
     * @param string $key
1299
     * @return string
1300
     */
1301
    public function getQueryParam($key)
1302
    {
1303
        if (isset($this->queryParams[$key])) {
1304
            return $this->queryParams[$key];
1305
        }
1306
        return null;
1307
    }
1308
1309
    /**
1310
     * Returns all query parameters
1311
     * @return array query parameters array
1312
     */
1313
    public function getQueryParams()
1314
    {
1315
        return $this->queryParams;
1316
    }
1317
1318
    /**
1319
     * Get query manipulators
1320
     *
1321
     * @return DataQueryManipulator[]
1322
     */
1323
    public function getDataQueryManipulators()
1324
    {
1325
        return $this->dataQueryManipulators;
1326
    }
1327
1328
    /**
1329
     * Assign callback to be invoked in getFinalisedQuery()
1330
     *
1331
     * @param DataQueryManipulator $manipulator
1332
     * @return $this
1333
     */
1334
    public function pushQueryManipulator(DataQueryManipulator $manipulator)
1335
    {
1336
        $this->dataQueryManipulators[] = $manipulator;
1337
        return $this;
1338
    }
1339
1340
    private function validateColumnField($field, SQLSelect $query)
1341
    {
1342
        // standard column - nothing to process here
1343
        if (strpos((string) $field, '.') === false) {
1344
            return false;
1345
        }
1346
1347
        $fieldData = explode('.', (string) $field);
1348
        $tablePrefix = str_replace('"', '', $fieldData[0]);
1349
1350
        // check if related table is available
1351
        return $query->isJoinedTo($tablePrefix);
1352
    }
1353
1354
    /**
1355
     * Use this extension point to alter the table name
1356
     * useful for versioning for example
1357
     *
1358
     * @param $class
1359
     * @param $table
1360
     * @return mixed
1361
     */
1362
    private function getJoinTableName($class, $table)
1363
    {
1364
        $updated = $table;
1365
        $this->invokeWithExtensions('updateJoinTableName', $class, $table, $updated);
1366
1367
        return $updated;
1368
    }
1369
}
1370