diff options
Diffstat (limited to 'vendor/doctrine/orm/src/QueryBuilder.php')
-rw-r--r-- | vendor/doctrine/orm/src/QueryBuilder.php | 1375 |
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 | |||
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\DBAL\ArrayParameterType; | ||
10 | use Doctrine\DBAL\ParameterType; | ||
11 | use Doctrine\ORM\Internal\NoUnknownNamedArguments; | ||
12 | use Doctrine\ORM\Internal\QueryType; | ||
13 | use Doctrine\ORM\Query\Expr; | ||
14 | use Doctrine\ORM\Query\Parameter; | ||
15 | use Doctrine\ORM\Query\QueryExpressionVisitor; | ||
16 | use InvalidArgumentException; | ||
17 | use RuntimeException; | ||
18 | use Stringable; | ||
19 | |||
20 | use function array_keys; | ||
21 | use function array_unshift; | ||
22 | use function assert; | ||
23 | use function count; | ||
24 | use function implode; | ||
25 | use function in_array; | ||
26 | use function is_array; | ||
27 | use function is_numeric; | ||
28 | use function is_object; | ||
29 | use function is_string; | ||
30 | use function key; | ||
31 | use function reset; | ||
32 | use function sprintf; | ||
33 | use function str_starts_with; | ||
34 | use function strpos; | ||
35 | use function strrpos; | ||
36 | use function substr; | ||
37 | |||
38 | /** | ||
39 | * This class is responsible for building DQL query strings via an object oriented | ||
40 | * PHP interface. | ||
41 | */ | ||
42 | class 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 | } | ||