Completed
Push — master ( a0071b...e33605 )
by Michael
12s
created

lib/Doctrine/ORM/Query.php (1 issue)

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\ORM;
6
7
use Doctrine\Common\Collections\ArrayCollection;
8
use Doctrine\DBAL\LockMode;
9
use Doctrine\DBAL\Types\Type;
10
use Doctrine\ORM\Mapping\ClassMetadata;
11
use Doctrine\ORM\Query\AST\DeleteStatement;
12
use Doctrine\ORM\Query\AST\SelectStatement;
13
use Doctrine\ORM\Query\Exec\AbstractSqlExecutor;
14
use Doctrine\ORM\Query\Parameter;
15
use Doctrine\ORM\Query\ParameterTypeInferer;
16
use Doctrine\ORM\Query\Parser;
17
use Doctrine\ORM\Query\ParserResult;
18
use Doctrine\ORM\Query\QueryException;
19
use Doctrine\ORM\Utility\HierarchyDiscriminatorResolver;
20
21
/**
22
 * A Query object represents a DQL query.
23
 *
24
 */
25
final class Query extends AbstractQuery
26
{
27
    /**
28
     * A query object is in CLEAN state when it has NO unparsed/unprocessed DQL parts.
29
     */
30
    public const STATE_CLEAN = 1;
31
32
    /**
33
     * A query object is in state DIRTY when it has DQL parts that have not yet been
34
     * parsed/processed. This is automatically defined as DIRTY when addDqlQueryPart
35
     * is called.
36
     */
37
    public const STATE_DIRTY = 2;
38
39
    /* Query HINTS */
40
41
    /**
42
     * The refresh hint turns any query into a refresh query with the result that
43
     * any local changes in entities are overridden with the fetched values.
44
     *
45
     * @var string
46
     */
47
    public const HINT_REFRESH = 'doctrine.refresh';
48
49
    /**
50
     * @var string
51
     */
52
    public const HINT_CACHE_ENABLED = 'doctrine.cache.enabled';
53
54
    /**
55
     * @var string
56
     */
57
    public const HINT_CACHE_EVICT = 'doctrine.cache.evict';
58
59
    /**
60
     * Internal hint: is set to the proxy entity that is currently triggered for loading
61
     *
62
     * @var string
63
     */
64
    public const HINT_REFRESH_ENTITY = 'doctrine.refresh.entity';
65
66
    /**
67
     * The forcePartialLoad query hint forces a particular query to return
68
     * partial objects.
69
     *
70
     * @var string
71
     * @todo Rename: HINT_OPTIMIZE
72
     */
73
    public const HINT_FORCE_PARTIAL_LOAD = 'doctrine.forcePartialLoad';
74
75
    /**
76
     * The includeMetaColumns query hint causes meta columns like foreign keys and
77
     * discriminator columns to be selected and returned as part of the query result.
78
     *
79
     * This hint does only apply to non-object queries.
80
     *
81
     * @var string
82
     */
83
    public const HINT_INCLUDE_META_COLUMNS = 'doctrine.includeMetaColumns';
84
85
    /**
86
     * An array of class names that implement \Doctrine\ORM\Query\TreeWalker and
87
     * are iterated and executed after the DQL has been parsed into an AST.
88
     *
89
     * @var string
90
     */
91
    public const HINT_CUSTOM_TREE_WALKERS = 'doctrine.customTreeWalkers';
92
93
    /**
94
     * A string with a class name that implements \Doctrine\ORM\Query\TreeWalker
95
     * and is used for generating the target SQL from any DQL AST tree.
96
     *
97
     * @var string
98
     */
99
    public const HINT_CUSTOM_OUTPUT_WALKER = 'doctrine.customOutputWalker';
100
101
    //const HINT_READ_ONLY = 'doctrine.readOnly';
0 ignored issues
show
Unused Code Comprehensibility introduced by
38% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
102
103
    /**
104
     * @var string
105
     */
106
    public const HINT_INTERNAL_ITERATION = 'doctrine.internal.iteration';
107
108
    /**
109
     * @var string
110
     */
111
    public const HINT_LOCK_MODE = 'doctrine.lockMode';
112
113
    /**
114
     * The current state of this query.
115
     *
116
     * @var int
117
     */
118
    private $state = self::STATE_CLEAN;
119
120
    /**
121
     * A snapshot of the parameter types the query was parsed with.
122
     *
123
     * @var mixed[]
124
     */
125
    private $parsedTypes = [];
126
127
    /**
128
     * Cached DQL query.
129
     *
130
     * @var string
131
     */
132
    private $dql;
133
134
    /**
135
     * The parser result that holds DQL => SQL information.
136
     *
137
     * @var \Doctrine\ORM\Query\ParserResult
138
     */
139
    private $parserResult;
140
141
    /**
142
     * The first result to return (the "offset").
143
     *
144
     * @var int
145
     */
146
    private $firstResult;
147
148
    /**
149
     * The maximum number of results to return (the "limit").
150
     *
151
     * @var int|null
152
     */
153
    private $maxResults;
154
155
    /**
156
     * The cache driver used for caching queries.
157
     *
158
     * @var \Doctrine\Common\Cache\Cache|null
159
     */
160
    private $queryCache;
161
162
    /**
163
     * Whether or not expire the query cache.
164
     *
165
     * @var bool
166
     */
167
    private $expireQueryCache = false;
168
169
    /**
170
     * The query cache lifetime.
171
     *
172
     * @var int
173
     */
174
    private $queryCacheTTL;
175
176
    /**
177
     * Whether to use a query cache, if available. Defaults to TRUE.
178
     *
179
     * @var bool
180
     */
181
    private $useQueryCache = true;
182
183
    /**
184
     * Gets the SQL query/queries that correspond to this DQL query.
185
     *
186
     * @return mixed The built sql query or an array of all sql queries.
187
     *
188
     * @override
189
     */
190 342
    public function getSQL()
191
    {
192 342
        return $this->parse()->getSQLExecutor()->getSQLStatements();
193
    }
194
195
    /**
196
     * Returns the corresponding AST for this DQL query.
197
     *
198
     * @return \Doctrine\ORM\Query\AST\SelectStatement |
199
     *         \Doctrine\ORM\Query\AST\UpdateStatement |
200
     *         \Doctrine\ORM\Query\AST\DeleteStatement
201
     */
202 2
    public function getAST()
203
    {
204 2
        $parser = new Parser($this);
205
206 2
        return $parser->getAST();
207
    }
208
209
    /**
210
     * {@inheritdoc}
211
     */
212 445
    protected function getResultSetMapping()
213
    {
214
        // parse query or load from cache
215 445
        if ($this->resultSetMapping === null) {
216 38
            $this->resultSetMapping = $this->parse()->getResultSetMapping();
217
        }
218
219 442
        return $this->resultSetMapping;
220
    }
221
222
    /**
223
     * Parses the DQL query, if necessary, and stores the parser result.
224
     *
225
     * Note: Populates $this->parserResult as a side-effect.
226
     *
227
     * @return \Doctrine\ORM\Query\ParserResult
228
     */
229 770
    private function parse()
230
    {
231 770
        $types = [];
232
233 770
        foreach ($this->parameters as $parameter) {
234
            /** @var Query\Parameter $parameter */
235 172
            $types[$parameter->getName()] = $parameter->getType();
236
        }
237
238
        // Return previous parser result if the query and the filter collection are both clean
239 770
        if ($this->state === self::STATE_CLEAN && $this->parsedTypes === $types && $this->em->isFiltersStateClean()) {
240 39
            return $this->parserResult;
241
        }
242
243 770
        $this->state       = self::STATE_CLEAN;
244 770
        $this->parsedTypes = $types;
245
246
        // Check query cache.
247 770
        $queryCache = $this->getQueryCacheDriver();
248 770
        if (! ($this->useQueryCache && $queryCache)) {
249 181
            $parser = new Parser($this);
250
251 181
            $this->parserResult = $parser->parse();
252
253 177
            return $this->parserResult;
254
        }
255
256 589
        $hash   = $this->getQueryCacheId();
257 589
        $cached = $this->expireQueryCache ? false : $queryCache->fetch($hash);
258
259 589
        if ($cached instanceof ParserResult) {
260
            // Cache hit.
261 118
            $this->parserResult = $cached;
262
263 118
            return $this->parserResult;
264
        }
265
266
        // Cache miss.
267 538
        $parser = new Parser($this);
268
269 538
        $this->parserResult = $parser->parse();
270
271 519
        $queryCache->save($hash, $this->parserResult, $this->queryCacheTTL);
272
273 519
        return $this->parserResult;
274
    }
275
276
    /**
277
     * {@inheritdoc}
278
     */
279 460
    protected function doExecute()
280
    {
281 460
        $executor = $this->parse()->getSqlExecutor();
282
283 453
        if ($this->queryCacheProfile) {
284 8
            $executor->setQueryCacheProfile($this->queryCacheProfile);
285
        } else {
286 447
            $executor->removeQueryCacheProfile();
287
        }
288
289 453
        if ($this->resultSetMapping === null) {
290 411
            $this->resultSetMapping = $this->parserResult->getResultSetMapping();
291
        }
292
293
        // Prepare parameters
294 453
        $paramMappings = $this->parserResult->getParameterMappings();
295 453
        $paramCount    = count($this->parameters);
296 453
        $mappingCount  = count($paramMappings);
297
298 453
        if ($paramCount > $mappingCount) {
299 1
            throw QueryException::tooManyParameters($mappingCount, $paramCount);
300
        }
301
302 452
        if ($paramCount < $mappingCount) {
303 1
            throw QueryException::tooFewParameters($mappingCount, $paramCount);
304
        }
305
306
        // evict all cache for the entity region
307 451
        if ($this->hasCache && isset($this->hints[self::HINT_CACHE_EVICT]) && $this->hints[self::HINT_CACHE_EVICT]) {
308 2
            $this->evictEntityCacheRegion();
309
        }
310
311 451
        list($sqlParams, $types) = $this->processParameterMappings($paramMappings);
312
313 450
        $this->evictResultSetCache(
314 450
            $executor,
315 450
            $sqlParams,
316 450
            $types,
317 450
            $this->em->getConnection()->getParams()
318
        );
319
320 450
        return $executor->execute($this->em->getConnection(), $sqlParams, $types);
321
    }
322
323
    /**
324
     * @param mixed[] $sqlParams
325
     * @param mixed[] $types
326
     * @param mixed[] $connectionParams
327
     */
328 450
    private function evictResultSetCache(
329
        AbstractSqlExecutor $executor,
330
        array $sqlParams,
331
        array $types,
332
        array $connectionParams
333
    ) {
334 450
        if ($this->queryCacheProfile === null || ! $this->getExpireResultCache()) {
335 450
            return;
336
        }
337
338 2
        $cacheDriver = $this->queryCacheProfile->getResultCacheDriver();
339 2
        $statements  = (array) $executor->getSqlStatements(); // Type casted since it can either be a string or an array
340
341 2
        foreach ($statements as $statement) {
342 2
            $cacheKeys = $this->queryCacheProfile->generateCacheKeys($statement, $sqlParams, $types, $connectionParams);
343
344 2
            $cacheDriver->delete(reset($cacheKeys));
345
        }
346 2
    }
347
348
    /**
349
     * Evict entity cache region
350
     */
351 2
    private function evictEntityCacheRegion()
352
    {
353 2
        $AST = $this->getAST();
354
355 2
        if ($AST instanceof SelectStatement) {
356
            throw new QueryException('The hint "HINT_CACHE_EVICT" is not valid for select statements.');
357
        }
358
359 2
        $className = ($AST instanceof DeleteStatement)
360 1
            ? $AST->deleteClause->abstractSchemaName
361 2
            : $AST->updateClause->abstractSchemaName;
362
363 2
        $this->em->getCache()->evictEntityRegion($className);
364 2
    }
365
366
    /**
367
     * Processes query parameter mappings.
368
     *
369
     * @param mixed[] $paramMappings
370
     *
371
     * @return mixed[][]
372
     *
373
     * @throws Query\QueryException
374
     */
375 451
    private function processParameterMappings($paramMappings)
376
    {
377 451
        $sqlParams = [];
378 451
        $types     = [];
379
380 451
        foreach ($this->parameters as $parameter) {
381 160
            $key   = $parameter->getName();
382 160
            $value = $parameter->getValue();
383 160
            $rsm   = $this->getResultSetMapping();
384
385 160
            if (! isset($paramMappings[$key])) {
386 1
                throw QueryException::unknownParameter($key);
387
            }
388
389 159
            if (isset($rsm->metadataParameterMapping[$key]) && $value instanceof ClassMetadata) {
390
                $value = $value->getMetadataValue($rsm->metadataParameterMapping[$key]);
391
            }
392
393 159
            if (isset($rsm->discriminatorParameters[$key]) && $value instanceof ClassMetadata) {
394 3
                $value = array_keys(HierarchyDiscriminatorResolver::resolveDiscriminatorsForClass($value, $this->em));
395
            }
396
397 159
            $value = $this->processParameterValue($value);
398 159
            $type  = ($parameter->getValue() === $value)
399 148
                ? $parameter->getType()
400 159
                : ParameterTypeInferer::inferType($value);
401
402 159
            foreach ($paramMappings[$key] as $position) {
403 159
                $types[$position] = $type;
404
            }
405
406 159
            $sqlPositions      = $paramMappings[$key];
407 159
            $sqlPositionsCount = count($sqlPositions);
408
409
            // optimized multi value sql positions away for now,
410
            // they are not allowed in DQL anyways.
411 159
            $value      = [$value];
412 159
            $countValue = count($value);
413
414 159
            for ($i = 0, $l = $sqlPositionsCount; $i < $l; $i++) {
415 159
                $sqlParams[$sqlPositions[$i]] = $value[($i % $countValue)];
416
            }
417
        }
418
419 450
        if (count($sqlParams) !== count($types)) {
420
            throw QueryException::parameterTypeMismatch();
421
        }
422
423 450
        if ($sqlParams) {
424 159
            ksort($sqlParams);
425 159
            $sqlParams = array_values($sqlParams);
426
427 159
            ksort($types);
428 159
            $types = array_values($types);
429
        }
430
431 450
        return [$sqlParams, $types];
432
    }
433
434
    /**
435
     * Defines a cache driver to be used for caching queries.
436
     *
437
     * @param \Doctrine\Common\Cache\Cache|null $queryCache Cache driver.
438
     *
439
     * @return Query This query instance.
440
     */
441 6
    public function setQueryCacheDriver($queryCache)
442
    {
443 6
        $this->queryCache = $queryCache;
444
445 6
        return $this;
446
    }
447
448
    /**
449
     * Defines whether the query should make use of a query cache, if available.
450
     *
451
     * @param bool $bool
452
     *
453
     * @return Query This query instance.
454
     */
455 184
    public function useQueryCache($bool)
456
    {
457 184
        $this->useQueryCache = $bool;
458
459 184
        return $this;
460
    }
461
462
    /**
463
     * Returns the cache driver used for query caching.
464
     *
465
     * @return \Doctrine\Common\Cache\Cache|null The cache driver used for query caching or NULL, if
466
     *                                           this Query does not use query caching.
467
     */
468 770
    public function getQueryCacheDriver()
469
    {
470 770
        if ($this->queryCache) {
471 9
            return $this->queryCache;
472
        }
473
474 761
        return $this->em->getConfiguration()->getQueryCacheImpl();
475
    }
476
477
    /**
478
     * Defines how long the query cache will be active before expire.
479
     *
480
     * @param int $timeToLive How long the cache entry is valid.
481
     *
482
     * @return Query This query instance.
483
     */
484 1
    public function setQueryCacheLifetime($timeToLive)
485
    {
486 1
        if ($timeToLive !== null) {
487 1
            $timeToLive = (int) $timeToLive;
488
        }
489
490 1
        $this->queryCacheTTL = $timeToLive;
491
492 1
        return $this;
493
    }
494
495
    /**
496
     * Retrieves the lifetime of resultset cache.
497
     *
498
     * @return int
499
     */
500
    public function getQueryCacheLifetime()
501
    {
502
        return $this->queryCacheTTL;
503
    }
504
505
    /**
506
     * Defines if the query cache is active or not.
507
     *
508
     * @param bool $expire Whether or not to force query cache expiration.
509
     *
510
     * @return Query This query instance.
511
     */
512 7
    public function expireQueryCache($expire = true)
513
    {
514 7
        $this->expireQueryCache = $expire;
515
516 7
        return $this;
517
    }
518
519
    /**
520
     * Retrieves if the query cache is active or not.
521
     *
522
     * @return bool
523
     */
524
    public function getExpireQueryCache()
525
    {
526
        return $this->expireQueryCache;
527
    }
528
529 220
    public function free()
530
    {
531 220
        parent::free();
532
533 220
        $this->dql   = null;
534 220
        $this->state = self::STATE_CLEAN;
535 220
    }
536
537
    /**
538
     * Sets a DQL query string.
539
     *
540
     * @param string $dqlQuery DQL Query.
541
     *
542
     * @return \Doctrine\ORM\AbstractQuery
543
     */
544 959
    public function setDQL($dqlQuery)
545
    {
546 959
        if ($dqlQuery !== null) {
547 959
            $this->dql   = $dqlQuery;
548 959
            $this->state = self::STATE_DIRTY;
549
        }
550
551 959
        return $this;
552
    }
553
554
    /**
555
     * Returns the DQL query that is represented by this query object.
556
     *
557
     * @return string DQL query.
558
     */
559 906
    public function getDQL()
560
    {
561 906
        return $this->dql;
562
    }
563
564
    /**
565
     * Returns the state of this query object
566
     * By default the type is Doctrine_ORM_Query_Abstract::STATE_CLEAN but if it appears any unprocessed DQL
567
     * part, it is switched to Doctrine_ORM_Query_Abstract::STATE_DIRTY.
568
     *
569
     * @see AbstractQuery::STATE_CLEAN
570
     * @see AbstractQuery::STATE_DIRTY
571
     *
572
     * @return int The query state.
573
     */
574
    public function getState()
575
    {
576
        return $this->state;
577
    }
578
579
    /**
580
     * Method to check if an arbitrary piece of DQL exists
581
     *
582
     * @param string $dql Arbitrary piece of DQL to check for.
583
     *
584
     * @return bool
585
     */
586
    public function contains($dql)
587
    {
588
        return stripos($this->getDQL(), $dql) !== false;
589
    }
590
591
    /**
592
     * Sets the position of the first result to retrieve (the "offset").
593
     *
594
     * @param int $firstResult The first result to return.
595
     *
596
     * @return Query This query object.
597
     */
598 221
    public function setFirstResult($firstResult)
599
    {
600 221
        $this->firstResult = $firstResult;
601 221
        $this->state       = self::STATE_DIRTY;
602
603 221
        return $this;
604
    }
605
606
    /**
607
     * Gets the position of the first result the query object was set to retrieve (the "offset").
608
     * Returns NULL if {@link setFirstResult} was not applied to this query.
609
     *
610
     * @return int The position of the first result.
611
     */
612 657
    public function getFirstResult()
613
    {
614 657
        return $this->firstResult;
615
    }
616
617
    /**
618
     * Sets the maximum number of results to retrieve (the "limit").
619
     *
620
     * @param int|null $maxResults
621
     *
622
     * @return Query This query object.
623
     */
624 241
    public function setMaxResults($maxResults)
625
    {
626 241
        $this->maxResults = $maxResults;
627 241
        $this->state      = self::STATE_DIRTY;
628
629 241
        return $this;
630
    }
631
632
    /**
633
     * Gets the maximum number of results the query object was set to retrieve (the "limit").
634
     * Returns NULL if {@link setMaxResults} was not applied to this query.
635
     *
636
     * @return int|null Maximum number of results.
637
     */
638 657
    public function getMaxResults()
639
    {
640 657
        return $this->maxResults;
641
    }
642
643
    /**
644
     * Executes the query and returns an IterableResult that can be used to incrementally
645
     * iterated over the result.
646
     *
647
     * @param ArrayCollection|array|Parameter[]|mixed[]|null $parameters    The query parameters.
648
     * @param int                                            $hydrationMode The hydration mode to use.
649
     *
650
     * @return \Doctrine\ORM\Internal\Hydration\IterableResult
651
     */
652 10
    public function iterate($parameters = null, $hydrationMode = self::HYDRATE_OBJECT)
653
    {
654 10
        $this->setHint(self::HINT_INTERNAL_ITERATION, true);
655
656 10
        return parent::iterate($parameters, $hydrationMode);
657
    }
658
659
    /**
660
     * {@inheritdoc}
661
     */
662 469
    public function setHint($name, $value)
663
    {
664 469
        $this->state = self::STATE_DIRTY;
665
666 469
        return parent::setHint($name, $value);
667
    }
668
669
    /**
670
     * {@inheritdoc}
671
     */
672 361
    public function setHydrationMode($hydrationMode)
673
    {
674 361
        $this->state = self::STATE_DIRTY;
675
676 361
        return parent::setHydrationMode($hydrationMode);
677
    }
678
679
    /**
680
     * Set the lock mode for this Query.
681
     *
682
     * @see \Doctrine\DBAL\LockMode
683
     *
684
     * @param int $lockMode
685
     *
686
     * @return Query
687
     *
688
     * @throws TransactionRequiredException
689
     */
690
    public function setLockMode($lockMode)
691
    {
692
        if (in_array($lockMode, [LockMode::NONE, LockMode::PESSIMISTIC_READ, LockMode::PESSIMISTIC_WRITE], true)) {
693
            if (! $this->em->getConnection()->isTransactionActive()) {
694
                throw TransactionRequiredException::transactionRequired();
695
            }
696
        }
697
698
        $this->setHint(self::HINT_LOCK_MODE, $lockMode);
699
700
        return $this;
701
    }
702
703
    /**
704
     * Get the current lock mode for this query.
705
     *
706
     * @return int|null The current lock mode of this query or NULL if no specific lock mode is set.
707
     */
708
    public function getLockMode()
709
    {
710
        $lockMode = $this->getHint(self::HINT_LOCK_MODE);
711
712
        if ($lockMode === false) {
713
            return null;
714
        }
715
716
        return $lockMode;
717
    }
718
719
    /**
720
     * Generate a cache id for the query cache - reusing the Result-Cache-Id generator.
721
     *
722
     * @return string
723
     */
724 589
    protected function getQueryCacheId()
725
    {
726 589
        ksort($this->hints);
727
728 589
        $platform = $this->getEntityManager()
729 589
            ->getConnection()
730 589
            ->getDatabasePlatform()
731 589
            ->getName();
732
733 589
        return md5(
734 589
            $this->getDQL() . serialize($this->hints) .
735 589
            '&platform=' . $platform .
736 589
            ($this->em->hasFilters() ? $this->em->getFilters()->getHash() : '') .
737 589
            '&firstResult=' . $this->firstResult . '&maxResult=' . $this->maxResults .
738 589
            '&hydrationMode=' . $this->hydrationMode . '&types=' . serialize($this->parsedTypes) . 'DOCTRINE_QUERY_CACHE_SALT'
739
        );
740
    }
741
742
    /**
743
     * {@inheritdoc}
744
     */
745 28
    protected function getHash()
746
    {
747 28
        return sha1(parent::getHash() . '-' . $this->firstResult . '-' . $this->maxResults);
748
    }
749
750
    /**
751
     * Cleanup Query resource when clone is called.
752
     */
753 139
    public function __clone()
754
    {
755 139
        parent::__clone();
756
757 139
        $this->state = self::STATE_DIRTY;
758 139
    }
759
}
760