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

DataQuery::query()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
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