Passed
Push — trunk ( f65a85...934bc2 )
by Christian
22:12 queued 08:53
created

QueryBuilder   F

Complexity

Total Complexity 141

Size/Duplication

Total Lines 1446
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 325
c 1
b 0
f 0
dl 0
loc 1446
rs 2
wmc 141

60 Methods

Rating   Name   Duplication   Size   Complexity  
B getSQL() 0 33 7
A getMaxResults() 0 3 1
A values() 0 3 1
A isLimitQuery() 0 3 2
A join() 0 3 1
A getParameterTypes() 0 3 1
A andWhere() 0 16 4
A setValue() 0 5 1
A resetQueryParts() 0 11 3
A addGroupBy() 0 18 4
A getQueryPart() 0 3 1
A __construct() 0 3 1
A getSQLForUpdate() 0 8 3
A getState() 0 3 1
A getConnection() 0 3 1
A select() 0 20 4
A setParameter() 0 9 2
A where() 0 7 3
A getFromClauses() 0 23 3
B __clone() 0 22 8
A verifyAllAliasesAreKnown() 0 5 3
A executeQuery() 0 6 1
A getFirstResult() 0 3 1
A groupBy() 0 18 4
A getQueryParts() 0 3 1
A getSQLForInsert() 0 5 1
A createPositionalParameter() 0 6 1
A setFirstResult() 0 6 1
A getSQLForJoins() 0 28 6
A leftJoin() 0 10 1
A setMaxResults() 0 6 1
A getParameterType() 0 3 1
A setParameters() 0 6 1
A update() 0 11 2
A executeStatement() 0 3 1
A innerJoin() 0 10 1
A orderBy() 0 3 2
A execute() 0 9 2
A addOrderBy() 0 3 2
A delete() 0 11 2
A getParameters() 0 3 1
A expr() 0 3 1
A from() 0 6 1
A getSQLForDelete() 0 7 3
A orWhere() 0 16 4
A resetQueryPart() 0 7 1
A rightJoin() 0 10 1
A addSelect() 0 20 4
A andHaving() 0 14 3
A __toString() 0 3 1
C add() 0 36 12
A getType() 0 3 1
A distinct() 0 5 1
A createNamedParameter() 0 10 2
A insert() 0 9 2
A set() 0 3 1
A having() 0 7 3
B getSQLForSelect() 0 20 8
A getParameter() 0 3 1
A orHaving() 0 14 3

How to fix   Complexity   

Complex Class

Complex classes like QueryBuilder often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

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

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

1
<?php declare(strict_types=1);
2
3
/**
4
 * Forward compatibility adaption to add `executeQuery` and `executeStatement` methods to the Query Builder
5
 */
6
7
namespace Doctrine\DBAL\Query;
8
9
use Doctrine\DBAL\Connection;
10
use Doctrine\DBAL\Exception;
11
use Doctrine\DBAL\ForwardCompatibility;
12
use Doctrine\DBAL\ParameterType;
13
use Doctrine\DBAL\Query\Expression\CompositeExpression;
14
use Doctrine\DBAL\Query\Expression\ExpressionBuilder;
15
use Doctrine\DBAL\Result;
16
use Doctrine\DBAL\Types\Type;
17
use Doctrine\Deprecations\Deprecation;
18
use function array_filter;
19
use function array_keys;
20
use function array_unshift;
21
use function implode;
22
use function key;
23
use function strtoupper;
24
use function substr;
25
26
/**
27
 * QueryBuilder class is responsible to dynamically create SQL queries.
28
 *
29
 * Important: Verify that every feature you use will work with your database vendor.
30
 * SQL Query Builder does not attempt to validate the generated SQL at all.
31
 *
32
 * The query builder does no validation whatsoever if certain features even work with the
33
 * underlying database vendor. Limit queries and joins are NOT applied to UPDATE and DELETE statements
34
 * even if some vendors such as MySQL support it.
35
 */
