summaryrefslogtreecommitdiff
path: root/vendor/doctrine/orm/src/QueryBuilder.php
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/doctrine/orm/src/QueryBuilder.php')
-rw-r--r--vendor/doctrine/orm/src/QueryBuilder.php1375
1 files changed, 1375 insertions, 0 deletions
diff --git a/vendor/doctrine/orm/src/QueryBuilder.php b/vendor/doctrine/orm/src/QueryBuilder.php
new file mode 100644
index 0000000..a6a39a9
--- /dev/null
+++ b/vendor/doctrine/orm/src/QueryBuilder.php
@@ -0,0 +1,1375 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM;
6
7use Doctrine\Common\Collections\ArrayCollection;
8use Doctrine\Common\Collections\Criteria;
9use Doctrine\DBAL\ArrayParameterType;
10use Doctrine\DBAL\ParameterType;
11use Doctrine\ORM\Internal\NoUnknownNamedArguments;
12use Doctrine\ORM\Internal\QueryType;
13use Doctrine\ORM\Query\Expr;
14use Doctrine\ORM\Query\Parameter;
15use Doctrine\ORM\Query\QueryExpressionVisitor;
16use InvalidArgumentException;
17use RuntimeException;
18use Stringable;
19
20use function array_keys;
21use function array_unshift;
22use function assert;
23use function count;
24use function implode;
25use function in_array;
26use function is_array;
27use function is_numeric;
28use function is_object;
29use function is_string;
30use function key;
31use function reset;
32use function sprintf;
33use function str_starts_with;
34use function strpos;
35use function strrpos;
36use function substr;
37
38/**
39 * This class is responsible for building DQL query strings via an object oriented
40 * PHP interface.
41 */
42class QueryBuilder implements Stringable
43{
44 use NoUnknownNamedArguments;
45
46 /**
47 * The array of DQL parts collected.
48 *
49 * @psalm-var array<string, mixed>
50 */
51 private array $dqlParts = [
52 'distinct' => false,
53 'select' => [],
54 'from' => [],
55 'join' => [],
56 'set' => [],
57 'where' => null,
58 'groupBy' => [],
59 'having' => null,
60 'orderBy' => [],
61 ];
62
63 private QueryType $type = QueryType::Select;
64
65 /**
66 * The complete DQL string for this query.
67 */
68 private string|null $dql = null;
69
70 /**
71 * The query parameters.
72 *
73 * @psalm-var ArrayCollection<int, Parameter>
74 */
75 private ArrayCollection $parameters;
76
77 /**
78 * The index of the first result to retrieve.
79 */
80 private int $firstResult = 0;
81
82 /**
83 * The maximum number of results to retrieve.
84 */
85 private int|null $maxResults = null;
86
87 /**
88 * Keeps root entity alias names for join entities.
89 *
90 * @psalm-var array<string, string>
91 */
92 private array $joinRootAliases = [];
93
94 /**
95 * Whether to use second level cache, if available.
96 */
97 protected bool $cacheable = false;
98
99 /**
100 * Second level cache region name.
101 */
102 protected string|null $cacheRegion = null;
103
104 /**
105 * Second level query cache mode.
106 *
107 * @psalm-var Cache::MODE_*|null
108 */
109 protected int|null $cacheMode = null;
110
111 protected int $lifetime = 0;
112
113 /**
114 * Initializes a new <tt>QueryBuilder</tt> that uses the given <tt>EntityManager</tt>.
115 *
116 * @param EntityManagerInterface $em The EntityManager to use.
117 */
118 public function __construct(
119 private readonly EntityManagerInterface $em,
120 ) {
121 $this->parameters = new ArrayCollection();
122 }
123
124 /**
125 * Gets an ExpressionBuilder used for object-oriented construction of query expressions.
126 * This producer method is intended for convenient inline usage. Example:
127 *
128 * <code>
129 * $qb = $em->createQueryBuilder();
130 * $qb
131 * ->select('u')
132 * ->from('User', 'u')
133 * ->where($qb->expr()->eq('u.id', 1));
134 * </code>
135 *
136 * For more complex expression construction, consider storing the expression
137 * builder object in a local variable.
138 */
139 public function expr(): Expr
140 {
141 return $this->em->getExpressionBuilder();
142 }
143
144 /**
145 * Enable/disable second level query (result) caching for this query.
146 *
147 * @return $this
148 */
149 public function setCacheable(bool $cacheable): static
150 {
151 $this->cacheable = $cacheable;
152
153 return $this;
154 }
155
156 /**
157 * Are the query results enabled for second level cache?
158 */
159 public function isCacheable(): bool
160 {
161 return $this->cacheable;
162 }
163
164 /** @return $this */
165 public function setCacheRegion(string $cacheRegion): static
166 {
167 $this->cacheRegion = $cacheRegion;
168
169 return $this;
170 }
171
172 /**
173 * Obtain the name of the second level query cache region in which query results will be stored
174 *
175 * @return string|null The cache region name; NULL indicates the default region.
176 */
177 public function getCacheRegion(): string|null
178 {
179 return $this->cacheRegion;
180 }
181
182 public function getLifetime(): int
183 {
184 return $this->lifetime;
185 }
186
187 /**
188 * Sets the life-time for this query into second level cache.
189 *
190 * @return $this
191 */
192 public function setLifetime(int $lifetime): static
193 {
194 $this->lifetime = $lifetime;
195
196 return $this;
197 }
198
199 /** @psalm-return Cache::MODE_*|null */
200 public function getCacheMode(): int|null
201 {
202 return $this->cacheMode;
203 }
204
205 /**
206 * @psalm-param Cache::MODE_* $cacheMode
207 *
208 * @return $this
209 */
210 public function setCacheMode(int $cacheMode): static
211 {
212 $this->cacheMode = $cacheMode;
213
214 return $this;
215 }
216
217 /**
218 * Gets the associated EntityManager for this query builder.
219 */
220 public function getEntityManager(): EntityManagerInterface
221 {
222 return $this->em;
223 }
224
225 /**
226 * Gets the complete DQL string formed by the current specifications of this QueryBuilder.
227 *
228 * <code>
229 * $qb = $em->createQueryBuilder()
230 * ->select('u')
231 * ->from('User', 'u');
232 * echo $qb->getDql(); // SELECT u FROM User u
233 * </code>
234 */
235 public function getDQL(): string
236 {
237 return $this->dql ??= match ($this->type) {
238 QueryType::Select => $this->getDQLForSelect(),
239 QueryType::Delete => $this->getDQLForDelete(),
240 QueryType::Update => $this->getDQLForUpdate(),
241 };
242 }
243
244 /**
245 * Constructs a Query instance from the current specifications of the builder.
246 *
247 * <code>
248 * $qb = $em->createQueryBuilder()
249 * ->select('u')
250 * ->from('User', 'u');
251 * $q = $qb->getQuery();
252 * $results = $q->execute();
253 * </code>
254 */
255 public function getQuery(): Query
256 {
257 $parameters = clone $this->parameters;
258 $query = $this->em->createQuery($this->getDQL())
259 ->setParameters($parameters)
260 ->setFirstResult($this->firstResult)
261 ->setMaxResults($this->maxResults);
262
263 if ($this->lifetime) {
264 $query->setLifetime($this->lifetime);
265 }
266
267 if ($this->cacheMode) {
268 $query->setCacheMode($this->cacheMode);
269 }
270
271 if ($this->cacheable) {
272 $query->setCacheable($this->cacheable);
273 }
274
275 if ($this->cacheRegion) {
276 $query->setCacheRegion($this->cacheRegion);
277 }
278
279 return $query;
280 }
281
282 /**
283 * Finds the root entity alias of the joined entity.
284 *
285 * @param string $alias The alias of the new join entity
286 * @param string $parentAlias The parent entity alias of the join relationship
287 */
288 private function findRootAlias(string $alias, string $parentAlias): string
289 {
290 if (in_array($parentAlias, $this->getRootAliases(), true)) {
291 $rootAlias = $parentAlias;
292 } elseif (isset($this->joinRootAliases[$parentAlias])) {
293 $rootAlias = $this->joinRootAliases[$parentAlias];
294 } else {
295 // Should never happen with correct joining order. Might be
296 // thoughtful to throw exception instead.
297 $rootAlias = $this->getRootAlias();
298 }
299
300 $this->joinRootAliases[$alias] = $rootAlias;
301
302 return $rootAlias;
303 }
304
305 /**
306 * Gets the FIRST root alias of the query. This is the first entity alias involved
307 * in the construction of the query.
308 *
309 * <code>
310 * $qb = $em->createQueryBuilder()
311 * ->select('u')
312 * ->from('User', 'u');
313 *
314 * echo $qb->getRootAlias(); // u
315 * </code>
316 *
317 * @deprecated Please use $qb->getRootAliases() instead.
318 *
319 * @throws RuntimeException
320 */
321 public function getRootAlias(): string
322 {
323 $aliases = $this->getRootAliases();
324
325 if (! isset($aliases[0])) {
326 throw new RuntimeException('No alias was set before invoking getRootAlias().');
327 }
328
329 return $aliases[0];
330 }
331
332 /**
333 * Gets the root aliases of the query. This is the entity aliases involved
334 * in the construction of the query.
335 *
336 * <code>
337 * $qb = $em->createQueryBuilder()
338 * ->select('u')
339 * ->from('User', 'u');
340 *
341 * $qb->getRootAliases(); // array('u')
342 * </code>
343 *
344 * @return string[]
345 * @psalm-return list<string>
346 */
347 public function getRootAliases(): array
348 {
349 $aliases = [];
350
351 foreach ($this->dqlParts['from'] as &$fromClause) {
352 if (is_string($fromClause)) {
353 $spacePos = strrpos($fromClause, ' ');
354
355 /** @psalm-var class-string $from */
356 $from = substr($fromClause, 0, $spacePos);
357 $alias = substr($fromClause, $spacePos + 1);
358
359 $fromClause = new Query\Expr\From($from, $alias);
360 }
361
362 $aliases[] = $fromClause->getAlias();
363 }
364
365 return $aliases;
366 }
367
368 /**
369 * Gets all the aliases that have been used in the query.
370 * Including all select root aliases and join aliases
371 *
372 * <code>
373 * $qb = $em->createQueryBuilder()
374 * ->select('u')
375 * ->from('User', 'u')
376 * ->join('u.articles','a');
377 *
378 * $qb->getAllAliases(); // array('u','a')
379 * </code>
380 *
381 * @return string[]
382 * @psalm-return list<string>
383 */
384 public function getAllAliases(): array
385 {
386 return [...$this->getRootAliases(), ...array_keys($this->joinRootAliases)];
387 }
388
389 /**
390 * Gets the root entities of the query. This is the entity classes involved
391 * in the construction of the query.
392 *
393 * <code>
394 * $qb = $em->createQueryBuilder()
395 * ->select('u')
396 * ->from('User', 'u');
397 *
398 * $qb->getRootEntities(); // array('User')
399 * </code>
400 *
401 * @return string[]
402 * @psalm-return list<class-string>
403 */
404 public function getRootEntities(): array
405 {
406 $entities = [];
407
408 foreach ($this->dqlParts['from'] as &$fromClause) {
409 if (is_string($fromClause)) {
410 $spacePos = strrpos($fromClause, ' ');
411
412 /** @psalm-var class-string $from */
413 $from = substr($fromClause, 0, $spacePos);
414 $alias = substr($fromClause, $spacePos + 1);
415
416 $fromClause = new Query\Expr\From($from, $alias);
417 }
418
419 $entities[] = $fromClause->getFrom();
420 }
421
422 return $entities;
423 }
424
425 /**
426 * Sets a query parameter for the query being constructed.
427 *
428 * <code>
429 * $qb = $em->createQueryBuilder()
430 * ->select('u')
431 * ->from('User', 'u')
432 * ->where('u.id = :user_id')
433 * ->setParameter('user_id', 1);
434 * </code>
435 *
436 * @param string|int $key The parameter position or name.
437 * @param ParameterType|ArrayParameterType|string|int|null $type ParameterType::*, ArrayParameterType::* or \Doctrine\DBAL\Types\Type::* constant
438 *
439 * @return $this
440 */
441 public function setParameter(string|int $key, mixed $value, ParameterType|ArrayParameterType|string|int|null $type = null): static
442 {
443 $existingParameter = $this->getParameter($key);
444
445 if ($existingParameter !== null) {
446 $existingParameter->setValue($value, $type);
447
448 return $this;
449 }
450
451 $this->parameters->add(new Parameter($key, $value, $type));
452
453 return $this;
454 }
455
456 /**
457 * Sets a collection of query parameters for the query being constructed.
458 *
459 * <code>
460 * $qb = $em->createQueryBuilder()
461 * ->select('u')
462 * ->from('User', 'u')
463 * ->where('u.id = :user_id1 OR u.id = :user_id2')
464 * ->setParameters(new ArrayCollection(array(
465 * new Parameter('user_id1', 1),
466 * new Parameter('user_id2', 2)
467 * )));
468 * </code>
469 *
470 * @psalm-param ArrayCollection<int, Parameter> $parameters
471 *
472 * @return $this
473 */
474 public function setParameters(ArrayCollection $parameters): static
475 {
476 $this->parameters = $parameters;
477
478 return $this;
479 }
480
481 /**
482 * Gets all defined query parameters for the query being constructed.
483 *
484 * @psalm-return ArrayCollection<int, Parameter>
485 */
486 public function getParameters(): ArrayCollection
487 {
488 return $this->parameters;
489 }
490
491 /**
492 * Gets a (previously set) query parameter of the query being constructed.
493 */
494 public function getParameter(string|int $key): Parameter|null
495 {
496 $key = Parameter::normalizeName($key);
497
498 $filteredParameters = $this->parameters->filter(
499 static fn (Parameter $parameter): bool => $key === $parameter->getName()
500 );
501
502 return ! $filteredParameters->isEmpty() ? $filteredParameters->first() : null;
503 }
504
505 /**
506 * Sets the position of the first result to retrieve (the "offset").
507 *
508 * @return $this
509 */
510 public function setFirstResult(int|null $firstResult): static
511 {
512 $this->firstResult = (int) $firstResult;
513
514 return $this;
515 }
516
517 /**
518 * Gets the position of the first result the query object was set to retrieve (the "offset").
519 */
520 public function getFirstResult(): int
521 {
522 return $this->firstResult;
523 }
524
525 /**
526 * Sets the maximum number of results to retrieve (the "limit").
527 *
528 * @return $this
529 */
530 public function setMaxResults(int|null $maxResults): static
531 {
532 $this->maxResults = $maxResults;
533
534 return $this;
535 }
536
537 /**
538 * Gets the maximum number of results the query object was set to retrieve (the "limit").
539 * Returns NULL if {@link setMaxResults} was not applied to this query builder.
540 */
541 public function getMaxResults(): int|null
542 {
543 return $this->maxResults;
544 }
545
546 /**
547 * Either appends to or replaces a single, generic query part.
548 *
549 * The available parts are: 'select', 'from', 'join', 'set', 'where',
550 * 'groupBy', 'having' and 'orderBy'.
551 *
552 * @psalm-param string|object|list<string>|array{join: array<int|string, object>} $dqlPart
553 *
554 * @return $this
555 */
556 public function add(string $dqlPartName, string|object|array $dqlPart, bool $append = false): static
557 {
558 if ($append && ($dqlPartName === 'where' || $dqlPartName === 'having')) {
559 throw new InvalidArgumentException(
560 "Using \$append = true does not have an effect with 'where' or 'having' " .
561 'parts. See QueryBuilder#andWhere() for an example for correct usage.',
562 );
563 }
564
565 $isMultiple = is_array($this->dqlParts[$dqlPartName])
566 && ! ($dqlPartName === 'join' && ! $append);
567
568 // Allow adding any part retrieved from self::getDQLParts().
569 if (is_array($dqlPart) && $dqlPartName !== 'join') {
570 $dqlPart = reset($dqlPart);
571 }
572
573 // This is introduced for backwards compatibility reasons.
574 // TODO: Remove for 3.0
575 if ($dqlPartName === 'join') {
576 $newDqlPart = [];
577
578 foreach ($dqlPart as $k => $v) {
579 $k = is_numeric($k) ? $this->getRootAlias() : $k;
580
581 $newDqlPart[$k] = $v;
582 }
583
584 $dqlPart = $newDqlPart;
585 }
586
587 if ($append && $isMultiple) {
588 if (is_array($dqlPart)) {
589 $key = key($dqlPart);
590
591 $this->dqlParts[$dqlPartName][$key][] = $dqlPart[$key];
592 } else {
593 $this->dqlParts[$dqlPartName][] = $dqlPart;
594 }
595 } else {
596 $this->dqlParts[$dqlPartName] = $isMultiple ? [$dqlPart] : $dqlPart;
597 }
598
599 $this->dql = null;
600
601 return $this;
602 }
603
604 /**
605 * Specifies an item that is to be returned in the query result.
606 * Replaces any previously specified selections, if any.
607 *
608 * <code>
609 * $qb = $em->createQueryBuilder()
610 * ->select('u', 'p')
611 * ->from('User', 'u')
612 * ->leftJoin('u.Phonenumbers', 'p');
613 * </code>
614 *
615 * @return $this
616 */
617 public function select(mixed ...$select): static
618 {
619 self::validateVariadicParameter($select);
620
621 $this->type = QueryType::Select;
622
623 if ($select === []) {
624 return $this;
625 }
626
627 return $this->add('select', new Expr\Select($select), false);
628 }
629
630 /**
631 * Adds a DISTINCT flag to this query.
632 *
633 * <code>
634 * $qb = $em->createQueryBuilder()
635 * ->select('u')
636 * ->distinct()
637 * ->from('User', 'u');
638 * </code>
639 *
640 * @return $this
641 */
642 public function distinct(bool $flag = true): static
643 {
644 if ($this->dqlParts['distinct'] !== $flag) {
645 $this->dqlParts['distinct'] = $flag;
646 $this->dql = null;
647 }
648
649 return $this;
650 }
651
652 /**
653 * Adds an item that is to be returned in the query result.
654 *
655 * <code>
656 * $qb = $em->createQueryBuilder()
657 * ->select('u')
658 * ->addSelect('p')
659 * ->from('User', 'u')
660 * ->leftJoin('u.Phonenumbers', 'p');
661 * </code>
662 *
663 * @return $this
664 */
665 public function addSelect(mixed ...$select): static
666 {
667 self::validateVariadicParameter($select);
668
669 $this->type = QueryType::Select;
670
671 if ($select === []) {
672 return $this;
673 }
674
675 return $this->add('select', new Expr\Select($select), true);
676 }
677
678 /**
679 * Turns the query being built into a bulk delete query that ranges over
680 * a certain entity type.
681 *
682 * <code>
683 * $qb = $em->createQueryBuilder()
684 * ->delete('User', 'u')
685 * ->where('u.id = :user_id')
686 * ->setParameter('user_id', 1);
687 * </code>
688 *
689 * @param class-string|null $delete The class/type whose instances are subject to the deletion.
690 * @param string|null $alias The class/type alias used in the constructed query.
691 *
692 * @return $this
693 */
694 public function delete(string|null $delete = null, string|null $alias = null): static
695 {
696 $this->type = QueryType::Delete;
697
698 if (! $delete) {
699 return $this;
700 }
701
702 if (! $alias) {
703 throw new InvalidArgumentException(sprintf(
704 '%s(): The alias for entity %s must not be omitted.',
705 __METHOD__,
706 $delete,
707 ));
708 }
709
710 return $this->add('from', new Expr\From($delete, $alias));
711 }
712
713 /**
714 * Turns the query being built into a bulk update query that ranges over
715 * a certain entity type.
716 *
717 * <code>
718 * $qb = $em->createQueryBuilder()
719 * ->update('User', 'u')
720 * ->set('u.password', '?1')
721 * ->where('u.id = ?2');
722 * </code>
723 *
724 * @param class-string|null $update The class/type whose instances are subject to the update.
725 * @param string|null $alias The class/type alias used in the constructed query.
726 *
727 * @return $this
728 */
729 public function update(string|null $update = null, string|null $alias = null): static
730 {
731 $this->type = QueryType::Update;
732
733 if (! $update) {
734 return $this;
735 }
736
737 if (! $alias) {
738 throw new InvalidArgumentException(sprintf(
739 '%s(): The alias for entity %s must not be omitted.',
740 __METHOD__,
741 $update,
742 ));
743 }
744
745 return $this->add('from', new Expr\From($update, $alias));
746 }
747
748 /**
749 * Creates and adds a query root corresponding to the entity identified by the given alias,
750 * forming a cartesian product with any existing query roots.
751 *
752 * <code>
753 * $qb = $em->createQueryBuilder()
754 * ->select('u')
755 * ->from('User', 'u');
756 * </code>
757 *
758 * @param class-string $from The class name.
759 * @param string $alias The alias of the class.
760 * @param string|null $indexBy The index for the from.
761 *
762 * @return $this
763 */
764 public function from(string $from, string $alias, string|null $indexBy = null): static
765 {
766 return $this->add('from', new Expr\From($from, $alias, $indexBy), true);
767 }
768
769 /**
770 * Updates a query root corresponding to an entity setting its index by. This method is intended to be used with
771 * EntityRepository->createQueryBuilder(), which creates the initial FROM clause and do not allow you to update it
772 * setting an index by.
773 *
774 * <code>
775 * $qb = $userRepository->createQueryBuilder('u')
776 * ->indexBy('u', 'u.id');
777 *
778 * // Is equivalent to...
779 *
780 * $qb = $em->createQueryBuilder()
781 * ->select('u')
782 * ->from('User', 'u', 'u.id');
783 * </code>
784 *
785 * @return $this
786 *
787 * @throws Query\QueryException
788 */
789 public function indexBy(string $alias, string $indexBy): static
790 {
791 $rootAliases = $this->getRootAliases();
792
793 if (! in_array($alias, $rootAliases, true)) {
794 throw new Query\QueryException(
795 sprintf('Specified root alias %s must be set before invoking indexBy().', $alias),
796 );
797 }
798
799 foreach ($this->dqlParts['from'] as &$fromClause) {
800 assert($fromClause instanceof Expr\From);
801 if ($fromClause->getAlias() !== $alias) {
802 continue;
803 }
804
805 $fromClause = new Expr\From($fromClause->getFrom(), $fromClause->getAlias(), $indexBy);
806 }
807
808 return $this;
809 }
810
811 /**
812 * Creates and adds a join over an entity association to the query.
813 *
814 * The entities in the joined association will be fetched as part of the query
815 * result if the alias used for the joined association is placed in the select
816 * expressions.
817 *
818 * <code>
819 * $qb = $em->createQueryBuilder()
820 * ->select('u')
821 * ->from('User', 'u')
822 * ->join('u.Phonenumbers', 'p', Expr\Join::WITH, 'p.is_primary = 1');
823 * </code>
824 *
825 * @psalm-param Expr\Join::ON|Expr\Join::WITH|null $conditionType
826 *
827 * @return $this
828 */
829 public function join(
830 string $join,
831 string $alias,
832 string|null $conditionType = null,
833 string|Expr\Composite|Expr\Comparison|Expr\Func|null $condition = null,
834 string|null $indexBy = null,
835 ): static {
836 return $this->innerJoin($join, $alias, $conditionType, $condition, $indexBy);
837 }
838
839 /**
840 * Creates and adds a join over an entity association to the query.
841 *
842 * The entities in the joined association will be fetched as part of the query
843 * result if the alias used for the joined association is placed in the select
844 * expressions.
845 *
846 * [php]
847 * $qb = $em->createQueryBuilder()
848 * ->select('u')
849 * ->from('User', 'u')
850 * ->innerJoin('u.Phonenumbers', 'p', Expr\Join::WITH, 'p.is_primary = 1');
851 *
852 * @psalm-param Expr\Join::ON|Expr\Join::WITH|null $conditionType
853 *
854 * @return $this
855 */
856 public function innerJoin(
857 string $join,
858 string $alias,
859 string|null $conditionType = null,
860 string|Expr\Composite|Expr\Comparison|Expr\Func|null $condition = null,
861 string|null $indexBy = null,
862 ): static {
863 $parentAlias = substr($join, 0, (int) strpos($join, '.'));
864
865 $rootAlias = $this->findRootAlias($alias, $parentAlias);
866
867 $join = new Expr\Join(
868 Expr\Join::INNER_JOIN,
869 $join,
870 $alias,
871 $conditionType,
872 $condition,
873 $indexBy,
874 );
875
876 return $this->add('join', [$rootAlias => $join], true);
877 }
878
879 /**
880 * Creates and adds a left join over an entity association to the query.
881 *
882 * The entities in the joined association will be fetched as part of the query
883 * result if the alias used for the joined association is placed in the select
884 * expressions.
885 *
886 * <code>
887 * $qb = $em->createQueryBuilder()
888 * ->select('u')
889 * ->from('User', 'u')
890 * ->leftJoin('u.Phonenumbers', 'p', Expr\Join::WITH, 'p.is_primary = 1');
891 * </code>
892 *
893 * @psalm-param Expr\Join::ON|Expr\Join::WITH|null $conditionType
894 *
895 * @return $this
896 */
897 public function leftJoin(
898 string $join,
899 string $alias,
900 string|null $conditionType = null,
901 string|Expr\Composite|Expr\Comparison|Expr\Func|null $condition = null,
902 string|null $indexBy = null,
903 ): static {
904 $parentAlias = substr($join, 0, (int) strpos($join, '.'));
905
906 $rootAlias = $this->findRootAlias($alias, $parentAlias);
907
908 $join = new Expr\Join(
909 Expr\Join::LEFT_JOIN,
910 $join,
911 $alias,
912 $conditionType,
913 $condition,
914 $indexBy,
915 );
916
917 return $this->add('join', [$rootAlias => $join], true);
918 }
919
920 /**
921 * Sets a new value for a field in a bulk update query.
922 *
923 * <code>
924 * $qb = $em->createQueryBuilder()
925 * ->update('User', 'u')
926 * ->set('u.password', '?1')
927 * ->where('u.id = ?2');
928 * </code>
929 *
930 * @return $this
931 */
932 public function set(string $key, mixed $value): static
933 {
934 return $this->add('set', new Expr\Comparison($key, Expr\Comparison::EQ, $value), true);
935 }
936
937 /**
938 * Specifies one or more restrictions to the query result.
939 * Replaces any previously specified restrictions, if any.
940 *
941 * <code>
942 * $qb = $em->createQueryBuilder()
943 * ->select('u')
944 * ->from('User', 'u')
945 * ->where('u.id = ?');
946 *
947 * // You can optionally programmatically build and/or expressions
948 * $qb = $em->createQueryBuilder();
949 *
950 * $or = $qb->expr()->orX();
951 * $or->add($qb->expr()->eq('u.id', 1));
952 * $or->add($qb->expr()->eq('u.id', 2));
953 *
954 * $qb->update('User', 'u')
955 * ->set('u.password', '?')
956 * ->where($or);
957 * </code>
958 *
959 * @return $this
960 */
961 public function where(mixed ...$predicates): static
962 {
963 self::validateVariadicParameter($predicates);
964
965 if (! (count($predicates) === 1 && $predicates[0] instanceof Expr\Composite)) {
966 $predicates = new Expr\Andx($predicates);
967 }
968
969 return $this->add('where', $predicates);
970 }
971
972 /**
973 * Adds one or more restrictions to the query results, forming a logical
974 * conjunction with any previously specified restrictions.
975 *
976 * <code>
977 * $qb = $em->createQueryBuilder()
978 * ->select('u')
979 * ->from('User', 'u')
980 * ->where('u.username LIKE ?')
981 * ->andWhere('u.is_active = 1');
982 * </code>
983 *
984 * @see where()
985 *
986 * @return $this
987 */
988 public function andWhere(mixed ...$where): static
989 {
990 self::validateVariadicParameter($where);
991
992 $dql = $this->getDQLPart('where');
993
994 if ($dql instanceof Expr\Andx) {
995 $dql->addMultiple($where);
996 } else {
997 array_unshift($where, $dql);
998 $dql = new Expr\Andx($where);
999 }
1000
1001 return $this->add('where', $dql);
1002 }
1003
1004 /**
1005 * Adds one or more restrictions to the query results, forming a logical
1006 * disjunction with any previously specified restrictions.
1007 *
1008 * <code>
1009 * $qb = $em->createQueryBuilder()
1010 * ->select('u')
1011 * ->from('User', 'u')
1012 * ->where('u.id = 1')
1013 * ->orWhere('u.id = 2');
1014 * </code>
1015 *
1016 * @see where()
1017 *
1018 * @return $this
1019 */
1020 public function orWhere(mixed ...$where): static
1021 {
1022 self::validateVariadicParameter($where);
1023
1024 $dql = $this->getDQLPart('where');
1025
1026 if ($dql instanceof Expr\Orx) {
1027 $dql->addMultiple($where);
1028 } else {
1029 array_unshift($where, $dql);
1030 $dql = new Expr\Orx($where);
1031 }
1032
1033 return $this->add('where', $dql);
1034 }
1035
1036 /**
1037 * Specifies a grouping over the results of the query.
1038 * Replaces any previously specified groupings, if any.
1039 *
1040 * <code>
1041 * $qb = $em->createQueryBuilder()
1042 * ->select('u')
1043 * ->from('User', 'u')
1044 * ->groupBy('u.id');
1045 * </code>
1046 *
1047 * @return $this
1048 */
1049 public function groupBy(string ...$groupBy): static
1050 {
1051 self::validateVariadicParameter($groupBy);
1052
1053 return $this->add('groupBy', new Expr\GroupBy($groupBy));
1054 }
1055
1056 /**
1057 * Adds a grouping expression to the query.
1058 *
1059 * <code>
1060 * $qb = $em->createQueryBuilder()
1061 * ->select('u')
1062 * ->from('User', 'u')
1063 * ->groupBy('u.lastLogin')
1064 * ->addGroupBy('u.createdAt');
1065 * </code>
1066 *
1067 * @return $this
1068 */
1069 public function addGroupBy(string ...$groupBy): static
1070 {
1071 self::validateVariadicParameter($groupBy);
1072
1073 return $this->add('groupBy', new Expr\GroupBy($groupBy), true);
1074 }
1075
1076 /**
1077 * Specifies a restriction over the groups of the query.
1078 * Replaces any previous having restrictions, if any.
1079 *
1080 * @return $this
1081 */
1082 public function having(mixed ...$having): static
1083 {
1084 self::validateVariadicParameter($having);
1085
1086 if (! (count($having) === 1 && ($having[0] instanceof Expr\Andx || $having[0] instanceof Expr\Orx))) {
1087 $having = new Expr\Andx($having);
1088 }
1089
1090 return $this->add('having', $having);
1091 }
1092
1093 /**
1094 * Adds a restriction over the groups of the query, forming a logical
1095 * conjunction with any existing having restrictions.
1096 *
1097 * @return $this
1098 */
1099 public function andHaving(mixed ...$having): static
1100 {
1101 self::validateVariadicParameter($having);
1102
1103 $dql = $this->getDQLPart('having');
1104
1105 if ($dql instanceof Expr\Andx) {
1106 $dql->addMultiple($having);
1107 } else {
1108 array_unshift($having, $dql);
1109 $dql = new Expr\Andx($having);
1110 }
1111
1112 return $this->add('having', $dql);
1113 }
1114
1115 /**
1116 * Adds a restriction over the groups of the query, forming a logical
1117 * disjunction with any existing having restrictions.
1118 *
1119 * @return $this
1120 */
1121 public function orHaving(mixed ...$having): static
1122 {
1123 self::validateVariadicParameter($having);
1124
1125 $dql = $this->getDQLPart('having');
1126
1127 if ($dql instanceof Expr\Orx) {
1128 $dql->addMultiple($having);
1129 } else {
1130 array_unshift($having, $dql);
1131 $dql = new Expr\Orx($having);
1132 }
1133
1134 return $this->add('having', $dql);
1135 }
1136
1137 /**
1138 * Specifies an ordering for the query results.
1139 * Replaces any previously specified orderings, if any.
1140 *
1141 * @return $this
1142 */
1143 public function orderBy(string|Expr\OrderBy $sort, string|null $order = null): static
1144 {
1145 $orderBy = $sort instanceof Expr\OrderBy ? $sort : new Expr\OrderBy($sort, $order);
1146
1147 return $this->add('orderBy', $orderBy);
1148 }
1149
1150 /**
1151 * Adds an ordering to the query results.
1152 *
1153 * @return $this
1154 */
1155 public function addOrderBy(string|Expr\OrderBy $sort, string|null $order = null): static
1156 {
1157 $orderBy = $sort instanceof Expr\OrderBy ? $sort : new Expr\OrderBy($sort, $order);
1158
1159 return $this->add('orderBy', $orderBy, true);
1160 }
1161
1162 /**
1163 * Adds criteria to the query.
1164 *
1165 * Adds where expressions with AND operator.
1166 * Adds orderings.
1167 * Overrides firstResult and maxResults if they're set.
1168 *
1169 * @return $this
1170 *
1171 * @throws Query\QueryException
1172 */
1173 public function addCriteria(Criteria $criteria): static
1174 {
1175 $allAliases = $this->getAllAliases();
1176 if (! isset($allAliases[0])) {
1177 throw new Query\QueryException('No aliases are set before invoking addCriteria().');
1178 }
1179
1180 $visitor = new QueryExpressionVisitor($this->getAllAliases());
1181
1182 $whereExpression = $criteria->getWhereExpression();
1183 if ($whereExpression) {
1184 $this->andWhere($visitor->dispatch($whereExpression));
1185 foreach ($visitor->getParameters() as $parameter) {
1186 $this->parameters->add($parameter);
1187 }
1188 }
1189
1190 foreach ($criteria->orderings() as $sort => $order) {
1191 $hasValidAlias = false;
1192 foreach ($allAliases as $alias) {
1193 if (str_starts_with($sort . '.', $alias . '.')) {
1194 $hasValidAlias = true;
1195 break;
1196 }
1197 }
1198
1199 if (! $hasValidAlias) {
1200 $sort = $allAliases[0] . '.' . $sort;
1201 }
1202
1203 $this->addOrderBy($sort, $order->value);
1204 }
1205
1206 // Overwrite limits only if they was set in criteria
1207 $firstResult = $criteria->getFirstResult();
1208 if ($firstResult > 0) {
1209 $this->setFirstResult($firstResult);
1210 }
1211
1212 $maxResults = $criteria->getMaxResults();
1213 if ($maxResults !== null) {
1214 $this->setMaxResults($maxResults);
1215 }
1216
1217 return $this;
1218 }
1219
1220 /**
1221 * Gets a query part by its name.
1222 */
1223 public function getDQLPart(string $queryPartName): mixed
1224 {
1225 return $this->dqlParts[$queryPartName];
1226 }
1227
1228 /**
1229 * Gets all query parts.
1230 *
1231 * @psalm-return array<string, mixed> $dqlParts
1232 */
1233 public function getDQLParts(): array
1234 {
1235 return $this->dqlParts;
1236 }
1237
1238 private function getDQLForDelete(): string
1239 {
1240 return 'DELETE'
1241 . $this->getReducedDQLQueryPart('from', ['pre' => ' ', 'separator' => ', '])
1242 . $this->getReducedDQLQueryPart('where', ['pre' => ' WHERE '])
1243 . $this->getReducedDQLQueryPart('orderBy', ['pre' => ' ORDER BY ', 'separator' => ', ']);
1244 }
1245
1246 private function getDQLForUpdate(): string
1247 {
1248 return 'UPDATE'
1249 . $this->getReducedDQLQueryPart('from', ['pre' => ' ', 'separator' => ', '])
1250 . $this->getReducedDQLQueryPart('set', ['pre' => ' SET ', 'separator' => ', '])
1251 . $this->getReducedDQLQueryPart('where', ['pre' => ' WHERE '])
1252 . $this->getReducedDQLQueryPart('orderBy', ['pre' => ' ORDER BY ', 'separator' => ', ']);
1253 }
1254
1255 private function getDQLForSelect(): string
1256 {
1257 $dql = 'SELECT'
1258 . ($this->dqlParts['distinct'] === true ? ' DISTINCT' : '')
1259 . $this->getReducedDQLQueryPart('select', ['pre' => ' ', 'separator' => ', ']);
1260
1261 $fromParts = $this->getDQLPart('from');
1262 $joinParts = $this->getDQLPart('join');
1263 $fromClauses = [];
1264
1265 // Loop through all FROM clauses
1266 if (! empty($fromParts)) {
1267 $dql .= ' FROM ';
1268
1269 foreach ($fromParts as $from) {
1270 $fromClause = (string) $from;
1271
1272 if ($from instanceof Expr\From && isset($joinParts[$from->getAlias()])) {
1273 foreach ($joinParts[$from->getAlias()] as $join) {
1274 $fromClause .= ' ' . ((string) $join);
1275 }
1276 }
1277
1278 $fromClauses[] = $fromClause;
1279 }
1280 }
1281
1282 $dql .= implode(', ', $fromClauses)
1283 . $this->getReducedDQLQueryPart('where', ['pre' => ' WHERE '])
1284 . $this->getReducedDQLQueryPart('groupBy', ['pre' => ' GROUP BY ', 'separator' => ', '])
1285 . $this->getReducedDQLQueryPart('having', ['pre' => ' HAVING '])
1286 . $this->getReducedDQLQueryPart('orderBy', ['pre' => ' ORDER BY ', 'separator' => ', ']);
1287
1288 return $dql;
1289 }
1290
1291 /** @psalm-param array<string, mixed> $options */
1292 private function getReducedDQLQueryPart(string $queryPartName, array $options = []): string
1293 {
1294 $queryPart = $this->getDQLPart($queryPartName);
1295
1296 if (empty($queryPart)) {
1297 return $options['empty'] ?? '';
1298 }
1299
1300 return ($options['pre'] ?? '')
1301 . (is_array($queryPart) ? implode($options['separator'], $queryPart) : $queryPart)
1302 . ($options['post'] ?? '');
1303 }
1304
1305 /**
1306 * Resets DQL parts.
1307 *
1308 * @param string[]|null $parts
1309 * @psalm-param list<string>|null $parts
1310 *
1311 * @return $this
1312 */
1313 public function resetDQLParts(array|null $parts = null): static
1314 {
1315 if ($parts === null) {
1316 $parts = array_keys($this->dqlParts);
1317 }
1318
1319 foreach ($parts as $part) {
1320 $this->resetDQLPart($part);
1321 }
1322
1323 return $this;
1324 }
1325
1326 /**
1327 * Resets single DQL part.
1328 *
1329 * @return $this
1330 */
1331 public function resetDQLPart(string $part): static
1332 {
1333 $this->dqlParts[$part] = is_array($this->dqlParts[$part]) ? [] : null;
1334 $this->dql = null;
1335
1336 return $this;
1337 }
1338
1339 /**
1340 * Gets a string representation of this QueryBuilder which corresponds to
1341 * the final DQL query being constructed.
1342 */
1343 public function __toString(): string
1344 {
1345 return $this->getDQL();
1346 }
1347
1348 /**
1349 * Deep clones all expression objects in the DQL parts.
1350 *
1351 * @return void
1352 */
1353 public function __clone()
1354 {
1355 foreach ($this->dqlParts as $part => $elements) {
1356 if (is_array($this->dqlParts[$part])) {
1357 foreach ($this->dqlParts[$part] as $idx => $element) {
1358 if (is_object($element)) {
1359 $this->dqlParts[$part][$idx] = clone $element;
1360 }
1361 }
1362 } elseif (is_object($elements)) {
1363 $this->dqlParts[$part] = clone $elements;
1364 }
1365 }
1366
1367 $parameters = [];
1368
1369 foreach ($this->parameters as $parameter) {
1370 $parameters[] = clone $parameter;
1371 }
1372
1373 $this->parameters = new ArrayCollection($parameters);
1374 }
1375}