summaryrefslogtreecommitdiff
path: root/vendor/doctrine/orm/src/Query.php
diff options
context:
space:
mode:
authorpolo <ordipolo@gmx.fr>2024-08-13 23:45:21 +0200
committerpolo <ordipolo@gmx.fr>2024-08-13 23:45:21 +0200
commitbf6655a534a6775d30cafa67bd801276bda1d98d (patch)
treec6381e3f6c81c33eab72508f410b165ba05f7e9c /vendor/doctrine/orm/src/Query.php
parent94d67a4b51f8e62e7d518cce26a526ae1ec48278 (diff)
downloadAppliGestionPHP-bf6655a534a6775d30cafa67bd801276bda1d98d.zip
VERSION 0.2 doctrine ORM et entités
Diffstat (limited to 'vendor/doctrine/orm/src/Query.php')
-rw-r--r--vendor/doctrine/orm/src/Query.php682
1 files changed, 682 insertions, 0 deletions
diff --git a/vendor/doctrine/orm/src/Query.php b/vendor/doctrine/orm/src/Query.php
new file mode 100644
index 0000000..a869316
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query.php
@@ -0,0 +1,682 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM;
6
7use Doctrine\DBAL\LockMode;
8use Doctrine\DBAL\Result;
9use Doctrine\DBAL\Types\Type;
10use Doctrine\ORM\Mapping\ClassMetadata;
11use Doctrine\ORM\Query\AST\DeleteStatement;
12use Doctrine\ORM\Query\AST\SelectStatement;
13use Doctrine\ORM\Query\AST\UpdateStatement;
14use Doctrine\ORM\Query\Exec\AbstractSqlExecutor;
15use Doctrine\ORM\Query\Parameter;
16use Doctrine\ORM\Query\ParameterTypeInferer;
17use Doctrine\ORM\Query\Parser;
18use Doctrine\ORM\Query\ParserResult;
19use Doctrine\ORM\Query\QueryException;
20use Doctrine\ORM\Query\ResultSetMapping;
21use Doctrine\ORM\Utility\HierarchyDiscriminatorResolver;
22use Psr\Cache\CacheItemPoolInterface;
23
24use function array_keys;
25use function array_values;
26use function assert;
27use function count;
28use function get_debug_type;
29use function in_array;
30use function ksort;
31use function md5;
32use function reset;
33use function serialize;
34use function sha1;
35use function stripos;
36
37/**
38 * A Query object represents a DQL query.
39 *
40 * @final
41 */
42class Query extends AbstractQuery
43{
44 /**
45 * A query object is in CLEAN state when it has NO unparsed/unprocessed DQL parts.
46 */
47 public const STATE_CLEAN = 1;
48
49 /**
50 * A query object is in state DIRTY when it has DQL parts that have not yet been
51 * parsed/processed. This is automatically defined as DIRTY when addDqlQueryPart
52 * is called.
53 */
54 public const STATE_DIRTY = 2;
55
56 /* Query HINTS */
57
58 /**
59 * The refresh hint turns any query into a refresh query with the result that
60 * any local changes in entities are overridden with the fetched values.
61 */
62 public const HINT_REFRESH = 'doctrine.refresh';
63
64 public const HINT_CACHE_ENABLED = 'doctrine.cache.enabled';
65
66 public const HINT_CACHE_EVICT = 'doctrine.cache.evict';
67
68 /**
69 * Internal hint: is set to the proxy entity that is currently triggered for loading
70 */
71 public const HINT_REFRESH_ENTITY = 'doctrine.refresh.entity';
72
73 /**
74 * The includeMetaColumns query hint causes meta columns like foreign keys and
75 * discriminator columns to be selected and returned as part of the query result.
76 *
77 * This hint does only apply to non-object queries.
78 */
79 public const HINT_INCLUDE_META_COLUMNS = 'doctrine.includeMetaColumns';
80
81 /**
82 * An array of class names that implement \Doctrine\ORM\Query\TreeWalker and
83 * are iterated and executed after the DQL has been parsed into an AST.
84 */
85 public const HINT_CUSTOM_TREE_WALKERS = 'doctrine.customTreeWalkers';
86
87 /**
88 * A string with a class name that implements \Doctrine\ORM\Query\TreeWalker
89 * and is used for generating the target SQL from any DQL AST tree.
90 */
91 public const HINT_CUSTOM_OUTPUT_WALKER = 'doctrine.customOutputWalker';
92
93 /**
94 * Marks queries as creating only read only objects.
95 *
96 * If the object retrieved from the query is already in the identity map
97 * then it does not get marked as read only if it wasn't already.
98 */
99 public const HINT_READ_ONLY = 'doctrine.readOnly';
100
101 public const HINT_INTERNAL_ITERATION = 'doctrine.internal.iteration';
102
103 public const HINT_LOCK_MODE = 'doctrine.lockMode';
104
105 /**
106 * The current state of this query.
107 *
108 * @psalm-var self::STATE_*
109 */
110 private int $state = self::STATE_DIRTY;
111
112 /**
113 * A snapshot of the parameter types the query was parsed with.
114 *
115 * @var array<string,Type>
116 */
117 private array $parsedTypes = [];
118
119 /**
120 * Cached DQL query.
121 */
122 private string|null $dql = null;
123
124 /**
125 * The parser result that holds DQL => SQL information.
126 */
127 private ParserResult $parserResult;
128
129 /**
130 * The first result to return (the "offset").
131 */
132 private int $firstResult = 0;
133
134 /**
135 * The maximum number of results to return (the "limit").
136 */
137 private int|null $maxResults = null;
138
139 /**
140 * The cache driver used for caching queries.
141 */
142 private CacheItemPoolInterface|null $queryCache = null;
143
144 /**
145 * Whether or not expire the query cache.
146 */
147 private bool $expireQueryCache = false;
148
149 /**
150 * The query cache lifetime.
151 */
152 private int|null $queryCacheTTL = null;
153
154 /**
155 * Whether to use a query cache, if available. Defaults to TRUE.
156 */
157 private bool $useQueryCache = true;
158
159 /**
160 * Gets the SQL query/queries that correspond to this DQL query.
161 *
162 * @return list<string>|string The built sql query or an array of all sql queries.
163 */
164 public function getSQL(): string|array
165 {
166 return $this->parse()->getSqlExecutor()->getSqlStatements();
167 }
168
169 /**
170 * Returns the corresponding AST for this DQL query.
171 */
172 public function getAST(): SelectStatement|UpdateStatement|DeleteStatement
173 {
174 $parser = new Parser($this);
175
176 return $parser->getAST();
177 }
178
179 protected function getResultSetMapping(): ResultSetMapping
180 {
181 // parse query or load from cache
182 if ($this->resultSetMapping === null) {
183 $this->resultSetMapping = $this->parse()->getResultSetMapping();
184 }
185
186 return $this->resultSetMapping;
187 }
188
189 /**
190 * Parses the DQL query, if necessary, and stores the parser result.
191 *
192 * Note: Populates $this->_parserResult as a side-effect.
193 */
194 private function parse(): ParserResult
195 {
196 $types = [];
197
198 foreach ($this->parameters as $parameter) {
199 /** @var Query\Parameter $parameter */
200 $types[$parameter->getName()] = $parameter->getType();
201 }
202
203 // Return previous parser result if the query and the filter collection are both clean
204 if ($this->state === self::STATE_CLEAN && $this->parsedTypes === $types && $this->em->isFiltersStateClean()) {
205 return $this->parserResult;
206 }
207
208 $this->state = self::STATE_CLEAN;
209 $this->parsedTypes = $types;
210
211 $queryCache = $this->queryCache ?? $this->em->getConfiguration()->getQueryCache();
212 // Check query cache.
213 if (! ($this->useQueryCache && $queryCache)) {
214 $parser = new Parser($this);
215
216 $this->parserResult = $parser->parse();
217
218 return $this->parserResult;
219 }
220
221 $cacheItem = $queryCache->getItem($this->getQueryCacheId());
222
223 if (! $this->expireQueryCache && $cacheItem->isHit()) {
224 $cached = $cacheItem->get();
225 if ($cached instanceof ParserResult) {
226 // Cache hit.
227 $this->parserResult = $cached;
228
229 return $this->parserResult;
230 }
231 }
232
233 // Cache miss.
234 $parser = new Parser($this);
235
236 $this->parserResult = $parser->parse();
237
238 $queryCache->save($cacheItem->set($this->parserResult)->expiresAfter($this->queryCacheTTL));
239
240 return $this->parserResult;
241 }
242
243 protected function _doExecute(): Result|int
244 {
245 $executor = $this->parse()->getSqlExecutor();
246
247 if ($this->queryCacheProfile) {
248 $executor->setQueryCacheProfile($this->queryCacheProfile);
249 } else {
250 $executor->removeQueryCacheProfile();
251 }
252
253 if ($this->resultSetMapping === null) {
254 $this->resultSetMapping = $this->parserResult->getResultSetMapping();
255 }
256
257 // Prepare parameters
258 $paramMappings = $this->parserResult->getParameterMappings();
259 $paramCount = count($this->parameters);
260 $mappingCount = count($paramMappings);
261
262 if ($paramCount > $mappingCount) {
263 throw QueryException::tooManyParameters($mappingCount, $paramCount);
264 }
265
266 if ($paramCount < $mappingCount) {
267 throw QueryException::tooFewParameters($mappingCount, $paramCount);
268 }
269
270 // evict all cache for the entity region
271 if ($this->hasCache && isset($this->hints[self::HINT_CACHE_EVICT]) && $this->hints[self::HINT_CACHE_EVICT]) {
272 $this->evictEntityCacheRegion();
273 }
274
275 [$sqlParams, $types] = $this->processParameterMappings($paramMappings);
276
277 $this->evictResultSetCache(
278 $executor,
279 $sqlParams,
280 $types,
281 $this->em->getConnection()->getParams(),
282 );
283
284 return $executor->execute($this->em->getConnection(), $sqlParams, $types);
285 }
286
287 /**
288 * @param array<string,mixed> $sqlParams
289 * @param array<string,Type> $types
290 * @param array<string,mixed> $connectionParams
291 */
292 private function evictResultSetCache(
293 AbstractSqlExecutor $executor,
294 array $sqlParams,
295 array $types,
296 array $connectionParams,
297 ): void {
298 if ($this->queryCacheProfile === null || ! $this->getExpireResultCache()) {
299 return;
300 }
301
302 $cache = $this->queryCacheProfile->getResultCache();
303
304 assert($cache !== null);
305
306 $statements = (array) $executor->getSqlStatements(); // Type casted since it can either be a string or an array
307
308 foreach ($statements as $statement) {
309 $cacheKeys = $this->queryCacheProfile->generateCacheKeys($statement, $sqlParams, $types, $connectionParams);
310 $cache->deleteItem(reset($cacheKeys));
311 }
312 }
313
314 /**
315 * Evict entity cache region
316 */
317 private function evictEntityCacheRegion(): void
318 {
319 $AST = $this->getAST();
320
321 if ($AST instanceof SelectStatement) {
322 throw new QueryException('The hint "HINT_CACHE_EVICT" is not valid for select statements.');
323 }
324
325 $className = $AST instanceof DeleteStatement
326 ? $AST->deleteClause->abstractSchemaName
327 : $AST->updateClause->abstractSchemaName;
328
329 $this->em->getCache()->evictEntityRegion($className);
330 }
331
332 /**
333 * Processes query parameter mappings.
334 *
335 * @param array<list<int>> $paramMappings
336 *
337 * @return mixed[][]
338 * @psalm-return array{0: list<mixed>, 1: array}
339 *
340 * @throws Query\QueryException
341 */
342 private function processParameterMappings(array $paramMappings): array
343 {
344 $sqlParams = [];
345 $types = [];
346
347 foreach ($this->parameters as $parameter) {
348 $key = $parameter->getName();
349
350 if (! isset($paramMappings[$key])) {
351 throw QueryException::unknownParameter($key);
352 }
353
354 [$value, $type] = $this->resolveParameterValue($parameter);
355
356 foreach ($paramMappings[$key] as $position) {
357 $types[$position] = $type;
358 }
359
360 $sqlPositions = $paramMappings[$key];
361
362 // optimized multi value sql positions away for now,
363 // they are not allowed in DQL anyways.
364 $value = [$value];
365 $countValue = count($value);
366
367 for ($i = 0, $l = count($sqlPositions); $i < $l; $i++) {
368 $sqlParams[$sqlPositions[$i]] = $value[$i % $countValue];
369 }
370 }
371
372 if (count($sqlParams) !== count($types)) {
373 throw QueryException::parameterTypeMismatch();
374 }
375
376 if ($sqlParams) {
377 ksort($sqlParams);
378 $sqlParams = array_values($sqlParams);
379
380 ksort($types);
381 $types = array_values($types);
382 }
383
384 return [$sqlParams, $types];
385 }
386
387 /**
388 * @return mixed[] tuple of (value, type)
389 * @psalm-return array{0: mixed, 1: mixed}
390 */
391 private function resolveParameterValue(Parameter $parameter): array
392 {
393 if ($parameter->typeWasSpecified()) {
394 return [$parameter->getValue(), $parameter->getType()];
395 }
396
397 $key = $parameter->getName();
398 $originalValue = $parameter->getValue();
399 $value = $originalValue;
400 $rsm = $this->getResultSetMapping();
401
402 if ($value instanceof ClassMetadata && isset($rsm->metadataParameterMapping[$key])) {
403 $value = $value->getMetadataValue($rsm->metadataParameterMapping[$key]);
404 }
405
406 if ($value instanceof ClassMetadata && isset($rsm->discriminatorParameters[$key])) {
407 $value = array_keys(HierarchyDiscriminatorResolver::resolveDiscriminatorsForClass($value, $this->em));
408 }
409
410 $processedValue = $this->processParameterValue($value);
411
412 return [
413 $processedValue,
414 $originalValue === $processedValue
415 ? $parameter->getType()
416 : ParameterTypeInferer::inferType($processedValue),
417 ];
418 }
419
420 /**
421 * Defines a cache driver to be used for caching queries.
422 *
423 * @return $this
424 */
425 public function setQueryCache(CacheItemPoolInterface|null $queryCache): self
426 {
427 $this->queryCache = $queryCache;
428
429 return $this;
430 }
431
432 /**
433 * Defines whether the query should make use of a query cache, if available.
434 *
435 * @return $this
436 */
437 public function useQueryCache(bool $bool): self
438 {
439 $this->useQueryCache = $bool;
440
441 return $this;
442 }
443
444 /**
445 * Defines how long the query cache will be active before expire.
446 *
447 * @param int|null $timeToLive How long the cache entry is valid.
448 *
449 * @return $this
450 */
451 public function setQueryCacheLifetime(int|null $timeToLive): self
452 {
453 $this->queryCacheTTL = $timeToLive;
454
455 return $this;
456 }
457
458 /**
459 * Retrieves the lifetime of resultset cache.
460 */
461 public function getQueryCacheLifetime(): int|null
462 {
463 return $this->queryCacheTTL;
464 }
465
466 /**
467 * Defines if the query cache is active or not.
468 *
469 * @return $this
470 */
471 public function expireQueryCache(bool $expire = true): self
472 {
473 $this->expireQueryCache = $expire;
474
475 return $this;
476 }
477
478 /**
479 * Retrieves if the query cache is active or not.
480 */
481 public function getExpireQueryCache(): bool
482 {
483 return $this->expireQueryCache;
484 }
485
486 public function free(): void
487 {
488 parent::free();
489
490 $this->dql = null;
491 $this->state = self::STATE_CLEAN;
492 }
493
494 /**
495 * Sets a DQL query string.
496 */
497 public function setDQL(string $dqlQuery): self
498 {
499 $this->dql = $dqlQuery;
500 $this->state = self::STATE_DIRTY;
501
502 return $this;
503 }
504
505 /**
506 * Returns the DQL query that is represented by this query object.
507 */
508 public function getDQL(): string|null
509 {
510 return $this->dql;
511 }
512
513 /**
514 * Returns the state of this query object
515 * By default the type is Doctrine_ORM_Query_Abstract::STATE_CLEAN but if it appears any unprocessed DQL
516 * part, it is switched to Doctrine_ORM_Query_Abstract::STATE_DIRTY.
517 *
518 * @see AbstractQuery::STATE_CLEAN
519 * @see AbstractQuery::STATE_DIRTY
520 *
521 * @return int The query state.
522 * @psalm-return self::STATE_* The query state.
523 */
524 public function getState(): int
525 {
526 return $this->state;
527 }
528
529 /**
530 * Method to check if an arbitrary piece of DQL exists
531 *
532 * @param string $dql Arbitrary piece of DQL to check for.
533 */
534 public function contains(string $dql): bool
535 {
536 return stripos($this->getDQL(), $dql) !== false;
537 }
538
539 /**
540 * Sets the position of the first result to retrieve (the "offset").
541 *
542 * @param int $firstResult The first result to return.
543 *
544 * @return $this
545 */
546 public function setFirstResult(int $firstResult): self
547 {
548 $this->firstResult = $firstResult;
549 $this->state = self::STATE_DIRTY;
550
551 return $this;
552 }
553
554 /**
555 * Gets the position of the first result the query object was set to retrieve (the "offset").
556 * Returns 0 if {@link setFirstResult} was not applied to this query.
557 *
558 * @return int The position of the first result.
559 */
560 public function getFirstResult(): int
561 {
562 return $this->firstResult;
563 }
564
565 /**
566 * Sets the maximum number of results to retrieve (the "limit").
567 *
568 * @return $this
569 */
570 public function setMaxResults(int|null $maxResults): self
571 {
572 $this->maxResults = $maxResults;
573 $this->state = self::STATE_DIRTY;
574
575 return $this;
576 }
577
578 /**
579 * Gets the maximum number of results the query object was set to retrieve (the "limit").
580 * Returns NULL if {@link setMaxResults} was not applied to this query.
581 *
582 * @return int|null Maximum number of results.
583 */
584 public function getMaxResults(): int|null
585 {
586 return $this->maxResults;
587 }
588
589 /** {@inheritDoc} */
590 public function toIterable(iterable $parameters = [], $hydrationMode = self::HYDRATE_OBJECT): iterable
591 {
592 $this->setHint(self::HINT_INTERNAL_ITERATION, true);
593
594 return parent::toIterable($parameters, $hydrationMode);
595 }
596
597 public function setHint(string $name, mixed $value): static
598 {
599 $this->state = self::STATE_DIRTY;
600
601 return parent::setHint($name, $value);
602 }
603
604 public function setHydrationMode(string|int $hydrationMode): static
605 {
606 $this->state = self::STATE_DIRTY;
607
608 return parent::setHydrationMode($hydrationMode);
609 }
610
611 /**
612 * Set the lock mode for this Query.
613 *
614 * @see \Doctrine\DBAL\LockMode
615 *
616 * @psalm-param LockMode::* $lockMode
617 *
618 * @return $this
619 *
620 * @throws TransactionRequiredException
621 */
622 public function setLockMode(LockMode|int $lockMode): self
623 {
624 if (in_array($lockMode, [LockMode::NONE, LockMode::PESSIMISTIC_READ, LockMode::PESSIMISTIC_WRITE], true)) {
625 if (! $this->em->getConnection()->isTransactionActive()) {
626 throw TransactionRequiredException::transactionRequired();
627 }
628 }
629
630 $this->setHint(self::HINT_LOCK_MODE, $lockMode);
631
632 return $this;
633 }
634
635 /**
636 * Get the current lock mode for this query.
637 *
638 * @return LockMode|int|null The current lock mode of this query or NULL if no specific lock mode is set.
639 * @psalm-return LockMode::*|null
640 */
641 public function getLockMode(): LockMode|int|null
642 {
643 $lockMode = $this->getHint(self::HINT_LOCK_MODE);
644
645 if ($lockMode === false) {
646 return null;
647 }
648
649 return $lockMode;
650 }
651
652 /**
653 * Generate a cache id for the query cache - reusing the Result-Cache-Id generator.
654 */
655 protected function getQueryCacheId(): string
656 {
657 ksort($this->hints);
658
659 return md5(
660 $this->getDQL() . serialize($this->hints) .
661 '&platform=' . get_debug_type($this->getEntityManager()->getConnection()->getDatabasePlatform()) .
662 ($this->em->hasFilters() ? $this->em->getFilters()->getHash() : '') .
663 '&firstResult=' . $this->firstResult . '&maxResult=' . $this->maxResults .
664 '&hydrationMode=' . $this->hydrationMode . '&types=' . serialize($this->parsedTypes) . 'DOCTRINE_QUERY_CACHE_SALT',
665 );
666 }
667
668 protected function getHash(): string
669 {
670 return sha1(parent::getHash() . '-' . $this->firstResult . '-' . $this->maxResults);
671 }
672
673 /**
674 * Cleanup Query resource when clone is called.
675 */
676 public function __clone()
677 {
678 parent::__clone();
679
680 $this->state = self::STATE_DIRTY;
681 }
682}