36
class QueryBuilder
37
{
38
    /*
39
     * The query types.
40
     */
41
    public const SELECT = 0;
42
    public const DELETE = 1;
43
    public const UPDATE = 2;
44
    public const INSERT = 3;
45
46
    /*
47
     * The builder states.
48
     */
49
    public const STATE_DIRTY = 0;
50
    public const STATE_CLEAN = 1;
51
52
    /*
53
     * The default values of SQL parts collection
54
     */
55
    private const SQL_PARTS_DEFAULTS = [
56
        'select' => [],
57
        'distinct' => false,
58
        'from' => [],
59
        'join' => [],
60
        'set' => [],
61
        'where' => null,
62
        'groupBy' => [],
63
        'having' => null,
64
        'orderBy' => [],
65
        'values' => [],
66
    ];
67
68
    /**
69
     * The DBAL Connection.
70
     *
71
     * @var Connection
72
     */
73
    private $connection;
74
75
    /**
76
     * The array of SQL parts collected.
77
     *
78
     * @var mixed[]
79
     */
80
    private $sqlParts = self::SQL_PARTS_DEFAULTS;
81
82
    /**
83
     * The complete SQL string for this query.
84
     *
85
     * @var string|null
86
     */
87
    private $sql;
88
89
    /**
90
     * The query parameters.
91
     *
92
     * @var array<int, mixed>|array<string, mixed>
93
     */
94
    private $params = [];
95
96
    /**
97
     * The parameter type map of this query.
98
     *
99
     * @var array<int, int|string|Type|null>|array<string, int|string|Type|null>
100
     */
101
    private $paramTypes = [];
102
103
    /**
104
     * The type of query this is. Can be select, update or delete.
105
     *
106
     * @var int
107
     */
108
    private $type = self::SELECT;
109
110
    /**
111
     * The state of the query object. Can be dirty or clean.
112
     *
113
     * @var int
114
     */
115
    private $state = self::STATE_CLEAN;
116
117
    /**
118
     * The index of the first result to retrieve.
119
     *
120
     * @var int
121
     */
122
    private $firstResult = 0;
123
124
    /**
125
     * The maximum number of results to retrieve or NULL to retrieve all results.
126
     *
127
     * @var int|null
128
     */
129
    private $maxResults;
130
131
    /**
132
     * The counter of bound parameters used with {@see bindValue).
133
     *
134
     * @var int
135
     */
136
    private $boundCounter = 0;
137
138
    /**
139
     * Initializes a new <tt>QueryBuilder</tt>.
140
     *
141
     * @param Connection $connection The DBAL Connection.
142
     */
143
    public function __construct(Connection $connection)
144
    {
145
        $this->connection = $connection;
146
    }
147
148
    /**
149
     * Gets a string representation of this QueryBuilder which corresponds to
150
     * the final SQL query being constructed.
151
     *
152
     * @return string The string representation of this QueryBuilder.
153
     */
154
    public function __toString()
155
    {
156
        return $this->getSQL();
157
    }
158
159
    /**
160
     * Deep clone of all expression objects in the SQL parts.
161
     *
162
     * @return void
163
     */
164
    public function __clone()
165
    {
166
        foreach ($this->sqlParts as $part => $elements) {
167
            if (\is_array($this->sqlParts[$part])) {
168
                foreach ($this->sqlParts[$part] as $idx => $element) {
169
                    if (!\is_object($element)) {
170
                        continue;
171
                    }
172
173
                    $this->sqlParts[$part][$idx] = clone $element;
174
                }
175
            } elseif (\is_object($elements)) {
176
                $this->sqlParts[$part] = clone $elements;
177
            }
178
        }
179
180
        foreach ($this->params as $name => $param) {
181
            if (!\is_object($param)) {
182
                continue;
183
            }
184
185
            $this->params[$name] = clone $param;
186
        }
187
    }
188
189
    /**
190
     * Gets an ExpressionBuilder used for object-oriented construction of query expressions.
191
     * This producer method is intended for convenient inline usage. Example:
192
     *
193
     * <code>
194
     *     $qb = $conn->createQueryBuilder()
195
     *         ->select('u')
196
     *         ->from('users', 'u')
197
     *         ->where($qb->expr()->eq('u.id', 1));
198
     * </code>
199
     *
200
     * For more complex expression construction, consider storing the expression
201
     * builder object in a local variable.
202
     *
203
     * @return ExpressionBuilder
204
     */
205
    public function expr()
206
    {
207
        return $this->connection->getExpressionBuilder();
208
    }
209
210
    /**
211
     * Gets the type of the currently built query.
212
     *
213
     * @return int
214
     */
215
    public function getType()
216
    {
217
        return $this->type;
218
    }
219
220
    /**
221
     * Gets the associated DBAL Connection for this query builder.
222
     *
223
     * @return Connection
224
     */
225
    public function getConnection()
226
    {
227
        return $this->connection;
228
    }
229
230
    /**
231
     * Gets the state of this query builder instance.
232
     *
233
     * @return int Either QueryBuilder::STATE_DIRTY or QueryBuilder::STATE_CLEAN.
234
     */
235
    public function getState()
236
    {
237
        return $this->state;
238
    }
239
240
    /**
241
     * Executes an SQL query (SELECT) and returns a Result.
242
     *
243
     * @throws Exception
244
     */
245
    public function executeQuery(): Result
246
    {
247
        return $this->connection->executeQuery(
248
            $this->getSQL(),
249
            $this->params,
250
            $this->paramTypes
251
        );
252
    }
253
254
    /**
255
     * Executes an SQL statement and returns the number of affected rows.
256
     *
257
     * Should be used for INSERT, UPDATE and DELETE
258
     *
259
     * @throws Exception
260
     *
261
     * @return int The number of affected rows.
262
     */
263
    public function executeStatement(): int
264
    {
265
        return (int) $this->connection->executeStatement($this->getSQL(), $this->params, $this->paramTypes);
266
    }
267
268
    /**
269
     * Executes this query using the bound parameters and their types.
270
     *
271
     * @throws Exception
272
     *
273
     * @return ForwardCompatibility\Result<mixed>|int|string
274
     */
275
    public function execute()
276
    {
277
        if ($this->type === self::SELECT) {
278
            return ForwardCompatibility\Result::ensure(
279
                $this->connection->executeQuery($this->getSQL(), $this->params, $this->paramTypes)
280
            );
281
        }
282
283
        return $this->connection->executeStatement($this->getSQL(), $this->params, $this->paramTypes);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->connection...ams, $this->paramTypes) returns the type integer|string which is incompatible with the documented return type Doctrine\DBAL\ForwardCompatibility\Result.
Loading history...
284
    }
285
286
    /**
287
     * Gets the complete SQL string formed by the current specifications of this QueryBuilder.
288
     *
289
     * <code>
290
     *     $qb = $em->createQueryBuilder()
291
     *         ->select('u')
292
     *         ->from('User', 'u')
293
     *     echo $qb->getSQL(); // SELECT u FROM User u
294
     * </code>
295
     *
296
     * @return string The SQL query string.
297
     */
298
    public function getSQL()
299
    {
300
        if ($this->sql !== null && $this->state === self::STATE_CLEAN) {
301
            return $this->sql;
302
        }
303
304
        switch ($this->type) {
305
            case self::INSERT:
306
                $sql = $this->getSQLForInsert();
307
308
                break;
309
310
            case self::DELETE:
311
                $sql = $this->getSQLForDelete();
312
313
                break;
314
315
            case self::UPDATE:
316
                $sql = $this->getSQLForUpdate();
317
318
                break;
319
320
            case self::SELECT:
321
            default:
322
                $sql = $this->getSQLForSelect();
323
324
                break;
325
        }
326
327
        $this->state = self::STATE_CLEAN;
328
        $this->sql = $sql;
329
330
        return $sql;
331
    }
332
333
    /**
334
     * Sets a query parameter for the query being constructed.
335
     *
336
     * <code>
337
     *     $qb = $conn->createQueryBuilder()
338
     *         ->select('u')
339
     *         ->from('users', 'u')
340
     *         ->where('u.id = :user_id')
341
     *         ->setParameter(':user_id', 1);
342
     * </code>
343
     *
344
     * @param int|string           $key   Parameter position or name
345
     * @param mixed                $value Parameter value
346
     * @param int|string|Type|null $type  Parameter type
347
     *
348
     * @return $this This QueryBuilder instance.
349
     */
350
    public function setParameter($key, $value, $type = null)
351
    {
352
        if ($type !== null) {
353
            $this->paramTypes[$key] = $type;
354
        }
355
356
        $this->params[$key] = $value;
357
358
        return $this;
359
    }
