Completed
Pull Request — master (#3831)
by Sergei
63:14
created

QueryBuilder::appendPredicates()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 3

Importance

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