Failed Conditions
Pull Request — master (#7085)
by Guilherme
14:34
created

lib/Doctrine/ORM/Query.php (2 issues)

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\ORM;
6
7
use Doctrine\Common\Cache\Cache;
0 ignored issues
show
This use statement conflicts with another class in this namespace, Doctrine\ORM\Cache. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

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