360
361
    /**
362
     * Sets a collection of query parameters for the query being constructed.
363
     *
364
     * <code>
365
     *     $qb = $conn->createQueryBuilder()
366
     *         ->select('u')
367
     *         ->from('users', 'u')
368
     *         ->where('u.id = :user_id1 OR u.id = :user_id2')
369
     *         ->setParameters(array(
370
     *             ':user_id1' => 1,
371
     *             ':user_id2' => 2
372
     *         ));
373
     * </code>
374
     *
375
     * @param array<int, mixed>|array<string, mixed>                               $params Parameters to set
376
     * @param array<int, int|string|Type|null>|array<string, int|string|Type|null> $types  Parameter types
377
     *
378
     * @return $this This QueryBuilder instance.
379
     */
380
    public function setParameters(array $params, array $types = [])
381
    {
382
        $this->paramTypes = $types;
383
        $this->params = $params;
384
385
        return $this;
386
    }
387
388
    /**
389
     * Gets all defined query parameters for the query being constructed indexed by parameter index or name.
390
     *
391
     * @return array<int, mixed>|array<string, mixed> The currently defined query parameters
392
     */
393
    public function getParameters()
394
    {
395
        return $this->params;
396
    }
397
398
    /**
399
     * Gets a (previously set) query parameter of the query being constructed.
400
     *
401
     * @param mixed $key The key (index or name) of the bound parameter.
402
     *
403
     * @return mixed The value of the bound parameter.
404
     */
405
    public function getParameter($key)
406
    {
407
        return $this->params[$key] ?? null;
408
    }
409
410
    /**
411
     * Gets all defined query parameter types for the query being constructed indexed by parameter index or name.
412
     *
413
     * @return array<int, int|string|Type|null>|array<string, int|string|Type|null> The currently defined
414
     *                                                                              query parameter types
415
     */
416
    public function getParameterTypes()
417
    {
418
        return $this->paramTypes;
419
    }
420
421
    /**
422
     * Gets a (previously set) query parameter type of the query being constructed.
423
     *
424
     * @param int|string $key The key of the bound parameter type
425
     *
426
     * @return int|string|Type|null The value of the bound parameter type
427
     */
428
    public function getParameterType($key)
429
    {
430
        return $this->paramTypes[$key] ?? null;
431
    }
432
433
    /**
434
     * Sets the position of the first result to retrieve (the "offset").
435
     *
436
     * @param int $firstResult The first result to return.
437
     *
438
     * @return $this This QueryBuilder instance.
439
     */
440
    public function setFirstResult($firstResult)
441
    {
442
        $this->state = self::STATE_DIRTY;
443
        $this->firstResult = $firstResult;
444
445
        return $this;
446
    }
447
448
    /**
449
     * Gets the position of the first result the query object was set to retrieve (the "offset").
450
     *
451
     * @return int The position of the first result.
452
     */
453
    public function getFirstResult()
454
    {
455
        return $this->firstResult;
456
    }
457
458
    /**
459
     * Sets the maximum number of results to retrieve (the "limit").
460
     *
461
     * @param int|null $maxResults The maximum number of results to retrieve or NULL to retrieve all results.
462
     *
463
     * @return $this This QueryBuilder instance.
464
     */
465
    public function setMaxResults($maxResults)
466
    {
467
        $this->state = self::STATE_DIRTY;
468
        $this->maxResults = $maxResults;
469
470
        return $this;
471
    }
472
473
    /**
474
     * Gets the maximum number of results the query object was set to retrieve (the "limit").
475
     * Returns NULL if all results will be returned.
476
     *
477
     * @return int|null The maximum number of results.
478
     */
479
    public function getMaxResults()
480
    {
481
        return $this->maxResults;
482
    }
483
484
    /**
485
     * Either appends to or replaces a single, generic query part.
486
     *
487
     * The available parts are: 'select', 'from', 'set', 'where',
488
     * 'groupBy', 'having' and 'orderBy'.
489
     *
490
     * @param string $sqlPartName
491
     * @param mixed  $sqlPart
492
     * @param bool   $append
493
     *
494
     * @return $this This QueryBuilder instance.
495
     */
496
    public function add($sqlPartName, $sqlPart, $append = false)
497
    {
498
        $isArray = \is_array($sqlPart);
499
        $isMultiple = \is_array($this->sqlParts[$sqlPartName]);
500
501
        if ($isMultiple && !$isArray) {
502
            $sqlPart = [$sqlPart];
503
        }
504
505
        $this->state = self::STATE_DIRTY;
506
507
        if ($append) {
508
            if (
509
                $sqlPartName === 'orderBy'
510
                || $sqlPartName === 'groupBy'
511
                || $sqlPartName === 'select'
512
                || $sqlPartName === 'set'
513
            ) {
514
                foreach ($sqlPart as $part) {
515
                    $this->sqlParts[$sqlPartName][] = $part;
516
                }
517
            } elseif ($isArray && \is_array($sqlPart[key($sqlPart)])) {
518
                $key = key($sqlPart);
519
                $this->sqlParts[$sqlPartName][$key][] = $sqlPart[$key];
520
            } elseif ($isMultiple) {
521
                $this->sqlParts[$sqlPartName][] = $sqlPart;
522
            } else {
523
                $this->sqlParts[$sqlPartName] = $sqlPart;
524
            }
525
526
            return $this;
527
        }
528
529
        $this->sqlParts[$sqlPartName] = $sqlPart;
530
531
        return $this;
532
    }
533
534
    /**
535
     * Specifies an item that is to be returned in the query result.
536
     * Replaces any previously specified selections, if any.
537
     *
538
     * USING AN ARRAY ARGUMENT IS DEPRECATED. Pass each value as an individual argument.
539
     *
540
     * <code>
541
     *     $qb = $conn->createQueryBuilder()
542
     *         ->select('u.id', 'p.id')
543
     *         ->from('users', 'u')
544
     *         ->leftJoin('u', 'phonenumbers', 'p', 'u.id = p.user_id');
545
     * </code>
546
     *
547
     * @param string|string[]|null $select The selection expression. USING AN ARRAY OR NULL IS DEPRECATED.
548
     *                                     Pass each value as an individual argument.
549
     *
550
     * @return $this This QueryBuilder instance.
551
     */
552
    public function select($select = null/*, string ...$selects*/)
