summaryrefslogtreecommitdiff
path: root/vendor/doctrine/orm/src/AbstractQuery.php
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/doctrine/orm/src/AbstractQuery.php')
-rw-r--r--vendor/doctrine/orm/src/AbstractQuery.php1116
1 files changed, 1116 insertions, 0 deletions
diff --git a/vendor/doctrine/orm/src/AbstractQuery.php b/vendor/doctrine/orm/src/AbstractQuery.php
new file mode 100644
index 0000000..0ff92c3
--- /dev/null
+++ b/vendor/doctrine/orm/src/AbstractQuery.php
@@ -0,0 +1,1116 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM;
6
7use BackedEnum;
8use Doctrine\Common\Collections\ArrayCollection;
9use Doctrine\Common\Collections\Collection;
10use Doctrine\DBAL\ArrayParameterType;
11use Doctrine\DBAL\Cache\QueryCacheProfile;
12use Doctrine\DBAL\ParameterType;
13use Doctrine\DBAL\Result;
14use Doctrine\ORM\Cache\Logging\CacheLogger;
15use Doctrine\ORM\Cache\QueryCacheKey;
16use Doctrine\ORM\Cache\TimestampCacheKey;
17use Doctrine\ORM\Mapping\ClassMetadata;
18use Doctrine\ORM\Mapping\MappingException as ORMMappingException;
19use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
20use Doctrine\ORM\Query\Parameter;
21use Doctrine\ORM\Query\QueryException;
22use Doctrine\ORM\Query\ResultSetMapping;
23use Doctrine\Persistence\Mapping\MappingException;
24use LogicException;
25use Psr\Cache\CacheItemPoolInterface;
26use Traversable;
27
28use function array_map;
29use function array_shift;
30use function assert;
31use function count;
32use function is_array;
33use function is_numeric;
34use function is_object;
35use function is_scalar;
36use function is_string;
37use function iterator_to_array;
38use function ksort;
39use function reset;
40use function serialize;
41use function sha1;
42
43/**
44 * Base contract for ORM queries. Base class for Query and NativeQuery.
45 *
46 * @link www.doctrine-project.org
47 */
48abstract class AbstractQuery
49{
50 /* Hydration mode constants */
51
52 /**
53 * Hydrates an object graph. This is the default behavior.
54 */
55 public const HYDRATE_OBJECT = 1;
56
57 /**
58 * Hydrates an array graph.
59 */
60 public const HYDRATE_ARRAY = 2;
61
62 /**
63 * Hydrates a flat, rectangular result set with scalar values.
64 */
65 public const HYDRATE_SCALAR = 3;
66
67 /**
68 * Hydrates a single scalar value.
69 */
70 public const HYDRATE_SINGLE_SCALAR = 4;
71
72 /**
73 * Very simple object hydrator (optimized for performance).
74 */
75 public const HYDRATE_SIMPLEOBJECT = 5;
76
77 /**
78 * Hydrates scalar column value.
79 */
80 public const HYDRATE_SCALAR_COLUMN = 6;
81
82 /**
83 * The parameter map of this query.
84 *
85 * @var ArrayCollection|Parameter[]
86 * @psalm-var ArrayCollection<int, Parameter>
87 */
88 protected ArrayCollection $parameters;
89
90 /**
91 * The user-specified ResultSetMapping to use.
92 */
93 protected ResultSetMapping|null $resultSetMapping = null;
94
95 /**
96 * The map of query hints.
97 *
98 * @psalm-var array<string, mixed>
99 */
100 protected array $hints = [];
101
102 /**
103 * The hydration mode.
104 *
105 * @psalm-var string|AbstractQuery::HYDRATE_*
106 */
107 protected string|int $hydrationMode = self::HYDRATE_OBJECT;
108
109 protected QueryCacheProfile|null $queryCacheProfile = null;
110
111 /**
112 * Whether or not expire the result cache.
113 */
114 protected bool $expireResultCache = false;
115
116 protected QueryCacheProfile|null $hydrationCacheProfile = null;
117
118 /**
119 * Whether to use second level cache, if available.
120 */
121 protected bool $cacheable = false;
122
123 protected bool $hasCache = false;
124
125 /**
126 * Second level cache region name.
127 */
128 protected string|null $cacheRegion = null;
129
130 /**
131 * Second level query cache mode.
132 *
133 * @psalm-var Cache::MODE_*|null
134 */
135 protected int|null $cacheMode = null;
136
137 protected CacheLogger|null $cacheLogger = null;
138
139 protected int $lifetime = 0;
140
141 /**
142 * Initializes a new instance of a class derived from <tt>AbstractQuery</tt>.
143 */
144 public function __construct(
145 /**
146 * The entity manager used by this query object.
147 */
148 protected EntityManagerInterface $em,
149 ) {
150 $this->parameters = new ArrayCollection();
151 $this->hints = $em->getConfiguration()->getDefaultQueryHints();
152 $this->hasCache = $this->em->getConfiguration()->isSecondLevelCacheEnabled();
153
154 if ($this->hasCache) {
155 $this->cacheLogger = $em->getConfiguration()
156 ->getSecondLevelCacheConfiguration()
157 ->getCacheLogger();
158 }
159 }
160
161 /**
162 * Enable/disable second level query (result) caching for this query.
163 *
164 * @return $this
165 */
166 public function setCacheable(bool $cacheable): static
167 {
168 $this->cacheable = $cacheable;
169
170 return $this;
171 }
172
173 /** @return bool TRUE if the query results are enabled for second level cache, FALSE otherwise. */
174 public function isCacheable(): bool
175 {
176 return $this->cacheable;
177 }
178
179 /** @return $this */
180 public function setCacheRegion(string $cacheRegion): static
181 {
182 $this->cacheRegion = $cacheRegion;
183
184 return $this;
185 }
186
187 /**
188 * Obtain the name of the second level query cache region in which query results will be stored
189 *
190 * @return string|null The cache region name; NULL indicates the default region.
191 */
192 public function getCacheRegion(): string|null
193 {
194 return $this->cacheRegion;
195 }
196
197 /** @return bool TRUE if the query cache and second level cache are enabled, FALSE otherwise. */
198 protected function isCacheEnabled(): bool
199 {
200 return $this->cacheable && $this->hasCache;
201 }
202
203 public function getLifetime(): int
204 {
205 return $this->lifetime;
206 }
207
208 /**
209 * Sets the life-time for this query into second level cache.
210 *
211 * @return $this
212 */
213 public function setLifetime(int $lifetime): static
214 {
215 $this->lifetime = $lifetime;
216
217 return $this;
218 }
219
220 /** @psalm-return Cache::MODE_*|null */
221 public function getCacheMode(): int|null
222 {
223 return $this->cacheMode;
224 }
225
226 /**
227 * @psalm-param Cache::MODE_* $cacheMode
228 *
229 * @return $this
230 */
231 public function setCacheMode(int $cacheMode): static
232 {
233 $this->cacheMode = $cacheMode;
234
235 return $this;
236 }
237
238 /**
239 * Gets the SQL query that corresponds to this query object.
240 * The returned SQL syntax depends on the connection driver that is used
241 * by this query object at the time of this method call.
242 *
243 * @return list<string>|string SQL query
244 */
245 abstract public function getSQL(): string|array;
246
247 /**
248 * Retrieves the associated EntityManager of this Query instance.
249 */
250 public function getEntityManager(): EntityManagerInterface
251 {
252 return $this->em;
253 }
254
255 /**
256 * Frees the resources used by the query object.
257 *
258 * Resets Parameters, Parameter Types and Query Hints.
259 */
260 public function free(): void
261 {
262 $this->parameters = new ArrayCollection();
263
264 $this->hints = $this->em->getConfiguration()->getDefaultQueryHints();
265 }
266
267 /**
268 * Get all defined parameters.
269 *
270 * @psalm-return ArrayCollection<int, Parameter>
271 */
272 public function getParameters(): ArrayCollection
273 {
274 return $this->parameters;
275 }
276
277 /**
278 * Gets a query parameter.
279 *
280 * @param int|string $key The key (index or name) of the bound parameter.
281 *
282 * @return Parameter|null The value of the bound parameter, or NULL if not available.
283 */
284 public function getParameter(int|string $key): Parameter|null
285 {
286 $key = Parameter::normalizeName($key);
287
288 $filteredParameters = $this->parameters->filter(
289 static fn (Parameter $parameter): bool => $parameter->getName() === $key
290 );
291
292 return ! $filteredParameters->isEmpty() ? $filteredParameters->first() : null;
293 }
294
295 /**
296 * Sets a collection of query parameters.
297 *
298 * @param ArrayCollection|mixed[] $parameters
299 * @psalm-param ArrayCollection<int, Parameter>|mixed[] $parameters
300 *
301 * @return $this
302 */
303 public function setParameters(ArrayCollection|array $parameters): static
304 {
305 if (is_array($parameters)) {
306 /** @psalm-var ArrayCollection<int, Parameter> $parameterCollection */
307 $parameterCollection = new ArrayCollection();
308
309 foreach ($parameters as $key => $value) {
310 $parameterCollection->add(new Parameter($key, $value));
311 }
312
313 $parameters = $parameterCollection;
314 }
315
316 $this->parameters = $parameters;
317
318 return $this;
319 }
320
321 /**
322 * Sets a query parameter.
323 *
324 * @param string|int $key The parameter position or name.
325 * @param mixed $value The parameter value.
326 * @param ParameterType|ArrayParameterType|string|int|null $type The parameter type. If specified, the given value
327 * will be run through the type conversion of this
328 * type. This is usually not needed for strings and
329 * numeric types.
330 *
331 * @return $this
332 */
333 public function setParameter(string|int $key, mixed $value, ParameterType|ArrayParameterType|string|int|null $type = null): static
334 {
335 $existingParameter = $this->getParameter($key);
336
337 if ($existingParameter !== null) {
338 $existingParameter->setValue($value, $type);
339
340 return $this;
341 }
342
343 $this->parameters->add(new Parameter($key, $value, $type));
344
345 return $this;
346 }
347
348 /**
349 * Processes an individual parameter value.
350 *
351 * @throws ORMInvalidArgumentException
352 */
353 public function processParameterValue(mixed $value): mixed
354 {
355 if (is_scalar($value)) {
356 return $value;
357 }
358
359 if ($value instanceof Collection) {
360 $value = iterator_to_array($value);
361 }
362
363 if (is_array($value)) {
364 $value = $this->processArrayParameterValue($value);
365
366 return $value;
367 }
368
369 if ($value instanceof ClassMetadata) {
370 return $value->name;
371 }
372
373 if ($value instanceof BackedEnum) {
374 return $value->value;
375 }
376
377 if (! is_object($value)) {
378 return $value;
379 }
380
381 try {
382 $class = DefaultProxyClassNameResolver::getClass($value);
383 $value = $this->em->getUnitOfWork()->getSingleIdentifierValue($value);
384
385 if ($value === null) {
386 throw ORMInvalidArgumentException::invalidIdentifierBindingEntity($class);
387 }
388 } catch (MappingException | ORMMappingException) {
389 /* Silence any mapping exceptions. These can occur if the object in
390 question is not a mapped entity, in which case we just don't do
391 any preparation on the value.
392 Depending on MappingDriver, either MappingException or
393 ORMMappingException is thrown. */
394
395 $value = $this->potentiallyProcessIterable($value);
396 }
397
398 return $value;
399 }
400
401 /**
402 * If no mapping is detected, trying to resolve the value as a Traversable
403 */
404 private function potentiallyProcessIterable(mixed $value): mixed
405 {
406 if ($value instanceof Traversable) {
407 $value = iterator_to_array($value);
408 $value = $this->processArrayParameterValue($value);
409 }
410
411 return $value;
412 }
413
414 /**
415 * Process a parameter value which was previously identified as an array
416 *
417 * @param mixed[] $value
418 *
419 * @return mixed[]
420 */
421 private function processArrayParameterValue(array $value): array
422 {
423 foreach ($value as $key => $paramValue) {
424 $paramValue = $this->processParameterValue($paramValue);
425 $value[$key] = is_array($paramValue) ? reset($paramValue) : $paramValue;
426 }
427
428 return $value;
429 }
430
431 /**
432 * Sets the ResultSetMapping that should be used for hydration.
433 *
434 * @return $this
435 */
436 public function setResultSetMapping(ResultSetMapping $rsm): static
437 {
438 $this->translateNamespaces($rsm);
439 $this->resultSetMapping = $rsm;
440
441 return $this;
442 }
443
444 /**
445 * Gets the ResultSetMapping used for hydration.
446 */
447 protected function getResultSetMapping(): ResultSetMapping|null
448 {
449 return $this->resultSetMapping;
450 }
451
452 /**
453 * Allows to translate entity namespaces to full qualified names.
454 */
455 private function translateNamespaces(ResultSetMapping $rsm): void
456 {
457 $translate = fn ($alias): string => $this->em->getClassMetadata($alias)->getName();
458
459 $rsm->aliasMap = array_map($translate, $rsm->aliasMap);
460 $rsm->declaringClasses = array_map($translate, $rsm->declaringClasses);
461 }
462
463 /**
464 * Set a cache profile for hydration caching.
465 *
466 * If no result cache driver is set in the QueryCacheProfile, the default
467 * result cache driver is used from the configuration.
468 *
469 * Important: Hydration caching does NOT register entities in the
470 * UnitOfWork when retrieved from the cache. Never use result cached
471 * entities for requests that also flush the EntityManager. If you want
472 * some form of caching with UnitOfWork registration you should use
473 * {@see AbstractQuery::setResultCacheProfile()}.
474 *
475 * @return $this
476 *
477 * @example
478 * $lifetime = 100;
479 * $resultKey = "abc";
480 * $query->setHydrationCacheProfile(new QueryCacheProfile());
481 * $query->setHydrationCacheProfile(new QueryCacheProfile($lifetime, $resultKey));
482 */
483 public function setHydrationCacheProfile(QueryCacheProfile|null $profile): static
484 {
485 if ($profile === null) {
486 $this->hydrationCacheProfile = null;
487
488 return $this;
489 }
490
491 if (! $profile->getResultCache()) {
492 $defaultHydrationCacheImpl = $this->em->getConfiguration()->getHydrationCache();
493 if ($defaultHydrationCacheImpl) {
494 $profile = $profile->setResultCache($defaultHydrationCacheImpl);
495 }
496 }
497
498 $this->hydrationCacheProfile = $profile;
499
500 return $this;
501 }
502
503 public function getHydrationCacheProfile(): QueryCacheProfile|null
504 {
505 return $this->hydrationCacheProfile;
506 }
507
508 /**
509 * Set a cache profile for the result cache.
510 *
511 * If no result cache driver is set in the QueryCacheProfile, the default
512 * result cache driver is used from the configuration.
513 *
514 * @return $this
515 */
516 public function setResultCacheProfile(QueryCacheProfile|null $profile): static
517 {
518 if ($profile === null) {
519 $this->queryCacheProfile = null;
520
521 return $this;
522 }
523
524 if (! $profile->getResultCache()) {
525 $defaultResultCache = $this->em->getConfiguration()->getResultCache();
526 if ($defaultResultCache) {
527 $profile = $profile->setResultCache($defaultResultCache);
528 }
529 }
530
531 $this->queryCacheProfile = $profile;
532
533 return $this;
534 }
535
536 /**
537 * Defines a cache driver to be used for caching result sets and implicitly enables caching.
538 */
539 public function setResultCache(CacheItemPoolInterface|null $resultCache): static
540 {
541 if ($resultCache === null) {
542 if ($this->queryCacheProfile) {
543 $this->queryCacheProfile = new QueryCacheProfile($this->queryCacheProfile->getLifetime(), $this->queryCacheProfile->getCacheKey());
544 }
545
546 return $this;
547 }
548
549 $this->queryCacheProfile = $this->queryCacheProfile
550 ? $this->queryCacheProfile->setResultCache($resultCache)
551 : new QueryCacheProfile(0, null, $resultCache);
552
553 return $this;
554 }
555
556 /**
557 * Enables caching of the results of this query, for given or default amount of seconds
558 * and optionally specifies which ID to use for the cache entry.
559 *
560 * @param int|null $lifetime How long the cache entry is valid, in seconds.
561 * @param string|null $resultCacheId ID to use for the cache entry.
562 *
563 * @return $this
564 */
565 public function enableResultCache(int|null $lifetime = null, string|null $resultCacheId = null): static
566 {
567 $this->setResultCacheLifetime($lifetime);
568 $this->setResultCacheId($resultCacheId);
569
570 return $this;
571 }
572
573 /**
574 * Disables caching of the results of this query.
575 *
576 * @return $this
577 */
578 public function disableResultCache(): static
579 {
580 $this->queryCacheProfile = null;
581
582 return $this;
583 }
584
585 /**
586 * Defines how long the result cache will be active before expire.
587 *
588 * @param int|null $lifetime How long the cache entry is valid, in seconds.
589 *
590 * @return $this
591 */
592 public function setResultCacheLifetime(int|null $lifetime): static
593 {
594 $lifetime = (int) $lifetime;
595
596 if ($this->queryCacheProfile) {
597 $this->queryCacheProfile = $this->queryCacheProfile->setLifetime($lifetime);
598
599 return $this;
600 }
601
602 $this->queryCacheProfile = new QueryCacheProfile($lifetime);
603
604 $cache = $this->em->getConfiguration()->getResultCache();
605 if (! $cache) {
606 return $this;
607 }
608
609 $this->queryCacheProfile = $this->queryCacheProfile->setResultCache($cache);
610
611 return $this;
612 }
613
614 /**
615 * Defines if the result cache is active or not.
616 *
617 * @param bool $expire Whether or not to force resultset cache expiration.
618 *
619 * @return $this
620 */
621 public function expireResultCache(bool $expire = true): static
622 {
623 $this->expireResultCache = $expire;
624
625 return $this;
626 }
627
628 /**
629 * Retrieves if the resultset cache is active or not.
630 */
631 public function getExpireResultCache(): bool
632 {
633 return $this->expireResultCache;
634 }
635
636 public function getQueryCacheProfile(): QueryCacheProfile|null
637 {
638 return $this->queryCacheProfile;
639 }
640
641 /**
642 * Change the default fetch mode of an association for this query.
643 *
644 * @param class-string $class
645 * @psalm-param Mapping\ClassMetadata::FETCH_EAGER|Mapping\ClassMetadata::FETCH_LAZY $fetchMode
646 */
647 public function setFetchMode(string $class, string $assocName, int $fetchMode): static
648 {
649 $this->hints['fetchMode'][$class][$assocName] = $fetchMode;
650
651 return $this;
652 }
653
654 /**
655 * Defines the processing mode to be used during hydration / result set transformation.
656 *
657 * @param string|int $hydrationMode Doctrine processing mode to be used during hydration process.
658 * One of the Query::HYDRATE_* constants.
659 * @psalm-param string|AbstractQuery::HYDRATE_* $hydrationMode
660 *
661 * @return $this
662 */
663 public function setHydrationMode(string|int $hydrationMode): static
664 {
665 $this->hydrationMode = $hydrationMode;
666
667 return $this;
668 }
669
670 /**
671 * Gets the hydration mode currently used by the query.
672 *
673 * @psalm-return string|AbstractQuery::HYDRATE_*
674 */
675 public function getHydrationMode(): string|int
676 {
677 return $this->hydrationMode;
678 }
679
680 /**
681 * Gets the list of results for the query.
682 *
683 * Alias for execute(null, $hydrationMode = HYDRATE_OBJECT).
684 *
685 * @psalm-param string|AbstractQuery::HYDRATE_* $hydrationMode
686 */
687 public function getResult(string|int $hydrationMode = self::HYDRATE_OBJECT): mixed
688 {
689 return $this->execute(null, $hydrationMode);
690 }
691
692 /**
693 * Gets the array of results for the query.
694 *
695 * Alias for execute(null, HYDRATE_ARRAY).
696 *
697 * @return mixed[]
698 */
699 public function getArrayResult(): array
700 {
701 return $this->execute(null, self::HYDRATE_ARRAY);
702 }
703
704 /**
705 * Gets one-dimensional array of results for the query.
706 *
707 * Alias for execute(null, HYDRATE_SCALAR_COLUMN).
708 *
709 * @return mixed[]
710 */
711 public function getSingleColumnResult(): array
712 {
713 return $this->execute(null, self::HYDRATE_SCALAR_COLUMN);
714 }
715
716 /**
717 * Gets the scalar results for the query.
718 *
719 * Alias for execute(null, HYDRATE_SCALAR).
720 *
721 * @return mixed[]
722 */
723 public function getScalarResult(): array
724 {
725 return $this->execute(null, self::HYDRATE_SCALAR);
726 }
727
728 /**
729 * Get exactly one result or null.
730 *
731 * @psalm-param string|AbstractQuery::HYDRATE_*|null $hydrationMode
732 *
733 * @throws NonUniqueResultException
734 */
735 public function getOneOrNullResult(string|int|null $hydrationMode = null): mixed
736 {
737 try {
738 $result = $this->execute(null, $hydrationMode);
739 } catch (NoResultException) {
740 return null;
741 }
742
743 if ($this->hydrationMode !== self::HYDRATE_SINGLE_SCALAR && ! $result) {
744 return null;
745 }
746
747 if (! is_array($result)) {
748 return $result;
749 }
750
751 if (count($result) > 1) {
752 throw new NonUniqueResultException();
753 }
754
755 return array_shift($result);
756 }
757
758 /**
759 * Gets the single result of the query.
760 *
761 * Enforces the presence as well as the uniqueness of the result.
762 *
763 * If the result is not unique, a NonUniqueResultException is thrown.
764 * If there is no result, a NoResultException is thrown.
765 *
766 * @psalm-param string|AbstractQuery::HYDRATE_*|null $hydrationMode
767 *
768 * @throws NonUniqueResultException If the query result is not unique.
769 * @throws NoResultException If the query returned no result.
770 */
771 public function getSingleResult(string|int|null $hydrationMode = null): mixed
772 {
773 $result = $this->execute(null, $hydrationMode);
774
775 if ($this->hydrationMode !== self::HYDRATE_SINGLE_SCALAR && ! $result) {
776 throw new NoResultException();
777 }
778
779 if (! is_array($result)) {
780 return $result;
781 }
782
783 if (count($result) > 1) {
784 throw new NonUniqueResultException();
785 }
786
787 return array_shift($result);
788 }
789
790 /**
791 * Gets the single scalar result of the query.
792 *
793 * Alias for getSingleResult(HYDRATE_SINGLE_SCALAR).
794 *
795 * @return bool|float|int|string|null The scalar result.
796 *
797 * @throws NoResultException If the query returned no result.
798 * @throws NonUniqueResultException If the query result is not unique.
799 */
800 public function getSingleScalarResult(): mixed
801 {
802 return $this->getSingleResult(self::HYDRATE_SINGLE_SCALAR);
803 }
804
805 /**
806 * Sets a query hint. If the hint name is not recognized, it is silently ignored.
807 *
808 * @return $this
809 */
810 public function setHint(string $name, mixed $value): static
811 {
812 $this->hints[$name] = $value;
813
814 return $this;
815 }
816
817 /**
818 * Gets the value of a query hint. If the hint name is not recognized, FALSE is returned.
819 *
820 * @return mixed The value of the hint or FALSE, if the hint name is not recognized.
821 */
822 public function getHint(string $name): mixed
823 {
824 return $this->hints[$name] ?? false;
825 }
826
827 public function hasHint(string $name): bool
828 {
829 return isset($this->hints[$name]);
830 }
831
832 /**
833 * Return the key value map of query hints that are currently set.
834 *
835 * @return array<string,mixed>
836 */
837 public function getHints(): array
838 {
839 return $this->hints;
840 }
841
842 /**
843 * Executes the query and returns an iterable that can be used to incrementally
844 * iterate over the result.
845 *
846 * @psalm-param ArrayCollection<int, Parameter>|mixed[] $parameters
847 * @psalm-param string|AbstractQuery::HYDRATE_*|null $hydrationMode
848 *
849 * @return iterable<mixed>
850 */
851 public function toIterable(
852 ArrayCollection|array $parameters = [],
853 string|int|null $hydrationMode = null,
854 ): iterable {
855 if ($hydrationMode !== null) {
856 $this->setHydrationMode($hydrationMode);
857 }
858
859 if (count($parameters) !== 0) {
860 $this->setParameters($parameters);
861 }
862
863 $rsm = $this->getResultSetMapping();
864 if ($rsm === null) {
865 throw new LogicException('Uninitialized result set mapping.');
866 }
867
868 if ($rsm->isMixed && count($rsm->scalarMappings) > 0) {
869 throw QueryException::iterateWithMixedResultNotAllowed();
870 }
871
872 $stmt = $this->_doExecute();
873
874 return $this->em->newHydrator($this->hydrationMode)->toIterable($stmt, $rsm, $this->hints);
875 }
876
877 /**
878 * Executes the query.
879 *
880 * @psalm-param ArrayCollection<int, Parameter>|mixed[]|null $parameters
881 * @psalm-param string|AbstractQuery::HYDRATE_*|null $hydrationMode
882 */
883 public function execute(
884 ArrayCollection|array|null $parameters = null,
885 string|int|null $hydrationMode = null,
886 ): mixed {
887 if ($this->cacheable && $this->isCacheEnabled()) {
888 return $this->executeUsingQueryCache($parameters, $hydrationMode);
889 }
890
891 return $this->executeIgnoreQueryCache($parameters, $hydrationMode);
892 }
893
894 /**
895 * Execute query ignoring second level cache.
896 *
897 * @psalm-param ArrayCollection<int, Parameter>|mixed[]|null $parameters
898 * @psalm-param string|AbstractQuery::HYDRATE_*|null $hydrationMode
899 */
900 private function executeIgnoreQueryCache(
901 ArrayCollection|array|null $parameters = null,
902 string|int|null $hydrationMode = null,
903 ): mixed {
904 if ($hydrationMode !== null) {
905 $this->setHydrationMode($hydrationMode);
906 }
907
908 if (! empty($parameters)) {
909 $this->setParameters($parameters);
910 }
911
912 $setCacheEntry = static function ($data): void {
913 };
914
915 if ($this->hydrationCacheProfile !== null) {
916 [$cacheKey, $realCacheKey] = $this->getHydrationCacheId();
917
918 $cache = $this->getHydrationCache();
919 $cacheItem = $cache->getItem($cacheKey);
920 $result = $cacheItem->isHit() ? $cacheItem->get() : [];
921
922 if (isset($result[$realCacheKey])) {
923 return $result[$realCacheKey];
924 }
925
926 if (! $result) {
927 $result = [];
928 }
929
930 $setCacheEntry = static function ($data) use ($cache, $result, $cacheItem, $realCacheKey): void {
931 $cache->save($cacheItem->set($result + [$realCacheKey => $data]));
932 };
933 }
934
935 $stmt = $this->_doExecute();
936
937 if (is_numeric($stmt)) {
938 $setCacheEntry($stmt);
939
940 return $stmt;
941 }
942
943 $rsm = $this->getResultSetMapping();
944 if ($rsm === null) {
945 throw new LogicException('Uninitialized result set mapping.');
946 }
947
948 $data = $this->em->newHydrator($this->hydrationMode)->hydrateAll($stmt, $rsm, $this->hints);
949
950 $setCacheEntry($data);
951
952 return $data;
953 }
954
955 private function getHydrationCache(): CacheItemPoolInterface
956 {
957 assert($this->hydrationCacheProfile !== null);
958
959 $cache = $this->hydrationCacheProfile->getResultCache();
960 assert($cache !== null);
961
962 return $cache;
963 }
964
965 /**
966 * Load from second level cache or executes the query and put into cache.
967 *
968 * @psalm-param ArrayCollection<int, Parameter>|mixed[]|null $parameters
969 * @psalm-param string|AbstractQuery::HYDRATE_*|null $hydrationMode
970 */
971 private function executeUsingQueryCache(
972 ArrayCollection|array|null $parameters = null,
973 string|int|null $hydrationMode = null,
974 ): mixed {
975 $rsm = $this->getResultSetMapping();
976 if ($rsm === null) {
977 throw new LogicException('Uninitialized result set mapping.');
978 }
979
980 $queryCache = $this->em->getCache()->getQueryCache($this->cacheRegion);
981 $queryKey = new QueryCacheKey(
982 $this->getHash(),
983 $this->lifetime,
984 $this->cacheMode ?: Cache::MODE_NORMAL,
985 $this->getTimestampKey(),
986 );
987
988 $result = $queryCache->get($queryKey, $rsm, $this->hints);
989
990 if ($result !== null) {
991 if ($this->cacheLogger) {
992 $this->cacheLogger->queryCacheHit($queryCache->getRegion()->getName(), $queryKey);
993 }
994
995 return $result;
996 }
997
998 $result = $this->executeIgnoreQueryCache($parameters, $hydrationMode);
999 $cached = $queryCache->put($queryKey, $rsm, $result, $this->hints);
1000
1001 if ($this->cacheLogger) {
1002 $this->cacheLogger->queryCacheMiss($queryCache->getRegion()->getName(), $queryKey);
1003
1004 if ($cached) {
1005 $this->cacheLogger->queryCachePut($queryCache->getRegion()->getName(), $queryKey);
1006 }
1007 }
1008
1009 return $result;
1010 }
1011
1012 private function getTimestampKey(): TimestampCacheKey|null
1013 {
1014 assert($this->resultSetMapping !== null);
1015 $entityName = reset($this->resultSetMapping->aliasMap);
1016
1017 if (empty($entityName)) {
1018 return null;
1019 }
1020
1021 $metadata = $this->em->getClassMetadata($entityName);
1022
1023 return new TimestampCacheKey($metadata->rootEntityName);
1024 }
1025
1026 /**
1027 * Get the result cache id to use to store the result set cache entry.
1028 * Will return the configured id if it exists otherwise a hash will be
1029 * automatically generated for you.
1030 *
1031 * @return string[] ($key, $hash)
1032 * @psalm-return array{string, string} ($key, $hash)
1033 */
1034 protected function getHydrationCacheId(): array
1035 {
1036 $parameters = [];
1037 $types = [];
1038
1039 foreach ($this->getParameters() as $parameter) {
1040 $parameters[$parameter->getName()] = $this->processParameterValue($parameter->getValue());
1041 $types[$parameter->getName()] = $parameter->getType();
1042 }
1043
1044 $sql = $this->getSQL();
1045 assert(is_string($sql));
1046 $queryCacheProfile = $this->getHydrationCacheProfile();
1047 $hints = $this->getHints();
1048 $hints['hydrationMode'] = $this->getHydrationMode();
1049
1050 ksort($hints);
1051 assert($queryCacheProfile !== null);
1052
1053 return $queryCacheProfile->generateCacheKeys($sql, $parameters, $types, $hints);
1054 }
1055
1056 /**
1057 * Set the result cache id to use to store the result set cache entry.
1058 * If this is not explicitly set by the developer then a hash is automatically
1059 * generated for you.
1060 */
1061 public function setResultCacheId(string|null $id): static
1062 {
1063 if (! $this->queryCacheProfile) {
1064 return $this->setResultCacheProfile(new QueryCacheProfile(0, $id));
1065 }
1066
1067 $this->queryCacheProfile = $this->queryCacheProfile->setCacheKey($id);
1068
1069 return $this;
1070 }
1071
1072 /**
1073 * Executes the query and returns a the resulting Statement object.
1074 *
1075 * @return Result|int The executed database statement that holds
1076 * the results, or an integer indicating how
1077 * many rows were affected.
1078 */
1079 abstract protected function _doExecute(): Result|int;
1080
1081 /**
1082 * Cleanup Query resource when clone is called.
1083 */
1084 public function __clone()
1085 {
1086 $this->parameters = new ArrayCollection();
1087
1088 $this->hints = [];
1089 $this->hints = $this->em->getConfiguration()->getDefaultQueryHints();
1090 }
1091
1092 /**
1093 * Generates a string of currently query to use for the cache second level cache.
1094 */
1095 protected function getHash(): string
1096 {
1097 $query = $this->getSQL();
1098 assert(is_string($query));
1099 $hints = $this->getHints();
1100 $params = array_map(function (Parameter $parameter) {
1101 $value = $parameter->getValue();
1102
1103 // Small optimization
1104 // Does not invoke processParameterValue for scalar value
1105 if (is_scalar($value)) {
1106 return $value;
1107 }
1108
1109 return $this->processParameterValue($value);
1110 }, $this->parameters->getValues());
1111
1112 ksort($hints);
1113
1114 return sha1($query . '-' . serialize($params) . '-' . serialize($hints));
1115 }
1116}