Failed Conditions
Pull Request — master (#7242)
by Gabriel
08:46
created

QueryBuilder::addSelect()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
cc 3
eloc 5
nc 3
nop 1
dl 0
loc 11
ccs 0
cts 6
cp 0
crap 12
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\ORM;
6
7
use Doctrine\Common\Collections\ArrayCollection;
8
use Doctrine\Common\Collections\Criteria;
9
use Doctrine\ORM\Query\Expr;
10
use Doctrine\ORM\Query\QueryExpressionVisitor;
11
use function array_keys;
12
use function array_merge;
13
use function array_unshift;
14
use function func_get_args;
15
use function func_num_args;
16
use function implode;
17
use function in_array;
18
use function is_array;
19
use function is_numeric;
20
use function is_object;
21
use function is_string;
22
use function key;
23
use function reset;
24
use function sprintf;
25
use function strpos;
26
use function strrpos;
27
use function substr;
28
29
/**
30
 * This class is responsible for building DQL query strings via an object oriented
31
 * PHP interface.
32
 */
33
class QueryBuilder
34
{
35
    /* The query types. */
36
    public const SELECT = 0;
37
    public const DELETE = 1;
38
    public const UPDATE = 2;
39
40
    /* The builder states. */
41
    public const STATE_DIRTY = 0;
42
    public const STATE_CLEAN = 1;
43
44
    /**
45
     * The EntityManager used by this QueryBuilder.
46
     *
47
     * @var EntityManagerInterface
48
     */
49
    private $em;
50
51
    /**
52
     * The array of DQL parts collected.
53
     *
54
     * @var mixed[]
55
     */
56
    private $dqlParts = [
57
        'distinct' => false,
58
        'select'  => [],
59
        'from'    => [],
60
        'join'    => [],
61
        'set'     => [],
62
        'where'   => null,
63
        'groupBy' => [],
64
        'having'  => null,
65
        'orderBy' => [],
66
    ];
67
68
    /**
69
     * The type of query this is. Can be select, update or delete.
70
     *
71
     * @var int
72
     */
73
    private $type = self::SELECT;
74
75
    /**
76
     * The state of the query object. Can be dirty or clean.
77
     *
78
     * @var int
79
     */
80
    private $state = self::STATE_CLEAN;
81
82
    /**
83
     * The complete DQL string for this query.
84
     *
85
     * @var string
86
     */
87
    private $dql;
88
89
    /**
90
     * The query parameters.
91
     *
92
     * @var ArrayCollection
93
     */
94
    private $parameters;
95
96
    /**
97
     * The index of the first result to retrieve.
98
     *
99
     * @var int
100
     */
101
    private $firstResult;
102
103
    /**
104
     * The maximum number of results to retrieve.
105
     *
106
     * @var int|null
107
     */
108
    private $maxResults;
109
110
    /**
111
     * Keeps root entity alias names for join entities.
112
     *
113
     * @var mixed[]
114
     */
115
    private $joinRootAliases = [];
116
117
    /**
118
     * Whether to use second level cache, if available.
119
     *
120
     * @var bool
121
     */
122
    protected $cacheable = false;
123
124
    /**
125
     * Second level cache region name.
126
     *
127
     * @var string|null
128
     */
129
    protected $cacheRegion;
130
131
    /**
132
     * Second level query cache mode.
133
     *
134
     * @var int|null
135
     */
136
    protected $cacheMode;
137
138
    /** @var int */
139
    protected $lifetime = 0;
140
141
    /**
142
     * Initializes a new <tt>QueryBuilder</tt> that uses the given <tt>EntityManager</tt>.
143
     *
144
     * @param EntityManagerInterface $em The EntityManager to use.
145
     */
146 112
    public function __construct(EntityManagerInterface $em)
147
    {
148 112
        $this->em         = $em;
149 112
        $this->parameters = new ArrayCollection();
150 112
    }
151
152
    /**
153
     * Gets an ExpressionBuilder used for object-oriented construction of query expressions.
154
     * This producer method is intended for convenient inline usage. Example:
155
     *
156
     * <code>
157
     *     $qb = $em->createQueryBuilder();
158
     *     $qb
159
     *         ->select('u')
160
     *         ->from('User', 'u')
161
     *         ->where($qb->expr()->eq('u.id', 1));
162
     * </code>
163
     *
164
     * For more complex expression construction, consider storing the expression
165
     * builder object in a local variable.
166
     *
167
     * @return Query\Expr
168
     */
169 11
    public function expr()
170
    {
171 11
        return $this->em->getExpressionBuilder();
172
    }
173
174
    /**
175
     * Enable/disable second level query (result) caching for this query.
176
     *
177
     * @param bool $cacheable
178
     *
179
     * @return self
180
     */
181 1
    public function setCacheable($cacheable)
182
    {
183 1
        $this->cacheable = (bool) $cacheable;
184
185 1
        return $this;
186
    }
187
188
    /**
189
     * @return bool TRUE if the query results are enable for second level cache, FALSE otherwise.
190
     */
191 1
    public function isCacheable()
192
    {
193 1
        return $this->cacheable;
194
    }
195
196
    /**
197
     * @param string $cacheRegion
198
     *
199
     * @return self
200
     */
201 1
    public function setCacheRegion($cacheRegion)
202
    {
203 1
        $this->cacheRegion = (string) $cacheRegion;
204
205 1
        return $this;
206
    }
207
208
    /**
209
     * Obtain the name of the second level query cache region in which query results will be stored
210
     *
211
     * @return string|null The cache region name; NULL indicates the default region.
212
     */
213 1
    public function getCacheRegion()
214
    {
215 1
        return $this->cacheRegion;
216
    }
217
218
    /**
219
     * @return int
220
     */
221 1
    public function getLifetime()
222
    {
223 1
        return $this->lifetime;
224
    }
225
226
    /**
227
     * Sets the life-time for this query into second level cache.
228
     *
229
     * @param int $lifetime
230
     *
231
     * @return self
232
     */
233 1
    public function setLifetime($lifetime)
234
    {
235 1
        $this->lifetime = (int) $lifetime;
236
237 1
        return $this;
238
    }
239
240
    /**
241
     * @return int
242
     */
243 1
    public function getCacheMode()
244
    {
245 1
        return $this->cacheMode;
246
    }
247
248
    /**
249
     * @param int $cacheMode
250
     *
251
     * @return self
252
     */
253 1
    public function setCacheMode($cacheMode)
254
    {
255 1
        $this->cacheMode = (int) $cacheMode;
256
257 1
        return $this;
258
    }
259
260
    /**
261
     * Gets the type of the currently built query.
262
     *
263
     * @return int
264
     */
265 4
    public function getType()
266
    {
267 4
        return $this->type;
268
    }
269
270
    /**
271
     * Gets the associated EntityManager for this query builder.
272
     *
273
     * @return EntityManagerInterface
274
     */
275 1
    public function getEntityManager()
276
    {
277 1
        return $this->em;
278
    }
279
280
    /**
281
     * Gets the state of this query builder instance.
282
     *
283
     * @return int Either QueryBuilder::STATE_DIRTY or QueryBuilder::STATE_CLEAN.
284
     */
285 2
    public function getState()
286
    {
287 2
        return $this->state;
288
    }
289
290
    /**
291
     * Gets the complete DQL string formed by the current specifications of this QueryBuilder.
292
     *
293
     * <code>
294
     *     $qb = $em->createQueryBuilder()
295
     *         ->select('u')
296
     *         ->from('User', 'u');
297
     *     echo $qb->getDql(); // SELECT u FROM User u
298
     * </code>
299
     *
300
     * @return string The DQL query string.
301
     */
302 72
    public function getDQL()
303
    {
304 72
        if ($this->dql !== null && $this->state === self::STATE_CLEAN) {
305 44
            return $this->dql;
306
        }
307
308 72
        switch ($this->type) {
309 72
            case self::DELETE:
310 1
                $dql = $this->getDQLForDelete();
311 1
                break;
312
313 71
            case self::UPDATE:
314 1
                $dql = $this->getDQLForUpdate();
315 1
                break;
316
317 70
            case self::SELECT:
318
            default:
319 70
                $dql = $this->getDQLForSelect();
320 70
                break;
321
        }
322
323 72
        $this->state = self::STATE_CLEAN;
324 72
        $this->dql   = $dql;
325
326 72
        return $dql;
327
    }
328
329
    /**
330
     * Constructs a Query instance from the current specifications of the builder.
331
     *
332
     * <code>
333
     *     $qb = $em->createQueryBuilder()
334
     *         ->select('u')
335
     *         ->from('User', 'u');
336
     *     $q = $qb->getQuery();
337
     *     $results = $q->execute();
338
     * </code>
339
     *
340
     * @return Query
341
     */
342 61
    public function getQuery()
343
    {
344 61
        $parameters = clone $this->parameters;
345 61
        $query      = $this->em->createQuery($this->getDQL())
346 61
            ->setParameters($parameters)
347 61
            ->setFirstResult($this->firstResult)
348 61
            ->setMaxResults($this->maxResults);
349
350 61
        if ($this->lifetime) {
351 1
            $query->setLifetime($this->lifetime);
352
        }
353
354 61
        if ($this->cacheMode) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->cacheMode of type null|integer is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
355 1
            $query->setCacheMode($this->cacheMode);
356
        }
357
358 61
        if ($this->cacheable) {
359 1
            $query->setCacheable($this->cacheable);
360
        }
361
362 61
        if ($this->cacheRegion) {
363 1
            $query->setCacheRegion($this->cacheRegion);
364
        }
365
366 61
        return $query;
367
    }
368
369
    /**
370
     * Finds the root entity alias of the joined entity.
371
     *
372
     * @param string $alias       The alias of the new join entity
373
     * @param string $parentAlias The parent entity alias of the join relationship
374
     *
375
     * @return string
376
     */
377 23
    private function findRootAlias($alias, $parentAlias)
378
    {
379 23
        $rootAlias = null;
380
381 23
        if (in_array($parentAlias, $this->getRootAliases(), true)) {
382 22
            $rootAlias = $parentAlias;
383 2
        } elseif (isset($this->joinRootAliases[$parentAlias])) {
384 1
            $rootAlias = $this->joinRootAliases[$parentAlias];
385
        } else {
386
            // Should never happen with correct joining order. Might be
387
            // thoughtful to throw exception instead.
388 1
            $rootAlias = $this->getRootAlias();
0 ignored issues
show
Deprecated Code introduced by
The function Doctrine\ORM\QueryBuilder::getRootAlias() has been deprecated: Please use $qb->getRootAliases() instead. ( Ignorable by Annotation )

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

388
            $rootAlias = /** @scrutinizer ignore-deprecated */ $this->getRootAlias();

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
389
        }
390
391 23
        $this->joinRootAliases[$alias] = $rootAlias;
392
393 23
        return $rootAlias;
394
    }
395
396
    /**
397
     * Gets the FIRST root alias of the query. This is the first entity alias involved
398
     * in the construction of the query.
399
     *
400
     * <code>
401
     * $qb = $em->createQueryBuilder()
402
     *     ->select('u')
403
     *     ->from('User', 'u');
404
     *
405
     * echo $qb->getRootAlias(); // u
406
     * </code>
407
     *
408
     * @deprecated Please use $qb->getRootAliases() instead.
409
     * @throws \RuntimeException
410
     *
411
     * @return string
412
     */
413 4
    public function getRootAlias()
414
    {
415 4
        $aliases = $this->getRootAliases();
416
417 4
        if (! isset($aliases[0])) {
418
            throw new \RuntimeException('No alias was set before invoking getRootAlias().');
419
        }
420
421 4
        return $aliases[0];
422
    }
423
424
    /**
425
     * Gets the root aliases of the query. This is the entity aliases involved
426
     * in the construction of the query.
427
     *
428
     * <code>
429
     *     $qb = $em->createQueryBuilder()
430
     *         ->select('u')
431
     *         ->from('User', 'u');
432
     *
433
     *     $qb->getRootAliases(); // array('u')
434
     * </code>
435
     *
436
     * @return string[]
437
     */
438 39
    public function getRootAliases()
439
    {
440 39
        $aliases = [];
441
442 39
        foreach ($this->dqlParts['from'] as &$fromClause) {
443 39
            if (is_string($fromClause)) {
444
                $spacePos = strrpos($fromClause, ' ');
445
                $from     = substr($fromClause, 0, $spacePos);
446
                $alias    = substr($fromClause, $spacePos + 1);
447
448
                $fromClause = new Query\Expr\From($from, $alias);
449
            }
450
451 39
            $aliases[] = $fromClause->getAlias();
452
        }
453
454 39
        return $aliases;
455
    }
456
457
    /**
458
     * Gets all the aliases that have been used in the query.
459
     * Including all select root aliases and join aliases
460
     *
461
     * <code>
462
     *     $qb = $em->createQueryBuilder()
463
     *         ->select('u')
464
     *         ->from('User', 'u')
465
     *         ->join('u.articles','a');
466
     *
467
     *     $qb->getAllAliases(); // array('u','a')
468
     * </code>
469
     *
470
     * @return string[]
471
     */
472 15
    public function getAllAliases()
473
    {
474 15
        return array_merge($this->getRootAliases(), array_keys($this->joinRootAliases));
475
    }
476
477
    /**
478
     * Gets the root entities of the query. This is the entity aliases involved
479
     * in the construction of the query.
480
     *
481
     * <code>
482
     *     $qb = $em->createQueryBuilder()
483
     *         ->select('u')
484
     *         ->from('User', 'u');
485
     *
486
     *     $qb->getRootEntities(); // array('User')
487
     * </code>
488
     *
489
     * @return string[]
490
     */
491 1
    public function getRootEntities()
492
    {
493 1
        $entities = [];
494
495 1
        foreach ($this->dqlParts['from'] as &$fromClause) {
496 1
            if (is_string($fromClause)) {
497
                $spacePos = strrpos($fromClause, ' ');
498
                $from     = substr($fromClause, 0, $spacePos);
499
                $alias    = substr($fromClause, $spacePos + 1);
500
501
                $fromClause = new Query\Expr\From($from, $alias);
502
            }
503
504 1
            $entities[] = $fromClause->getFrom();
505
        }
506
507 1
        return $entities;
508
    }
509
510
    /**
511
     * Sets a query parameter for the query being constructed.
512
     *
513
     * <code>
514
     *     $qb = $em->createQueryBuilder()
515
     *         ->select('u')
516
     *         ->from('User', 'u')
517
     *         ->where('u.id = :user_id')
518
     *         ->setParameter('user_id', 1);
519
     * </code>
520
     *
521
     * @param string|int      $key   The parameter position or name.
522
     * @param mixed           $value The parameter value.
523
     * @param string|int|null $type  ParameterType::* or \Doctrine\DBAL\Types\Type::* constant
524
     *
525
     * @return self
526
     */
527 9
    public function setParameter($key, $value, $type = null)
528
    {
529 9
        $existingParameter = $this->getParameter($key);
530
531 9
        if ($existingParameter !== null) {
532 1
            $existingParameter->setValue($value, $type);
533
534 1
            return $this;
535
        }
536
537 9
        $this->parameters->add(new Query\Parameter($key, $value, $type));
538
539 9
        return $this;
540
    }
541
542
    /**
543
     * Sets a collection of query parameters for the query being constructed.
544
     *
545
     * <code>
546
     *     $qb = $em->createQueryBuilder()
547
     *         ->select('u')
548
     *         ->from('User', 'u')
549
     *         ->where('u.id = :user_id1 OR u.id = :user_id2')
550
     *         ->setParameters(new ArrayCollection(array(
551
     *             new Parameter('user_id1', 1),
552
     *             new Parameter('user_id2', 2)
553
     *        )));
554
     * </code>
555
     *
556
     * @param ArrayCollection|array|mixed[] $parameters The query parameters to set.
557
     *
558
     * @return self
559
     */
560 3
    public function setParameters($parameters)
561
    {
562
        // BC compatibility with 2.3-
563 3
        if (is_array($parameters)) {
564
            $parameterCollection = new ArrayCollection();
565
566
            foreach ($parameters as $key => $value) {
567
                $parameter = new Query\Parameter($key, $value);
568
569
                $parameterCollection->add($parameter);
570
            }
571
572
            $parameters = $parameterCollection;
573
        }
574
575 3
        $this->parameters = $parameters;
576
577 3
        return $this;
578
    }
579
580
    /**
581
     * Gets all defined query parameters for the query being constructed.
582
     *
583
     * @return ArrayCollection The currently defined query parameters.
584
     */
585 6
    public function getParameters()
586
    {
587 6
        return $this->parameters;
588
    }
589
590
    /**
591
     * Gets a (previously set) query parameter of the query being constructed.
592
     *
593
     * @param mixed $key The key (index or name) of the bound parameter.
594
     *
595
     * @return Query\Parameter|null The value of the bound parameter.
596
     */
597 19
    public function getParameter($key)
598
    {
599 19
        $filteredParameters = $this->parameters->filter(
600
            function (Query\Parameter $parameter) use ($key) : bool {
601 18
                $parameterName = $parameter->getName();
602
603 18
                return $key === $parameterName || (string) $key === (string) $parameterName;
604 19
            }
605
        );
606
607 19
        return $filteredParameters->isEmpty() ? null : $filteredParameters->first();
608
    }
609
610
    /**
611
     * Sets the position of the first result to retrieve (the "offset").
612
     *
613
     * @param int $firstResult The first result to return.
614
     *
615
     * @return self
616
     */
617 2
    public function setFirstResult($firstResult)
618
    {
619 2
        $this->firstResult = $firstResult;
620
621 2
        return $this;
622
    }
623
624
    /**
625
     * Gets the position of the first result the query object was set to retrieve (the "offset").
626
     * Returns NULL if {@link setFirstResult} was not applied to this QueryBuilder.
627
     *
628
     * @return int The position of the first result.
629
     */
630 2
    public function getFirstResult()
631
    {
632 2
        return $this->firstResult;
633
    }
634
635
    /**
636
     * Sets the maximum number of results to retrieve (the "limit").
637
     *
638
     * @param int|null $maxResults The maximum number of results to retrieve.
639
     *
640
     * @return self
641
     */
642 2
    public function setMaxResults($maxResults)
643
    {
644 2
        $this->maxResults = $maxResults;
645
646 2
        return $this;
647
    }
648
649
    /**
650
     * Gets the maximum number of results the query object was set to retrieve (the "limit").
651
     * Returns NULL if {@link setMaxResults} was not applied to this query builder.
652
     *
653
     * @return int|null Maximum number of results.
654
     */
655 2
    public function getMaxResults()
656
    {
657 2
        return $this->maxResults;
658
    }
659
660
    /**
661
     * Either appends to or replaces a single, generic query part.
662
     *
663
     * The available parts are: 'select', 'from', 'join', 'set', 'where',
664
     * 'groupBy', 'having' and 'orderBy'.
665
     *
666
     * @param string         $dqlPartName The DQL part name.
667
     * @param object|mixed[] $dqlPart     An Expr object.
668
     * @param bool           $append      Whether to append (true) or replace (false).
669
     *
670
     * @return self
671
     */
672 108
    public function add($dqlPartName, $dqlPart, $append = false)
673
    {
674 108
        if ($append && ($dqlPartName === 'where' || $dqlPartName === 'having')) {
675 1
            throw new \InvalidArgumentException(
676
                "Using \$append = true does not have an effect with 'where' or 'having' " .
677 1
                'parts. See QueryBuilder#andWhere() for an example for correct usage.'
678
            );
679
        }
680
681 108
        $isMultiple = is_array($this->dqlParts[$dqlPartName])
682 108
            && ! ($dqlPartName === 'join' && ! $append);
683
684
        // Allow adding any part retrieved from self::getDQLParts().
685 108
        if (is_array($dqlPart) && $dqlPartName !== 'join') {
686 1
            $dqlPart = reset($dqlPart);
687
        }
688
689
        // This is introduced for backwards compatibility reasons.
690
        // TODO: Remove for 3.0
691 108
        if ($dqlPartName === 'join') {
692 24
            $newDqlPart = [];
693
694 24
            foreach ($dqlPart as $k => $v) {
695 24
                $k = is_numeric($k) ? $this->getRootAlias() : $k;
0 ignored issues
show
Deprecated Code introduced by
The function Doctrine\ORM\QueryBuilder::getRootAlias() has been deprecated: Please use $qb->getRootAliases() instead. ( Ignorable by Annotation )

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

695
                $k = is_numeric($k) ? /** @scrutinizer ignore-deprecated */ $this->getRootAlias() : $k;

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
696
697 24
                $newDqlPart[$k] = $v;
698
            }
699
700 24
            $dqlPart = $newDqlPart;
701
        }
702
703 108
        if ($append && $isMultiple) {
704 102
            if (is_array($dqlPart)) {
705 24
                $key = key($dqlPart);
706
707 24
                $this->dqlParts[$dqlPartName][$key][] = $dqlPart[$key];
708
            } else {
709 102
                $this->dqlParts[$dqlPartName][] = $dqlPart;
710
            }
711
        } else {
712 106
            $this->dqlParts[$dqlPartName] = ($isMultiple) ? [$dqlPart] : $dqlPart;
713
        }
714
715 108
        $this->state = self::STATE_DIRTY;
716
717 108
        return $this;
718
    }
719
720
    /**
721
     * Specifies an item that is to be returned in the query result.
722
     * Replaces any previously specified selections, if any.
723
     *
724
     * <code>
725
     *     $qb = $em->createQueryBuilder()
726
     *         ->select('u', 'p')
727
     *         ->from('User', 'u')
728
     *         ->leftJoin('u.Phonenumbers', 'p');
729
     * </code>
730
     *
731
     * @param mixed $select The selection expressions.
732
     *
733
     * @return self
734
     */
735 102
    public function select($select = null)
736
    {
737 102
        $this->type = self::SELECT;
738
739 102
        if (empty($select)) {
740 1
            return $this;
741
        }
742
743 101
        $selects = is_array($select) ? $select : func_get_args();
744
745 101
        return $this->add('select', new Expr\Select($selects), false);
746
    }
747
748
    /**
749
     * Adds a DISTINCT flag to this query.
750
     *
751
     * <code>
752
     *     $qb = $em->createQueryBuilder()
753
     *         ->select('u')
754
     *         ->distinct()
755
     *         ->from('User', 'u');
756
     * </code>
757
     *
758
     * @param bool $flag
759
     *
760
     * @return self
761
     */
762 1
    public function distinct($flag = true)
763
    {
764 1
        $this->dqlParts['distinct'] = (bool) $flag;
765
766 1
        return $this;
767
    }
768
769
    /**
770
     * Adds an item that is to be returned in the query result.
771
     *
772
     * <code>
773
     *     $qb = $em->createQueryBuilder()
774
     *         ->select('u')
775
     *         ->addSelect('p')
776
     *         ->from('User', 'u')
777
     *         ->leftJoin('u.Phonenumbers', 'p');
778
     * </code>
779
     *
780
     * @param mixed $select The selection expression.
781
     *
782
     * @return self
783
     */
784
    public function addSelect($select = null)
785
    {
786
        $this->type = self::SELECT;
787
788
        if (empty($select)) {
789
            return $this;
790
        }
791
792
        $selects = is_array($select) ? $select : func_get_args();
793
794
        return $this->add('select', new Expr\Select($selects), true);
795
    }
796
797
    /**
798
     * Turns the query being built into a bulk delete query that ranges over
799
     * a certain entity type.
800
     *
801
     * <code>
802
     *     $qb = $em->createQueryBuilder()
803
     *         ->delete('User', 'u')
804
     *         ->where('u.id = :user_id')
805
     *         ->setParameter('user_id', 1);
806
     * </code>
807
     *
808
     * @param string $delete The class/type whose instances are subject to the deletion.
809
     * @param string $alias  The class/type alias used in the constructed query.
810
     *
811
     * @return self
812
     */
813 4
    public function delete($delete = null, $alias = null)
814
    {
815 4
        $this->type = self::DELETE;
816
817 4
        if (! $delete) {
818 1
            return $this;
819
        }
820
821 3
        return $this->add('from', new Expr\From($delete, $alias));
822
    }
823
824
    /**
825
     * Turns the query being built into a bulk update query that ranges over
826
     * a certain entity type.
827
     *
828
     * <code>
829
     *     $qb = $em->createQueryBuilder()
830
     *         ->update('User', 'u')
831
     *         ->set('u.password', '?1')
832
     *         ->where('u.id = ?2');
833
     * </code>
834
     *
835
     * @param string $update The class/type whose instances are subject to the update.
836
     * @param string $alias  The class/type alias used in the constructed query.
837
     *
838
     * @return self
839
     */
840 2
    public function update($update = null, $alias = null)
841
    {
842 2
        $this->type = self::UPDATE;
843
844 2
        if (! $update) {
845 1
            return $this;
846
        }
847
848 1
        return $this->add('from', new Expr\From($update, $alias));
849
    }
850
851
    /**
852
     * Creates and adds a query root corresponding to the entity identified by the given alias,
853
     * forming a cartesian product with any existing query roots.
854
     *
855
     * <code>
856
     *     $qb = $em->createQueryBuilder()
857
     *         ->select('u')
858
     *         ->from('User', 'u');
859
     * </code>
860
     *
861
     * @param string $from    The class name.
862
     * @param string $alias   The alias of the class.
863
     * @param string $indexBy The index for the from.
864
     *
865
     * @return self
866
     */
867 101
    public function from($from, $alias, $indexBy = null)
868
    {
869 101
        return $this->add('from', new Expr\From($from, $alias, $indexBy), true);
870
    }
871
872
    /**
873
     * Updates a query root corresponding to an entity setting its index by. This method is intended to be used with
874
     * EntityRepository->createQueryBuilder(), which creates the initial FROM clause and do not allow you to update it
875
     * setting an index by.
876
     *
877
     * <code>
878
     *     $qb = $userRepository->createQueryBuilder('u')
879
     *         ->indexBy('u', 'u.id');
880
     *
881
     *     // Is equivalent to...
882
     *
883
     *     $qb = $em->createQueryBuilder()
884
     *         ->select('u')
885
     *         ->from('User', 'u', 'u.id');
886
     * </code>
887
     *
888
     * @param string $alias   The root alias of the class.
889
     * @param string $indexBy The index for the from.
890
     *
891
     * @return self
892
     *
893
     * @throws Query\QueryException
894
     */
895 2
    public function indexBy($alias, $indexBy)
896
    {
897 2
        $rootAliases = $this->getRootAliases();
898
899 2
        if (! in_array($alias, $rootAliases, true)) {
900
            throw new Query\QueryException(
901
                sprintf('Specified root alias %s must be set before invoking indexBy().', $alias)
902
            );
903
        }
904
905 2
        foreach ($this->dqlParts['from'] as &$fromClause) {
906
            /** @var Expr\From $fromClause */
907 2
            if ($fromClause->getAlias() !== $alias) {
908 1
                continue;
909
            }
910
911 2
            $fromClause = new Expr\From($fromClause->getFrom(), $fromClause->getAlias(), $indexBy);
912
        }
913
914 2
        return $this;
915
    }
916
917
    /**
918
     * Creates and adds a join over an entity association to the query.
919
     *
920
     * The entities in the joined association will be fetched as part of the query
921
     * result if the alias used for the joined association is placed in the select
922
     * expressions.
923
     *
924
     * <code>
925
     *     $qb = $em->createQueryBuilder()
926
     *         ->select('u')
927
     *         ->from('User', 'u')
928
     *         ->join('u.Phonenumbers', 'p', Expr\Join::WITH, 'p.is_primary = 1');
929
     * </code>
930
     *
931
     * @param string      $join          The relationship to join.
932
     * @param string      $alias         The alias of the join.
933
     * @param string|null $conditionType The condition type constant. Either ON or WITH.
934
     * @param string|null $condition     The condition for the join.
935
     * @param string|null $indexBy       The index for the join.
936
     *
937
     * @return self
938
     */
939 8
    public function join($join, $alias, $conditionType = null, $condition = null, $indexBy = null)
940
    {
941 8
        return $this->innerJoin($join, $alias, $conditionType, $condition, $indexBy);
942
    }
943
944
    /**
945
     * Creates and adds a join over an entity association to the query.
946
     *
947
     * The entities in the joined association will be fetched as part of the query
948
     * result if the alias used for the joined association is placed in the select
949
     * expressions.
950
     *
951
     *     [php]
952
     *     $qb = $em->createQueryBuilder()
953
     *         ->select('u')
954
     *         ->from('User', 'u')
955
     *         ->innerJoin('u.Phonenumbers', 'p', Expr\Join::WITH, 'p.is_primary = 1');
956
     *
957
     * @param string      $join          The relationship to join.
958
     * @param string      $alias         The alias of the join.
959
     * @param string|null $conditionType The condition type constant. Either ON or WITH.
960
     * @param string|null $condition     The condition for the join.
961
     * @param string|null $indexBy       The index for the join.
962
     *
963
     * @return self
964
     */
965 15
    public function innerJoin($join, $alias, $conditionType = null, $condition = null, $indexBy = null)
966
    {
967 15
        $hasParentAlias = strpos($join, '.');
968 15
        $parentAlias    = substr($join, 0, $hasParentAlias === false ? 0 : $hasParentAlias);
969 15
        $rootAlias      = $this->findRootAlias($alias, $parentAlias);
970 15
        $join           = new Expr\Join(
971 15
            Expr\Join::INNER_JOIN,
972 15
            $join,
973 15
            $alias,
974 15
            $conditionType,
975 15
            $condition,
976 15
            $indexBy
977
        );
978
979 15
        return $this->add('join', [$rootAlias => $join], true);
980
    }
981
982
    /**
983
     * Creates and adds a left join over an entity association to the query.
984
     *
985
     * The entities in the joined association will be fetched as part of the query
986
     * result if the alias used for the joined association is placed in the select
987
     * expressions.
988
     *
989
     * <code>
990
     *     $qb = $em->createQueryBuilder()
991
     *         ->select('u')
992
     *         ->from('User', 'u')
993
     *         ->leftJoin('u.Phonenumbers', 'p', Expr\Join::WITH, 'p.is_primary = 1');
994
     * </code>
995
     *
996
     * @param string      $join          The relationship to join.
997
     * @param string      $alias         The alias of the join.
998
     * @param string|null $conditionType The condition type constant. Either ON or WITH.
999
     * @param string|null $condition     The condition for the join.
1000
     * @param string|null $indexBy       The index for the join.
1001
     *
1002
     * @return self
1003
     */
1004 9
    public function leftJoin($join, $alias, $conditionType = null, $condition = null, $indexBy = null)
1005
    {
1006 9
        $hasParentAlias = strpos($join, '.');
1007 9
        $parentAlias    = substr($join, 0, $hasParentAlias === false ? 0 : $hasParentAlias);
1008 9
        $rootAlias      = $this->findRootAlias($alias, $parentAlias);
1009 9
        $join           = new Expr\Join(Expr\Join::LEFT_JOIN, $join, $alias, $conditionType, $condition, $indexBy);
1010
1011 9
        return $this->add('join', [$rootAlias => $join], true);
1012
    }
1013
1014
    /**
1015
     * Sets a new value for a field in a bulk update query.
1016
     *
1017
     * <code>
1018
     *     $qb = $em->createQueryBuilder()
1019
     *         ->update('User', 'u')
1020
     *         ->set('u.password', '?1')
1021
     *         ->where('u.id = ?2');
1022
     * </code>
1023
     *
1024
     * @param string $key   The key/field to set.
1025
     * @param string $value The value, expression, placeholder, etc.
1026
     *
1027
     * @return self
1028
     */
1029 1
    public function set($key, $value)
1030
    {
1031 1
        return $this->add('set', new Expr\Comparison($key, Expr\Comparison::EQ, $value), true);
1032
    }
1033
1034
    /**
1035
     * Specifies one or more restrictions to the query result.
1036
     * Replaces any previously specified restrictions, if any.
1037
     *
1038
     * <code>
1039
     *     $qb = $em->createQueryBuilder()
1040
     *         ->select('u')
1041
     *         ->from('User', 'u')
1042
     *         ->where('u.id = ?');
1043
     *
1044
     *     // You can optionally programmatically build and/or expressions
1045
     *     $qb = $em->createQueryBuilder();
1046
     *
1047
     *     $or = $qb->expr()->orX();
1048
     *     $or->add($qb->expr()->eq('u.id', 1));
1049
     *     $or->add($qb->expr()->eq('u.id', 2));
1050
     *
1051
     *     $qb->update('User', 'u')
1052
     *         ->set('u.password', '?')
1053
     *         ->where($or);
1054
     * </code>
1055
     *
1056
     * @param mixed $predicates The restriction predicates.
1057
     */
1058 37
    public function where($predicates)
1059
    {
1060 37
        if (! (func_num_args() === 1 && $predicates instanceof Expr\Composite)) {
1061 34
            $predicates = new Expr\Andx(func_get_args());
1062
        }
1063
1064 37
        return $this->add('where', $predicates);
1065
    }
1066
1067
    /**
1068
     * Adds one or more restrictions to the query results, forming a logical
1069
     * conjunction with any previously specified restrictions.
1070
     *
1071
     * <code>
1072
     *     $qb = $em->createQueryBuilder()
1073
     *         ->select('u')
1074
     *         ->from('User', 'u')
1075
     *         ->where('u.username LIKE ?')
1076
     *         ->andWhere('u.is_active = 1');
1077
     * </code>
1078
     *
1079
     * @//param mixed $where The query restrictions.
1080
     *
1081
     * @see where()
1082
     */
1083 22
    public function andWhere()
1084
    {
1085 22
        $args  = func_get_args();
1086 22
        $where = $this->getDQLPart('where');
1087
1088 22
        if ($where instanceof Expr\Andx) {
1089 11
            $where->addMultiple($args);
1090
        } else {
1091 14
            array_unshift($args, $where);
1092 14
            $where = new Expr\Andx($args);
1093
        }
1094
1095 22
        return $this->add('where', $where);
1096
    }
1097
1098
    /**
1099
     * Adds one or more restrictions to the query results, forming a logical
1100
     * disjunction with any previously specified restrictions.
1101
     *
1102
     * <code>
1103
     *     $qb = $em->createQueryBuilder()
1104
     *         ->select('u')
1105
     *         ->from('User', 'u')
1106
     *         ->where('u.id = 1')
1107
     *         ->orWhere('u.id = 2');
1108
     * </code>
1109
     *
1110
     * @//param mixed $where The WHERE statement.
1111
     *
1112
     * @see where()
1113
     */
1114 5
    public function orWhere()
1115
    {
1116 5
        $args  = func_get_args();
1117 5
        $where = $this->getDQLPart('where');
1118
1119 5
        if ($where instanceof Expr\Orx) {
1120
            $where->addMultiple($args);
1121
        } else {
1122 5
            array_unshift($args, $where);
1123 5
            $where = new Expr\Orx($args);
1124
        }
1125
1126 5
        return $this->add('where', $where);
1127
    }
1128
1129
    /**
1130
     * Specifies a grouping over the results of the query.
1131
     * Replaces any previously specified groupings, if any.
1132
     *
1133
     * <code>
1134
     *     $qb = $em->createQueryBuilder()
1135
     *         ->select('u')
1136
     *         ->from('User', 'u')
1137
     *         ->groupBy('u.id');
1138
     * </code>
1139
     *
1140
     * @param string $groupBy The grouping expression.
1141
     *
1142
     * @return self
1143
     */
1144 7
    public function groupBy($groupBy)
1145
    {
1146 7
        return $this->add('groupBy', new Expr\GroupBy(func_get_args()));
1147
    }
1148
1149
    /**
1150
     * Adds a grouping expression to the query.
1151
     *
1152
     * <code>
1153
     *     $qb = $em->createQueryBuilder()
1154
     *         ->select('u')
1155
     *         ->from('User', 'u')
1156
     *         ->groupBy('u.lastLogin')
1157
     *         ->addGroupBy('u.createdAt');
1158
     * </code>
1159
     *
1160
     * @param string $groupBy The grouping expression.
1161
     *
1162
     * @return self
1163
     */
1164 1
    public function addGroupBy($groupBy)
1165
    {
1166 1
        return $this->add('groupBy', new Expr\GroupBy(func_get_args()), true);
1167
    }
1168
1169
    /**
1170
     * Specifies a restriction over the groups of the query.
1171
     * Replaces any previous having restrictions, if any.
1172
     *
1173
     * @param mixed $having The restriction over the groups.
1174
     *
1175
     * @return self
1176
     */
1177 3
    public function having($having)
1178
    {
1179 3
        if (! (func_num_args() === 1 && ($having instanceof Expr\Andx || $having instanceof Expr\Orx))) {
1180 3
            $having = new Expr\Andx(func_get_args());
1181
        }
1182
1183 3
        return $this->add('having', $having);
1184
    }
1185
1186
    /**
1187
     * Adds a restriction over the groups of the query, forming a logical
1188
     * conjunction with any existing having restrictions.
1189
     *
1190
     * @param mixed $having The restriction to append.
1191
     *
1192
     * @return self
1193
     */
1194 2
    public function andHaving($having)
1195
    {
1196 2
        $args   = func_get_args();
1197 2
        $having = $this->getDQLPart('having');
1198
1199 2
        if ($having instanceof Expr\Andx) {
1200 2
            $having->addMultiple($args);
1201
        } else {
1202
            array_unshift($args, $having);
1203
            $having = new Expr\Andx($args);
1204
        }
1205
1206 2
        return $this->add('having', $having);
1207
    }
1208
1209
    /**
1210
     * Adds a restriction over the groups of the query, forming a logical
1211
     * disjunction with any existing having restrictions.
1212
     *
1213
     * @param mixed $having The restriction to add.
1214
     *
1215
     * @return self
1216
     */
1217 1
    public function orHaving($having)
1218
    {
1219 1
        $args   = func_get_args();
1220 1
        $having = $this->getDQLPart('having');
1221
1222 1
        if ($having instanceof Expr\Orx) {
1223
            $having->addMultiple($args);
1224
        } else {
1225 1
            array_unshift($args, $having);
1226 1
            $having = new Expr\Orx($args);
1227
        }
1228
1229 1
        return $this->add('having', $having);
1230
    }
1231
1232
    /**
1233
     * Specifies an ordering for the query results.
1234
     * Replaces any previously specified orderings, if any.
1235
     *
1236
     * @param string|Expr\OrderBy $sort  The ordering expression.
1237
     * @param string              $order The ordering direction.
1238
     *
1239
     * @return self
1240
     */
1241 10
    public function orderBy($sort, $order = null)
1242
    {
1243 10
        $orderBy = ($sort instanceof Expr\OrderBy) ? $sort : new Expr\OrderBy($sort, $order);
1244
1245 10
        return $this->add('orderBy', $orderBy);
1246
    }
1247
1248
    /**
1249
     * Adds an ordering to the query results.
1250
     *
1251
     * @param string|Expr\OrderBy $sort  The ordering expression.
1252
     * @param string              $order The ordering direction.
1253
     *
1254
     * @return self
1255
     */
1256 4
    public function addOrderBy($sort, $order = null)
1257
    {
1258 4
        $orderBy = ($sort instanceof Expr\OrderBy) ? $sort : new Expr\OrderBy($sort, $order);
1259
1260 4
        return $this->add('orderBy', $orderBy, true);
1261
    }
1262
1263
    /**
1264
     * Adds criteria to the query.
1265
     *
1266
     * Adds where expressions with AND operator.
1267
     * Adds orderings.
1268
     * Overrides firstResult and maxResults if they're set.
1269
     *
1270
     * @return self
1271
     *
1272
     * @throws Query\QueryException
1273
     */
1274 13
    public function addCriteria(Criteria $criteria)
1275
    {
1276 13
        $allAliases = $this->getAllAliases();
1277 13
        if (! isset($allAliases[0])) {
1278
            throw new Query\QueryException('No aliases are set before invoking addCriteria().');
1279
        }
1280
1281 13
        $visitor         = new QueryExpressionVisitor($this->getAllAliases());
1282 13
        $whereExpression = $criteria->getWhereExpression();
1283
1284 13
        if ($whereExpression) {
1285 9
            $this->andWhere($visitor->dispatch($whereExpression));
1286 9
            foreach ($visitor->getParameters() as $parameter) {
1287 9
                $this->parameters->add($parameter);
1288
            }
1289
        }
1290
1291 13
        if ($criteria->getOrderings()) {
1292 2
            foreach ($criteria->getOrderings() as $sort => $order) {
1293 2
                $hasValidAlias = false;
1294 2
                foreach ($allAliases as $alias) {
1295 2
                    if (strpos($sort . '.', $alias . '.') === 0) {
1296 1
                        $hasValidAlias = true;
1297 2
                        break;
1298
                    }
1299
                }
1300
1301 2
                if (! $hasValidAlias) {
1302 1
                    $sort = $allAliases[0] . '.' . $sort;
1303
                }
1304
1305 2
                $this->addOrderBy($sort, $order);
1306
            }
1307
        }
1308
1309 13
        $firstResult = $criteria->getFirstResult();
1310 13
        $maxResults  = $criteria->getMaxResults();
1311
1312
        // Overwrite limits only if they was set in criteria
1313 13
        if ($firstResult !== null) {
1314 1
            $this->setFirstResult($firstResult);
1315
        }
1316 13
        if ($maxResults !== null) {
1317 1
            $this->setMaxResults($maxResults);
1318
        }
1319
1320 13
        return $this;
1321
    }
1322
1323
    /**
1324
     * Gets a query part by its name.
1325
     *
1326
     * @return mixed $queryPart
1327
     *
1328
     * @todo Rename: getQueryPart (or remove?)
1329
     */
1330 90
    public function getDQLPart($queryPartName)
1331
    {
1332 90
        return $this->dqlParts[$queryPartName];
1333
    }
1334
1335
    /**
1336
     * Gets all query parts.
1337
     *
1338
     * @return mixed[] $dqlParts
1339
     *
1340
     * @todo Rename: getQueryParts (or remove?)
1341
     */
1342 1
    public function getDQLParts()
1343
    {
1344 1
        return $this->dqlParts;
1345
    }
1346
1347
    /**
1348
     * @return string
1349
     */
1350 1
    private function getDQLForDelete()
1351
    {
1352
        return 'DELETE'
1353 1
              . $this->getReducedDQLQueryPart('from', ['pre' => ' ', 'separator' => ', '])
1354 1
              . $this->getReducedDQLQueryPart('where', ['pre' => ' WHERE '])
1355 1
              . $this->getReducedDQLQueryPart('orderBy', ['pre' => ' ORDER BY ', 'separator' => ', ']);
1356
    }
1357
1358
    /**
1359
     * @return string
1360
     */
1361 1
    private function getDQLForUpdate()
1362
    {
1363
        return 'UPDATE'
1364 1
              . $this->getReducedDQLQueryPart('from', ['pre' => ' ', 'separator' => ', '])
1365 1
              . $this->getReducedDQLQueryPart('set', ['pre' => ' SET ', 'separator' => ', '])
1366 1
              . $this->getReducedDQLQueryPart('where', ['pre' => ' WHERE '])
1367 1
              . $this->getReducedDQLQueryPart('orderBy', ['pre' => ' ORDER BY ', 'separator' => ', ']);
1368
    }
1369
1370
    /**
1371
     * @return string
1372
     */
1373 70
    private function getDQLForSelect()
1374
    {
1375
        $dql = 'SELECT'
1376 70
             . ($this->dqlParts['distinct']===true ? ' DISTINCT' : '')
1377 70
             . $this->getReducedDQLQueryPart('select', ['pre' => ' ', 'separator' => ', ']);
1378
1379 70
        $fromParts   = $this->getDQLPart('from');
1380 70
        $joinParts   = $this->getDQLPart('join');
1381 70
        $fromClauses = [];
1382
1383
        // Loop through all FROM clauses
1384 70
        if (! empty($fromParts)) {
1385 69
            $dql .= ' FROM ';
1386
1387 69
            foreach ($fromParts as $from) {
1388 69
                $fromClause = (string) $from;
1389
1390 69
                if ($from instanceof Expr\From && isset($joinParts[$from->getAlias()])) {
1391 19
                    foreach ($joinParts[$from->getAlias()] as $join) {
1392 19
                        $fromClause .= ' ' . ((string) $join);
1393
                    }
1394
                }
1395
1396 69
                $fromClauses[] = $fromClause;
1397
            }
1398
        }
1399
1400 70
        $dql .= implode(', ', $fromClauses)
1401 70
              . $this->getReducedDQLQueryPart('where', ['pre' => ' WHERE '])
1402 70
              . $this->getReducedDQLQueryPart('groupBy', ['pre' => ' GROUP BY ', 'separator' => ', '])
1403 70
              . $this->getReducedDQLQueryPart('having', ['pre' => ' HAVING '])
1404 70
              . $this->getReducedDQLQueryPart('orderBy', ['pre' => ' ORDER BY ', 'separator' => ', ']);
1405
1406 70
        return $dql;
1407
    }
1408
1409
    /**
1410
     * @param string  $queryPartName
1411
     * @param mixed[] $options
1412
     *
1413
     * @return string
1414
     */
1415 72
    private function getReducedDQLQueryPart($queryPartName, $options = [])
1416
    {
1417 72
        $queryPart = $this->getDQLPart($queryPartName);
1418
1419 72
        if (empty($queryPart)) {
1420 72
            return $options['empty'] ?? '';
1421
        }
1422
1423 72
        return ($options['pre'] ?? '')
1424 72
             . (is_array($queryPart) ? implode($options['separator'], $queryPart) : $queryPart)
1425 72
             . ($options['post'] ?? '');
1426
    }
1427
1428
    /**
1429
     * Resets DQL parts.
1430
     *
1431
     * @param string[]|null $parts
1432
     */
1433 2
    public function resetDQLParts($parts = null)
1434
    {
1435 2
        if ($parts === null) {
1436 1
            $parts = array_keys($this->dqlParts);
1437
        }
1438
1439 2
        foreach ($parts as $part) {
1440 2
            $this->resetDQLPart($part);
1441
        }
1442
1443 2
        return $this;
1444
    }
1445
1446
    /**
1447
     * Resets single DQL part.
1448
     *
1449
     * @param string $part
1450
     *
1451
     * @return self
1452
     */
1453 3
    public function resetDQLPart($part)
1454
    {
1455 3
        $this->dqlParts[$part] = is_array($this->dqlParts[$part]) ? [] : null;
1456 3
        $this->state           = self::STATE_DIRTY;
1457
1458 3
        return $this;
1459
    }
1460
1461
    /**
1462
     * Gets a string representation of this QueryBuilder which corresponds to
1463
     * the final DQL query being constructed.
1464
     *
1465
     * @return string The string representation of this QueryBuilder.
1466
     */
1467 5
    public function __toString()
1468
    {
1469 5
        return $this->getDQL();
1470
    }
1471
1472
    /**
1473
     * Deep clones all expression objects in the DQL parts.
1474
     *
1475
     */
1476 3
    public function __clone()
1477
    {
1478 3
        foreach ($this->dqlParts as $part => $elements) {
1479 3
            if (is_array($this->dqlParts[$part])) {
1480 3
                foreach ($this->dqlParts[$part] as $idx => $element) {
1481 2
                    if (! is_object($element)) {
1482
                        continue;
1483
                    }
1484
1485 3
                    $this->dqlParts[$part][$idx] = clone $element;
1486
                }
1487 3
            } elseif (is_object($elements)) {
1488 3
                $this->dqlParts[$part] = clone $elements;
1489
            }
1490
        }
1491
1492 3
        $parameters = [];
1493
1494 3
        foreach ($this->parameters as $parameter) {
1495 1
            $parameters[] = clone $parameter;
1496
        }
1497
1498 3
        $this->parameters = new ArrayCollection($parameters);
1499 3
    }
1500
}
1501