553
    {
554
        $this->type = self::SELECT;
555
556
        if (empty($select)) {
557
            return $this;
558
        }
559
560
        if (\is_array($select)) {
561
            Deprecation::trigger(
562
                'doctrine/dbal',
563
                'https://github.com/doctrine/dbal/issues/3837',
564
                'Passing an array for the first argument to QueryBuilder::select is deprecated, '
565
                . 'pass each value as an individual variadic argument instead.'
566
            );
567
        }
568
569
        $selects = \is_array($select) ? $select : \func_get_args();
570
571
        return $this->add('select', $selects);
572
    }
573
574
    /**
575
     * Adds DISTINCT to the query.
576
     *
577
     * <code>
578
     *     $qb = $conn->createQueryBuilder()
579
     *         ->select('u.id')
580
     *         ->distinct()
581
     *         ->from('users', 'u')
582
     * </code>
583
     *
584
     * @return $this This QueryBuilder instance.
585
     */
586
    public function distinct(): self
587
    {
588
        $this->sqlParts['distinct'] = true;
589
590
        return $this;
591
    }
592
593
    /**
594
     * Adds an item that is to be returned in the query result.
595
     *
596
     * USING AN ARRAY ARGUMENT IS DEPRECATED. Pass each value as an individual argument.
597
     *
598
     * <code>
599
     *     $qb = $conn->createQueryBuilder()
600
     *         ->select('u.id')
601
     *         ->addSelect('p.id')
602
     *         ->from('users', 'u')
603
     *         ->leftJoin('u', 'phonenumbers', 'u.id = p.user_id');
604
     * </code>
605
     *
606
     * @param string|string[]|null $select The selection expression. USING AN ARRAY OR NULL IS DEPRECATED.
607
     *                                     Pass each value as an individual argument.
608
     *
609
     * @return $this This QueryBuilder instance.
610
     */
611
    public function addSelect($select = null/*, string ...$selects*/)
612
    {
613
        $this->type = self::SELECT;
614
615
        if (empty($select)) {
616
            return $this;
617
        }
618
619
        if (\is_array($select)) {
620
            Deprecation::trigger(
621
                'doctrine/dbal',
622
                'https://github.com/doctrine/dbal/issues/3837',
623
                'Passing an array for the first argument to QueryBuilder::addSelect is deprecated, '
624
                . 'pass each value as an individual variadic argument instead.'
625
            );
626
        }
627
628
        $selects = \is_array($select) ? $select : \func_get_args();
629
630
        return $this->add('select', $selects, true);
631
    }
632
633
    /**
634
     * Turns the query being built into a bulk delete query that ranges over
635
     * a certain table.
636
     *
637
     * <code>
638
     *     $qb = $conn->createQueryBuilder()
639
     *         ->delete('users', 'u')
640
     *         ->where('u.id = :user_id')
641
     *         ->setParameter(':user_id', 1);
642
     * </code>
643
     *
644
     * @param string $delete The table whose rows are subject to the deletion.
645
     * @param string $alias  The table alias used in the constructed query.
646
     *
647
     * @return $this This QueryBuilder instance.
648
     */
649
    public function delete($delete = null, $alias = null)
650
    {
651
        $this->type = self::DELETE;
652
653
        if (!$delete) {
654
            return $this;
655
        }
656
657
        return $this->add('from', [
658
            'table' => $delete,
659
            'alias' => $alias,
660
        ]);
661
    }
662
663
    /**
664
     * Turns the query being built into a bulk update query that ranges over
665
     * a certain table
666
     *
667
     * <code>
668
     *     $qb = $conn->createQueryBuilder()
669
     *         ->update('counters', 'c')
670
     *         ->set('c.value', 'c.value + 1')
671
     *         ->where('c.id = ?');
672
     * </code>
673
     *
674
     * @param string $update The table whose rows are subject to the update.
675
     * @param string $alias  The table alias used in the constructed query.
676
     *
677
     * @return $this This QueryBuilder instance.
678
     */
679
    public function update($update = null, $alias = null)
680
    {
681
        $this->type = self::UPDATE;
682
683
        if (!$update) {
684
            return $this;
685
        }
686
687
        return $this->add('from', [
688
            'table' => $update,
689
            'alias' => $alias,
690
        ]);
691
    }
692
693
    /**
694
     * Turns the query being built into an insert query that inserts into
695
     * a certain table
696
     *
697
     * <code>
698
     *     $qb = $conn->createQueryBuilder()
699
     *         ->insert('users')
700
     *         ->values(
701
     *             array(
702
     *                 'name' => '?',
703
     *                 'password' => '?'
704
     *             )
705
     *         );
706
     * </code>
707
     *
708
     * @param string $insert The table into which the rows should be inserted.
709
     *
710
     * @return $this This QueryBuilder instance.
711
     */
712
    public function insert($insert = null)
713
    {
714
        $this->type = self::INSERT;
715
716
        if (!$insert) {
717
            return $this;
718
        }
719
720
        return $this->add('from', ['table' => $insert]);
721
    }
722
723
    /**
724
     * Creates and adds a query root corresponding to the table identified by the
725
     * given alias, forming a cartesian product with any existing query roots.
726
     *
727
     * <code>
728
     *     $qb = $conn->createQueryBuilder()
729
     *         ->select('u.id')
730
     *         ->from('users', 'u')
731
     * </code>
732
     *
733
     * @param string      $from  The table.
734
     * @param string|null $alias The alias of the table.
735
     *
736
     * @return $this This QueryBuilder instance.
737
     */
738
    public function from($from, $alias = null)
739
    {
740
        return $this->add('from', [
741
            'table' => $from,
742
            'alias' => $alias,
743
        ], true);
744
    }
745
746
    /**
747
     * Creates and adds a join to the query.
748
     *
749
     * <code>
750
     *     $qb = $conn->createQueryBuilder()
751
     *         ->select('u.name')
752
     *         ->from('users', 'u')
753
     *         ->join('u', 'phonenumbers', 'p', 'p.is_primary = 1');
754
     * </code>
755
     *
756
     * @param string $fromAlias The alias that points to a from clause.
757
     * @param string $join      The table name to join.
758
     * @param string $alias     The alias of the join table.
759
     * @param string $condition The condition for the join.
760
     *
761
     * @return $this This QueryBuilder instance.
762
     */
763
    public function join($fromAlias, $join, $alias, $condition = null)
764
    {
765
        return $this->innerJoin($fromAlias, $join, $alias, $condition);
766
    }
