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