767
768
    /**
769
     * Creates and adds a join to the query.
770
     *
771
     * <code>
772
     *     $qb = $conn->createQueryBuilder()
773
     *         ->select('u.name')
774
     *         ->from('users', 'u')
775
     *         ->innerJoin('u', 'phonenumbers', 'p', 'p.is_primary = 1');
776
     * </code>
777
     *
778
     * @param string $fromAlias The alias that points to a from clause.
779
     * @param string $join      The table name to join.
780
     * @param string $alias     The alias of the join table.
781
     * @param string $condition The condition for the join.
782
     *
783
     * @return $this This QueryBuilder instance.
784
     */
785
    public function innerJoin($fromAlias, $join, $alias, $condition = null)
786
    {
787
        return $this->add('join', [
788
            $fromAlias => [
789
                'joinType' => 'inner',
790
                'joinTable' => $join,
791
                'joinAlias' => $alias,
792
                'joinCondition' => $condition,
793
            ],
794
        ], true);
795
    }
796
797
    /**
798
     * Creates and adds a left join to the query.
799
     *
800
     * <code>
801
     *     $qb = $conn->createQueryBuilder()
802
     *         ->select('u.name')
803
     *         ->from('users', 'u')
804
     *         ->leftJoin('u', 'phonenumbers', 'p', 'p.is_primary = 1');
805
     * </code>
806
     *
807
     * @param string $fromAlias The alias that points to a from clause.
808
     * @param string $join      The table name to join.
809
     * @param string $alias     The alias of the join table.
810
     * @param string $condition The condition for the join.
811
     *
812
     * @return $this This QueryBuilder instance.
813
     */
814
    public function leftJoin($fromAlias, $join, $alias, $condition = null)
815
    {
816
        return $this->add('join', [
817
            $fromAlias => [
818
                'joinType' => 'left',
819
                'joinTable' => $join,
820
                'joinAlias' => $alias,
821
                'joinCondition' => $condition,
822
            ],
823
        ], true);
824
    }
825
826
    /**
827
     * Creates and adds a right join to the query.
828
     *
829
     * <code>
830
     *     $qb = $conn->createQueryBuilder()
831
     *         ->select('u.name')
832
     *         ->from('users', 'u')
833
     *         ->rightJoin('u', 'phonenumbers', 'p', 'p.is_primary = 1');
834
     * </code>
835
     *
836
     * @param string $fromAlias The alias that points to a from clause.
837
     * @param string $join      The table name to join.
838
     * @param string $alias     The alias of the join table.
839
     * @param string $condition The condition for the join.
840
     *
841
     * @return $this This QueryBuilder instance.
842
     */
843
    public function rightJoin($fromAlias, $join, $alias, $condition = null)
844
    {
845
        return $this->add('join', [
846
            $fromAlias => [
847
                'joinType' => 'right',
848
                'joinTable' => $join,
849
                'joinAlias' => $alias,
850
                'joinCondition' => $condition,
851
            ],
852
        ], true);
853
    }
854
855
    /**
856
     * Sets a new value for a column in a bulk update query.
857
     *
858
     * <code>
859
     *     $qb = $conn->createQueryBuilder()
860
     *         ->update('counters', 'c')
861
     *         ->set('c.value', 'c.value + 1')
862
     *         ->where('c.id = ?');
863
     * </code>
864
     *
865
     * @param string $key   The column to set.
866
     * @param string $value The value, expression, placeholder, etc.
867
     *
868
     * @return $this This QueryBuilder instance.
869
     */
870
    public function set($key, $value)
871
    {
872
        return $this->add('set', $key . ' = ' . $value, true);
873
    }
874
875
    /**
876
     * Specifies one or more restrictions to the query result.
877
     * Replaces any previously specified restrictions, if any.
878
     *
879
     * <code>
880
     *     $qb = $conn->createQueryBuilder()
881
     *         ->select('c.value')
882
     *         ->from('counters', 'c')
883
     *         ->where('c.id = ?');
884
     *
885
     *     // You can optionally programatically build and/or expressions
886
     *     $qb = $conn->createQueryBuilder();
887
     *
888
     *     $or = $qb->expr()->orx();
889
     *     $or->add($qb->expr()->eq('c.id', 1));
890
     *     $or->add($qb->expr()->eq('c.id', 2));
891
     *
892
     *     $qb->update('counters', 'c')
893
     *         ->set('c.value', 'c.value + 1')
894
     *         ->where($or);
895
     * </code>
896
     *
897
     * @param mixed $predicates The restriction predicates.
898
     *
899
     * @return $this This QueryBuilder instance.
900
     */
901
    public function where($predicates)
902
    {
903
        if (!(\func_num_args() === 1 && $predicates instanceof CompositeExpression)) {
904
            $predicates = CompositeExpression::and(...\func_get_args());
905
        }
906
907
        return $this->add('where', $predicates);
908
    }
909
910
    /**
911
     * Adds one or more restrictions to the query results, forming a logical
912
     * conjunction with any previously specified restrictions.
913
     *
914
     * <code>
915
     *     $qb = $conn->createQueryBuilder()
916
     *         ->select('u')
917
     *         ->from('users', 'u')
918
     *         ->where('u.username LIKE ?')
919
     *         ->andWhere('u.is_active = 1');
920
     * </code>
921
     *
922
     * @see where()
923
     *
924
     * @param mixed $where The query restrictions.
925
     *
926
     * @return $this This QueryBuilder instance.
927
     */
928
    public function andWhere($where)
929
    {
930
        $args = \func_get_args();
931
        $args = array_filter($args); // https://github.com/doctrine/dbal/issues/4282
932
        $where = $this->getQueryPart('where');
933
934
        if ($where instanceof CompositeExpression && $where->getType() === CompositeExpression::TYPE_AND) {
935
            if (\count($args) > 0) {
936
                $where = $where->with(...$args);
937
            }
938
        } else {
939
            array_unshift($args, $where);
940
            $where = CompositeExpression::and(...$args);
941
        }
942
943
        return $this->add('where', $where, true);
944
    }
945
946
    /**
947
     * Adds one or more restrictions to the query results, forming a logical
948
     * disjunction with any previously specified restrictions.
949
     *
950
     * <code>
951
     *     $qb = $em->createQueryBuilder()
952
     *         ->select('u.name')
953
     *         ->from('users', 'u')
954
     *         ->where('u.id = 1')
955
     *         ->orWhere('u.id = 2');
956
     * </code>
957
     *
958
     * @see where()
959
     *
960
     * @param mixed $where The WHERE statement.
961
     *
962
     * @return $this This QueryBuilder instance.
963
     */
964
    public function orWhere($where)
965
    {
966
        $args = \func_get_args();
967
        $args = array_filter($args); // https://github.com/doctrine/dbal/issues/4282
968
        $where = $this->getQueryPart('where');
969
970
        if ($where instanceof CompositeExpression && $where->getType() === CompositeExpression::TYPE_OR) {
971
            if (\count($args) > 0) {
972
                $where = $where->with(...$args);
973
            }
974
        } else {
975
            array_unshift($args, $where);
976
            $where = CompositeExpression::or(...$args);
977
        }
978
979
        return $this->add('where', $where, true);
980
    }
981
982
    /**
983
     * Specifies a grouping over the results of the query.
984
     * Replaces any previously specified groupings, if any.
985
     *
986
     * USING AN ARRAY ARGUMENT IS DEPRECATED. Pass each value as an individual argument.
987
     *
988
     * <code>
989
     *     $qb = $conn->createQueryBuilder()
990
     *         ->select('u.name')
991
     *         ->from('users', 'u')
992
     *         ->groupBy('u.id');
993
     * </code>
994
     *
995
     * @param string|string[] $groupBy The grouping expression. USING AN ARRAY IS DEPRECATED.
996
     *                                 Pass each value as an individual argument.
997
     *
998
     * @return $this This QueryBuilder instance.
999
     */
1000
    public function groupBy($groupBy/*, string ...$groupBys*/)
1001
    {
1002
        if (empty($groupBy)) {
1003
            return $this;
1004
        }
1005
1006
        if (\is_array($groupBy)) {
1007
            Deprecation::trigger(
1008
                'doctrine/dbal',
1009
                'https://github.com/doctrine/dbal/issues/3837',
1010
                'Passing an array for the first argument to QueryBuilder::groupBy is deprecated, '
1011
                . 'pass each value as an individual variadic argument instead.'
1012
            );
1013
        }
1014
1015
        $groupBy = \is_array($groupBy) ? $groupBy : \func_get_args();
1016
1017
        return $this->add('groupBy', $groupBy, false);
1018
    }
1019
1020
    /**
1021
     * Adds a grouping expression to the query.
1022
     *
1023
     * USING AN ARRAY ARGUMENT IS DEPRECATED. Pass each value as an individual argument.
1024
     *
1025
     * <code>
1026
     *     $qb = $conn->createQueryBuilder()
1027
     *         ->select('u.name')
1028
     *         ->from('users', 'u')
1029
     *         ->groupBy('u.lastLogin')
1030
     *         ->addGroupBy('u.createdAt');
1031
     * </code>
1032
     *
1033
     * @param string|string[] $groupBy The grouping expression. USING AN ARRAY IS DEPRECATED.
1034
     *                                 Pass each value as an individual argument.
1035
     *
1036
     * @return $this This QueryBuilder instance.
1037
     */
1038
    public function addGroupBy($groupBy/*, string ...$groupBys*/)
1039
    {
1040
        if (empty($groupBy)) {
1041
            return $this;
1042
        }
1043
1044
        if (\is_array($groupBy)) {
1045
            Deprecation::trigger(
1046
                'doctrine/dbal',
1047
                'https://github.com/doctrine/dbal/issues/3837',
1048
                'Passing an array for the first argument to QueryBuilder::addGroupBy is deprecated, '
1049
                . 'pass each value as an individual variadic argument instead.'
1050
            );
1051
        }
1052
1053
        $groupBy = \is_array($groupBy) ? $groupBy : \func_get_args();
1054
1055
        return $this->add('groupBy', $groupBy, true);
1056
    }
1057
1058
    /**
1059
     * Sets a value for a column in an insert query.
1060
     *
1061
     * <code>
1062
     *     $qb = $conn->createQueryBuilder()
1063
     *         ->insert('users')
1064
     *         ->values(
1065
     *             array(
1066
     *                 'name' => '?'
1067
     *             )
1068
     *         )
1069
     *         ->setValue('password', '?');
1070
     * </code>
1071
     *
1072
     * @param string $column The column into which the value should be inserted.
1073
     * @param string $value  The value that should be inserted into the column.
1074
     *
1075
     * @return $this This QueryBuilder instance.
1076
     */
1077
    public function setValue($column, $value)
1078
    {
1079
        $this->sqlParts['values'][$column] = $value;
1080
1081
        return $this;
1082
    }
1083
1084
    /**
1085
     * Specifies values for an insert query indexed by column names.
1086
     * Replaces any previous values, if any.
1087
     *
1088
     * <code>
1089
     *     $qb = $conn->createQueryBuilder()
1090
     *         ->insert('users')
1091
     *         ->values(
1092
     *             array(
1093
     *                 'name' => '?',
1094
     *                 'password' => '?'
1095
     *             )
1096
     *         );
1097
     * </code>
1098
     *
1099
     * @param mixed[] $values The values to specify for the insert query indexed by column names.
1100
     *
1101
     * @return $this This QueryBuilder instance.
1102
     */
1103
    public function values(array $values)
1104
    {
1105
        return $this->add('values', $values);
1106
    }
1107
1108
    /**
1109
     * Specifies a restriction over the groups of the query.
1110
     * Replaces any previous having restrictions, if any.
1111
     *
1112
     * @param mixed $having The restriction over the groups.
1113
     *
1114
     * @return $this This QueryBuilder instance.
1115
     */
1116
    public function having($having)
1117
    {
1118
        if (!(\func_num_args() === 1 && $having instanceof CompositeExpression)) {
1119
            $having = CompositeExpression::and(...\func_get_args());
1120
        }
1121
1122
        return $this->add('having', $having);
1123
    }
1124
1125
    /**
1126
     * Adds a restriction over the groups of the query, forming a logical
1127
     * conjunction with any existing having restrictions.
1128
     *
1129
     * @param mixed $having The restriction to append.
1130
     *
1131
     * @return $this This QueryBuilder instance.
1132
     */
1133
    public function andHaving($having)
1134
    {
1135
        $args = \func_get_args();
1136
        $args = array_filter($args); // https://github.com/doctrine/dbal/issues/4282
1137
        $having = $this->getQueryPart('having');
1138
1139
        if ($having instanceof CompositeExpression && $having->getType() === CompositeExpression::TYPE_AND) {
1140
            $having = $having->with(...$args);
1141
        } else {
1142
            array_unshift($args, $having);
1143
            $having = CompositeExpression::and(...$args);
1144
        }
1145
1146
        return $this->add('having', $having);
1147
    }
1148
1149
    /**
1150
     * Adds a restriction over the groups of the query, forming a logical
1151
     * disjunction with any existing having restrictions.
1152
     *
1153
     * @param mixed $having The restriction to add.
1154
     *
1155
     * @return $this This QueryBuilder instance.
1156
     */
1157
    public function orHaving($having)
1158
    {
1159
        $args = \func_get_args();
1160
        $args = array_filter($args); // https://github.com/doctrine/dbal/issues/4282
1161
        $having = $this->getQueryPart('having');
1162
1163
        if ($having instanceof CompositeExpression && $having->getType() === CompositeExpression::TYPE_OR) {
1164
            $having = $having->with(...$args);
1165
        } else {
1166
            array_unshift($args, $having);
1167
            $having = CompositeExpression::or(...$args);
1168
        }
1169
1170
        return $this->add('having', $having);
1171
    }
1172
1173
    /**
1174
     * Specifies an ordering for the query results.
1175
     * Replaces any previously specified orderings, if any.
1176
     *
1177
     * @param string $sort  The ordering expression.
1178
     * @param string $order The ordering direction.
1179
     *
1180
     * @return $this This QueryBuilder instance.
1181
     */
1182
    public function orderBy($sort, $order = null)
1183
    {
1184
        return $this->add('orderBy', $sort . ' ' . (!$order ? 'ASC' : $order), false);
1185
    }
1186
1187
    /**
1188
     * Adds an ordering to the query results.
1189
     *
1190
     * @param string $sort  The ordering expression.
1191
     * @param string $order The ordering direction.
1192
     *
1193
     * @return $this This QueryBuilder instance.
1194
     */
1195
    public function addOrderBy($sort, $order = null)
1196
    {
1197
        return $this->add('orderBy', $sort . ' ' . (!$order ? 'ASC' : $order), true);
1198
    }
1199
1200
    /**
1201
     * Gets a query part by its name.
1202
     *
1203
     * @param string $queryPartName
1204
     *
1205
     * @return mixed
1206
     */
1207
    public function getQueryPart($queryPartName)
1208
    {
1209
        return $this->sqlParts[$queryPartName];
1210
    }
1211
1212
    /**
1213
     * Gets all query parts.
1214
     *
1215
     * @return mixed[]
1216
     */
1217
    public function getQueryParts()
1218
    {
1219
        return $this->sqlParts;
1220
    }
1221
1222
    /**
1223
     * Resets SQL parts.
1224
     *
1225
     * @param string[]|null $queryPartNames
1226
     *
1227
     * @return $this This QueryBuilder instance.
1228
     */
1229
    public function resetQueryParts($queryPartNames = null)
1230
    {
1231
        if ($queryPartNames === null) {
1232
            $queryPartNames = array_keys($this->sqlParts);
1233
        }
1234
1235
        foreach ($queryPartNames as $queryPartName) {
1236
            $this->resetQueryPart($queryPartName);
1237
        }
1238
1239
        return $this;
1240
    }
1241
1242
    /**
1243
     * Resets a single SQL part.
1244
     *
1245
     * @param string $queryPartName
1246
     *
1247
     * @return $this This QueryBuilder instance.
1248
     */
1249
    public function resetQueryPart($queryPartName)
1250
    {
1251
        $this->sqlParts[$queryPartName] = self::SQL_PARTS_DEFAULTS[$queryPartName];
1252
1253
        $this->state = self::STATE_DIRTY;
1254
1255
        return $this;
1256
    }
1257
1258
    /**
1259
     * Creates a new named parameter and bind the value $value to it.
1260
     *
1261
     * This method provides a shortcut for PDOStatement::bindValue
1262
     * when using prepared statements.
1263
     *
1264
     * The parameter $value specifies the value that you want to bind. If
1265
     * $placeholder is not provided bindValue() will automatically create a
1266
     * placeholder for you. An automatic placeholder will be of the name
1267
     * ':dcValue1', ':dcValue2' etc.
1268
     *
1269
     * For more information see {@link http://php.net/pdostatement-bindparam}
1270
     *
1271
     * Example:
1272
     * <code>
1273
     * $value = 2;
1274
     * $q->eq( 'id', $q->bindValue( $value ) );
1275
     * $stmt = $q->executeQuery(); // executed with 'id = 2'
1276
     * </code>
1277
     *
1278
     * @see http://www.zetacomponents.org
1279
     *
1280
     * @param mixed                $value
1281
     * @param int|string|Type|null $type
1282
     * @param string               $placeHolder The name to bind with. The string must start with a colon ':'.
1283
     *
1284
     * @return string the placeholder name used.
1285
     */
1286
    public function createNamedParameter($value, $type = ParameterType::STRING, $placeHolder = null)
1287
    {
1288
        if ($placeHolder === null) {
1289
            ++$this->boundCounter;
1290
            $placeHolder = ':dcValue' . $this->boundCounter;
1291
        }
1292
1293
        $this->setParameter(substr($placeHolder, 1), $value, $type);
1294
1295
        return $placeHolder;
1296
    }
1297
1298
    /**
1299
     * Creates a new positional parameter and bind the given value to it.
1300
     *
1301
     * Attention: If you are using positional parameters with the query builder you have
1302
     * to be very careful to bind all parameters in the order they appear in the SQL
1303
     * statement , otherwise they get bound in the wrong order which can lead to serious
1304
     * bugs in your code.
1305
     *
1306
     * Example:
1307
     * <code>
1308
     *  $qb = $conn->createQueryBuilder();
1309
     *  $qb->select('u.*')
1310
     *     ->from('users', 'u')
1311
     *     ->where('u.username = ' . $qb->createPositionalParameter('Foo', ParameterType::STRING))
1312
     *     ->orWhere('u.username = ' . $qb->createPositionalParameter('Bar', ParameterType::STRING))
1313
     * </code>
1314
     *
1315
     * @param mixed                $value
1316
     * @param int|string|Type|null $type
1317
     *
1318
     * @return string
1319
     */
1320
    public function createPositionalParameter($value, $type = ParameterType::STRING)
1321
    {
1322
        ++$this->boundCounter;
1323
        $this->setParameter($this->boundCounter, $value, $type);
1324
1325
        return '?';
1326
    }
1327
1328
    /**
1329
     * @throws QueryException
1330
     *
1331
     * @return string
1332
     */
1333
    private function getSQLForSelect()
1334
    {
1335
        $query = 'SELECT ' . ($this->sqlParts['distinct'] ? 'DISTINCT ' : '')
1336
            . implode(', ', $this->sqlParts['select']);
1337
1338
        $query .= ($this->sqlParts['from'] ? ' FROM ' . implode(', ', $this->getFromClauses()) : '')
1339
            . ($this->sqlParts['where'] !== null ? ' WHERE ' . ((string) $this->sqlParts['where']) : '')
1340
            . ($this->sqlParts['groupBy'] ? ' GROUP BY ' . implode(', ', $this->sqlParts['groupBy']) : '')
1341
            . ($this->sqlParts['having'] !== null ? ' HAVING ' . ((string) $this->sqlParts['having']) : '')
1342
            . ($this->sqlParts['orderBy'] ? ' ORDER BY ' . implode(', ', $this->sqlParts['orderBy']) : '');
1343
1344
        if ($this->isLimitQuery()) {
1345
            return $this->connection->getDatabasePlatform()->modifyLimitQuery(
1346
                $query,
1347
                $this->maxResults,
1348
                $this->firstResult
1349
            );
1350
        }
1351
1352
        return $query;
1353
    }
1354
1355
    /**
1356
     * @return string[]
1357
     */
1358
    private function getFromClauses()
1359
    {
1360
        $fromClauses = [];
1361
        $knownAliases = [];
1362
1363
        // Loop through all FROM clauses
1364
        foreach ($this->sqlParts['from'] as $from) {
1365
            if ($from['alias'] === null) {
1366
                $tableSql = $from['table'];
1367
                $tableReference = $from['table'];
1368
            } else {
1369
                $tableSql = $from['table'] . ' ' . $from['alias'];
1370
                $tableReference = $from['alias'];
1371
            }
1372
1373
            $knownAliases[$tableReference] = true;
1374
1375
            $fromClauses[$tableReference] = $tableSql . $this->getSQLForJoins($tableReference, $knownAliases);
1376
        }
1377
1378
        $this->verifyAllAliasesAreKnown($knownAliases);
1379
1380
        return $fromClauses;
1381
    }
1382
1383
    /**
1384
     * @param array<string,true> $knownAliases
1385
     *
1386
     * @throws QueryException
1387
     */
1388
    private function verifyAllAliasesAreKnown(array $knownAliases): void
1389
    {
1390
        foreach ($this->sqlParts['join'] as $fromAlias => $joins) {
1391
            if (!isset($knownAliases[$fromAlias])) {
1392
                throw QueryException::unknownAlias($fromAlias, array_keys($knownAliases));
1393
            }
1394
        }
1395
    }
1396
1397
    /**
1398
     * @return bool
1399
     */
1400
    private function isLimitQuery()
1401
    {
1402
        return $this->maxResults !== null || $this->firstResult !== 0;
1403
    }
1404
1405
    /**
1406
     * Converts this instance into an INSERT string in SQL.
1407
     *
1408
     * @return string
1409
     */
1410
    private function getSQLForInsert()
1411
    {
1412
        return 'INSERT INTO ' . $this->sqlParts['from']['table']
1413
            . ' (' . implode(', ', array_keys($this->sqlParts['values'])) . ')'
1414
            . ' VALUES(' . implode(', ', $this->sqlParts['values']) . ')';
1415
    }
1416
1417
    /**
1418
     * Converts this instance into an UPDATE string in SQL.
1419
     *
1420
     * @return string
1421
     */
1422
    private function getSQLForUpdate()
1423
    {
1424
        $table = $this->sqlParts['from']['table']
1425
            . ($this->sqlParts['from']['alias'] ? ' ' . $this->sqlParts['from']['alias'] : '');
1426
1427
        return 'UPDATE ' . $table
1428
            . ' SET ' . implode(', ', $this->sqlParts['set'])
1429
            . ($this->sqlParts['where'] !== null ? ' WHERE ' . ((string) $this->sqlParts['where']) : '');
1430
    }
1431
1432
    /**
1433
     * Converts this instance into a DELETE string in SQL.
1434
     *
1435
     * @return string
1436
     */
1437
    private function getSQLForDelete()
1438
    {
1439
        $table = $this->sqlParts['from']['table']
1440
            . ($this->sqlParts['from']['alias'] ? ' ' . $this->sqlParts['from']['alias'] : '');
1441
1442
        return 'DELETE FROM ' . $table
1443
            . ($this->sqlParts['where'] !== null ? ' WHERE ' . ((string) $this->sqlParts['where']) : '');
1444
    }
1445
1446
    /**
1447
     * @param string             $fromAlias
1448
     * @param array<string,true> $knownAliases
1449
     *
1450
     * @throws QueryException
1451
     *
1452
     * @return string
1453
     */
1454
    private function getSQLForJoins($fromAlias, array &$knownAliases)
1455
    {
1456
        $sql = '';
1457
1458
        if (isset($this->sqlParts['join'][$fromAlias])) {
1459
            foreach ($this->sqlParts['join'][$fromAlias] as $join) {
1460
                if (\array_key_exists($join['joinAlias'], $knownAliases)) {
1461
                    /** @var array<string> $keys */
1462
                    $keys = array_keys($knownAliases);
1463
1464
                    throw QueryException::nonUniqueAlias($join['joinAlias'], $keys);
1465
                }
1466
1467
                $sql .= ' ' . strtoupper($join['joinType'])
1468
                    . ' JOIN ' . $join['joinTable'] . ' ' . $join['joinAlias'];
1469
                if ($join['joinCondition'] !== null) {
1470
                    $sql .= ' ON ' . $join['joinCondition'];
1471
                }
1472
1473
                $knownAliases[$join['joinAlias']] = true;
1474
            }
1475
1476
            foreach ($this->sqlParts['join'][$fromAlias] as $join) {
1477
                $sql .= $this->getSQLForJoins($join['joinAlias'], $knownAliases);
1478
            }
1479
        }
1480
1481
        return $sql;
1482
    }
1483
}
1484