123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797 |
- <?php
- /*
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *
- * This software consists of voluntary contributions made by many individuals
- * and is licensed under the MIT license. For more information, see
- * <http://www.doctrine-project.org>.
- */
- namespace Doctrine\ORM;
- use Doctrine\Common\Cache\Cache;
- use Doctrine\Common\Collections\ArrayCollection;
- use Doctrine\DBAL\LockMode;
- use Doctrine\DBAL\Types\Type;
- use Doctrine\ORM\Internal\Hydration\IterableResult;
- use Doctrine\ORM\Mapping\ClassMetadata;
- use Doctrine\ORM\Query\AST\DeleteStatement;
- use Doctrine\ORM\Query\AST\SelectStatement;
- use Doctrine\ORM\Query\AST\UpdateStatement;
- use Doctrine\ORM\Query\Exec\AbstractSqlExecutor;
- use Doctrine\ORM\Query\Parameter;
- use Doctrine\ORM\Query\ParameterTypeInferer;
- use Doctrine\ORM\Query\Parser;
- use Doctrine\ORM\Query\ParserResult;
- use Doctrine\ORM\Query\QueryException;
- use Doctrine\ORM\Utility\HierarchyDiscriminatorResolver;
- use function array_keys;
- use function array_values;
- use function assert;
- use function count;
- use function in_array;
- use function ksort;
- use function md5;
- use function reset;
- use function serialize;
- use function sha1;
- use function stripos;
- /**
- * A Query object represents a DQL query.
- */
- final class Query extends AbstractQuery
- {
- /**
- * A query object is in CLEAN state when it has NO unparsed/unprocessed DQL parts.
- */
- public const STATE_CLEAN = 1;
- /**
- * A query object is in state DIRTY when it has DQL parts that have not yet been
- * parsed/processed. This is automatically defined as DIRTY when addDqlQueryPart
- * is called.
- */
- public const STATE_DIRTY = 2;
- /* Query HINTS */
- /**
- * The refresh hint turns any query into a refresh query with the result that
- * any local changes in entities are overridden with the fetched values.
- */
- public const HINT_REFRESH = 'doctrine.refresh';
- public const HINT_CACHE_ENABLED = 'doctrine.cache.enabled';
- public const HINT_CACHE_EVICT = 'doctrine.cache.evict';
- /**
- * Internal hint: is set to the proxy entity that is currently triggered for loading
- */
- public const HINT_REFRESH_ENTITY = 'doctrine.refresh.entity';
- /**
- * The forcePartialLoad query hint forces a particular query to return
- * partial objects.
- *
- * @todo Rename: HINT_OPTIMIZE
- */
- public const HINT_FORCE_PARTIAL_LOAD = 'doctrine.forcePartialLoad';
- /**
- * The includeMetaColumns query hint causes meta columns like foreign keys and
- * discriminator columns to be selected and returned as part of the query result.
- *
- * This hint does only apply to non-object queries.
- */
- public const HINT_INCLUDE_META_COLUMNS = 'doctrine.includeMetaColumns';
- /**
- * An array of class names that implement \Doctrine\ORM\Query\TreeWalker and
- * are iterated and executed after the DQL has been parsed into an AST.
- */
- public const HINT_CUSTOM_TREE_WALKERS = 'doctrine.customTreeWalkers';
- /**
- * A string with a class name that implements \Doctrine\ORM\Query\TreeWalker
- * and is used for generating the target SQL from any DQL AST tree.
- */
- public const HINT_CUSTOM_OUTPUT_WALKER = 'doctrine.customOutputWalker';
- //const HINT_READ_ONLY = 'doctrine.readOnly';
- public const HINT_INTERNAL_ITERATION = 'doctrine.internal.iteration';
- public const HINT_LOCK_MODE = 'doctrine.lockMode';
- /**
- * The current state of this query.
- *
- * @var int
- */
- private $_state = self::STATE_DIRTY;
- /**
- * A snapshot of the parameter types the query was parsed with.
- *
- * @var array<string,Type>
- */
- private $parsedTypes = [];
- /**
- * Cached DQL query.
- *
- * @var string|null
- */
- private $dql = null;
- /**
- * The parser result that holds DQL => SQL information.
- *
- * @var ParserResult
- */
- private $parserResult;
- /**
- * The first result to return (the "offset").
- *
- * @var int|null
- */
- private $firstResult = null;
- /**
- * The maximum number of results to return (the "limit").
- *
- * @var int|null
- */
- private $maxResults = null;
- /**
- * The cache driver used for caching queries.
- *
- * @var Cache|null
- */
- private $queryCache;
- /**
- * Whether or not expire the query cache.
- *
- * @var bool
- */
- private $expireQueryCache = false;
- /**
- * The query cache lifetime.
- *
- * @var int
- */
- private $queryCacheTTL;
- /**
- * Whether to use a query cache, if available. Defaults to TRUE.
- *
- * @var bool
- */
- private $useQueryCache = true;
- /**
- * Gets the SQL query/queries that correspond to this DQL query.
- *
- * @return mixed The built sql query or an array of all sql queries.
- *
- * @override
- */
- public function getSQL()
- {
- return $this->parse()->getSqlExecutor()->getSqlStatements();
- }
- /**
- * Returns the corresponding AST for this DQL query.
- *
- * @return SelectStatement|UpdateStatement|DeleteStatement
- */
- public function getAST()
- {
- $parser = new Parser($this);
- return $parser->getAST();
- }
- /**
- * {@inheritdoc}
- */
- protected function getResultSetMapping()
- {
- // parse query or load from cache
- if ($this->_resultSetMapping === null) {
- $this->_resultSetMapping = $this->parse()->getResultSetMapping();
- }
- return $this->_resultSetMapping;
- }
- /**
- * Parses the DQL query, if necessary, and stores the parser result.
- *
- * Note: Populates $this->_parserResult as a side-effect.
- *
- * @return ParserResult
- */
- private function parse()
- {
- $types = [];
- foreach ($this->parameters as $parameter) {
- /** @var Query\Parameter $parameter */
- $types[$parameter->getName()] = $parameter->getType();
- }
- // Return previous parser result if the query and the filter collection are both clean
- if ($this->_state === self::STATE_CLEAN && $this->parsedTypes === $types && $this->_em->isFiltersStateClean()) {
- return $this->parserResult;
- }
- $this->_state = self::STATE_CLEAN;
- $this->parsedTypes = $types;
- $queryCache = $this->getQueryCacheDriver();
- // Check query cache.
- if (! ($this->useQueryCache && $queryCache)) {
- $parser = new Parser($this);
- $this->parserResult = $parser->parse();
- return $this->parserResult;
- }
- $hash = $this->getQueryCacheId();
- $cached = $this->expireQueryCache ? false : $queryCache->fetch($hash);
- if ($cached instanceof ParserResult) {
- // Cache hit.
- $this->parserResult = $cached;
- return $this->parserResult;
- }
- // Cache miss.
- $parser = new Parser($this);
- $this->parserResult = $parser->parse();
- $queryCache->save($hash, $this->parserResult, $this->queryCacheTTL);
- return $this->parserResult;
- }
- /**
- * {@inheritdoc}
- */
- protected function _doExecute()
- {
- $executor = $this->parse()->getSqlExecutor();
- if ($this->_queryCacheProfile) {
- $executor->setQueryCacheProfile($this->_queryCacheProfile);
- } else {
- $executor->removeQueryCacheProfile();
- }
- if ($this->_resultSetMapping === null) {
- $this->_resultSetMapping = $this->parserResult->getResultSetMapping();
- }
- // Prepare parameters
- $paramMappings = $this->parserResult->getParameterMappings();
- $paramCount = count($this->parameters);
- $mappingCount = count($paramMappings);
- if ($paramCount > $mappingCount) {
- throw QueryException::tooManyParameters($mappingCount, $paramCount);
- }
- if ($paramCount < $mappingCount) {
- throw QueryException::tooFewParameters($mappingCount, $paramCount);
- }
- // evict all cache for the entity region
- if ($this->hasCache && isset($this->_hints[self::HINT_CACHE_EVICT]) && $this->_hints[self::HINT_CACHE_EVICT]) {
- $this->evictEntityCacheRegion();
- }
- [$sqlParams, $types] = $this->processParameterMappings($paramMappings);
- $this->evictResultSetCache(
- $executor,
- $sqlParams,
- $types,
- $this->_em->getConnection()->getParams()
- );
- return $executor->execute($this->_em->getConnection(), $sqlParams, $types);
- }
- /**
- * @param array<string,mixed> $sqlParams
- * @param array<string,Type> $types
- * @param array<string,mixed> $connectionParams
- */
- private function evictResultSetCache(
- AbstractSqlExecutor $executor,
- array $sqlParams,
- array $types,
- array $connectionParams
- ) {
- if ($this->_queryCacheProfile === null || ! $this->getExpireResultCache()) {
- return;
- }
- $cacheDriver = $this->_queryCacheProfile->getResultCacheDriver();
- $statements = (array) $executor->getSqlStatements(); // Type casted since it can either be a string or an array
- foreach ($statements as $statement) {
- $cacheKeys = $this->_queryCacheProfile->generateCacheKeys($statement, $sqlParams, $types, $connectionParams);
- $cacheDriver->delete(reset($cacheKeys));
- }
- }
- /**
- * Evict entity cache region
- */
- private function evictEntityCacheRegion()
- {
- $AST = $this->getAST();
- if ($AST instanceof SelectStatement) {
- throw new QueryException('The hint "HINT_CACHE_EVICT" is not valid for select statements.');
- }
- $className = $AST instanceof DeleteStatement
- ? $AST->deleteClause->abstractSchemaName
- : $AST->updateClause->abstractSchemaName;
- $this->_em->getCache()->evictEntityRegion($className);
- }
- /**
- * Processes query parameter mappings.
- *
- * @param Parameter[] $paramMappings
- *
- * @return mixed[][]
- *
- * @throws Query\QueryException
- *
- * @psalm-return array{0: list<mixed>, 1: array}
- */
- private function processParameterMappings(array $paramMappings): array
- {
- $sqlParams = [];
- $types = [];
- foreach ($this->parameters as $parameter) {
- $key = $parameter->getName();
- if (! isset($paramMappings[$key])) {
- throw QueryException::unknownParameter($key);
- }
- [$value, $type] = $this->resolveParameterValue($parameter);
- foreach ($paramMappings[$key] as $position) {
- $types[$position] = $type;
- }
- $sqlPositions = $paramMappings[$key];
- // optimized multi value sql positions away for now,
- // they are not allowed in DQL anyways.
- $value = [$value];
- $countValue = count($value);
- for ($i = 0, $l = count($sqlPositions); $i < $l; $i++) {
- $sqlParams[$sqlPositions[$i]] = $value[$i % $countValue];
- }
- }
- if (count($sqlParams) !== count($types)) {
- throw QueryException::parameterTypeMismatch();
- }
- if ($sqlParams) {
- ksort($sqlParams);
- $sqlParams = array_values($sqlParams);
- ksort($types);
- $types = array_values($types);
- }
- return [$sqlParams, $types];
- }
- /**
- * @return mixed[] tuple of (value, type)
- *
- * @psalm-return array{0: mixed, 1: mixed}
- */
- private function resolveParameterValue(Parameter $parameter): array
- {
- if ($parameter->typeWasSpecified()) {
- return [$parameter->getValue(), $parameter->getType()];
- }
- $key = $parameter->getName();
- $originalValue = $parameter->getValue();
- $value = $originalValue;
- $rsm = $this->getResultSetMapping();
- assert($rsm !== null);
- if ($value instanceof ClassMetadata && isset($rsm->metadataParameterMapping[$key])) {
- $value = $value->getMetadataValue($rsm->metadataParameterMapping[$key]);
- }
- if ($value instanceof ClassMetadata && isset($rsm->discriminatorParameters[$key])) {
- $value = array_keys(HierarchyDiscriminatorResolver::resolveDiscriminatorsForClass($value, $this->_em));
- }
- $processedValue = $this->processParameterValue($value);
- return [
- $processedValue,
- $originalValue === $processedValue
- ? $parameter->getType()
- : ParameterTypeInferer::inferType($processedValue),
- ];
- }
- /**
- * Defines a cache driver to be used for caching queries.
- *
- * @param Cache|null $queryCache Cache driver.
- *
- * @return self This query instance.
- */
- public function setQueryCacheDriver($queryCache): self
- {
- $this->queryCache = $queryCache;
- return $this;
- }
- /**
- * Defines whether the query should make use of a query cache, if available.
- *
- * @param bool $bool
- *
- * @return self This query instance.
- */
- public function useQueryCache($bool): self
- {
- $this->useQueryCache = $bool;
- return $this;
- }
- /**
- * Returns the cache driver used for query caching.
- *
- * @return Cache|null The cache driver used for query caching or NULL, if
- * this Query does not use query caching.
- */
- public function getQueryCacheDriver()
- {
- if ($this->queryCache) {
- return $this->queryCache;
- }
- return $this->_em->getConfiguration()->getQueryCacheImpl();
- }
- /**
- * Defines how long the query cache will be active before expire.
- *
- * @param int $timeToLive How long the cache entry is valid.
- *
- * @return self This query instance.
- */
- public function setQueryCacheLifetime($timeToLive): self
- {
- if ($timeToLive !== null) {
- $timeToLive = (int) $timeToLive;
- }
- $this->queryCacheTTL = $timeToLive;
- return $this;
- }
- /**
- * Retrieves the lifetime of resultset cache.
- *
- * @return int
- */
- public function getQueryCacheLifetime()
- {
- return $this->queryCacheTTL;
- }
- /**
- * Defines if the query cache is active or not.
- *
- * @param bool $expire Whether or not to force query cache expiration.
- *
- * @return self This query instance.
- */
- public function expireQueryCache($expire = true): self
- {
- $this->expireQueryCache = $expire;
- return $this;
- }
- /**
- * Retrieves if the query cache is active or not.
- *
- * @return bool
- */
- public function getExpireQueryCache()
- {
- return $this->expireQueryCache;
- }
- /**
- * @override
- */
- public function free()
- {
- parent::free();
- $this->dql = null;
- $this->_state = self::STATE_CLEAN;
- }
- /**
- * Sets a DQL query string.
- *
- * @param string $dqlQuery DQL Query.
- */
- public function setDQL($dqlQuery): self
- {
- if ($dqlQuery !== null) {
- $this->dql = $dqlQuery;
- $this->_state = self::STATE_DIRTY;
- }
- return $this;
- }
- /**
- * Returns the DQL query that is represented by this query object.
- *
- * @return string|null
- */
- public function getDQL()
- {
- return $this->dql;
- }
- /**
- * Returns the state of this query object
- * By default the type is Doctrine_ORM_Query_Abstract::STATE_CLEAN but if it appears any unprocessed DQL
- * part, it is switched to Doctrine_ORM_Query_Abstract::STATE_DIRTY.
- *
- * @see AbstractQuery::STATE_CLEAN
- * @see AbstractQuery::STATE_DIRTY
- *
- * @return int The query state.
- */
- public function getState()
- {
- return $this->_state;
- }
- /**
- * Method to check if an arbitrary piece of DQL exists
- *
- * @param string $dql Arbitrary piece of DQL to check for.
- *
- * @return bool
- */
- public function contains($dql)
- {
- return stripos($this->getDQL(), $dql) !== false;
- }
- /**
- * Sets the position of the first result to retrieve (the "offset").
- *
- * @param int|null $firstResult The first result to return.
- *
- * @return self This query object.
- */
- public function setFirstResult($firstResult): self
- {
- $this->firstResult = $firstResult;
- $this->_state = self::STATE_DIRTY;
- return $this;
- }
- /**
- * Gets the position of the first result the query object was set to retrieve (the "offset").
- * Returns NULL if {@link setFirstResult} was not applied to this query.
- *
- * @return int|null The position of the first result.
- */
- public function getFirstResult()
- {
- return $this->firstResult;
- }
- /**
- * Sets the maximum number of results to retrieve (the "limit").
- *
- * @param int|null $maxResults
- *
- * @return self This query object.
- */
- public function setMaxResults($maxResults): self
- {
- $this->maxResults = $maxResults;
- $this->_state = self::STATE_DIRTY;
- return $this;
- }
- /**
- * Gets the maximum number of results the query object was set to retrieve (the "limit").
- * Returns NULL if {@link setMaxResults} was not applied to this query.
- *
- * @return int|null Maximum number of results.
- */
- public function getMaxResults()
- {
- return $this->maxResults;
- }
- /**
- * Executes the query and returns an IterableResult that can be used to incrementally
- * iterated over the result.
- *
- * @deprecated
- *
- * @param ArrayCollection|mixed[]|null $parameters The query parameters.
- * @param string|int $hydrationMode The hydration mode to use.
- *
- * @return IterableResult
- */
- public function iterate($parameters = null, $hydrationMode = self::HYDRATE_OBJECT)
- {
- $this->setHint(self::HINT_INTERNAL_ITERATION, true);
- return parent::iterate($parameters, $hydrationMode);
- }
- /** {@inheritDoc} */
- public function toIterable(iterable $parameters = [], $hydrationMode = self::HYDRATE_OBJECT): iterable
- {
- $this->setHint(self::HINT_INTERNAL_ITERATION, true);
- return parent::toIterable($parameters, $hydrationMode);
- }
- /**
- * {@inheritdoc}
- */
- public function setHint($name, $value)
- {
- $this->_state = self::STATE_DIRTY;
- return parent::setHint($name, $value);
- }
- /**
- * {@inheritdoc}
- */
- public function setHydrationMode($hydrationMode)
- {
- $this->_state = self::STATE_DIRTY;
- return parent::setHydrationMode($hydrationMode);
- }
- /**
- * Set the lock mode for this Query.
- *
- * @see \Doctrine\DBAL\LockMode
- *
- * @param int $lockMode
- *
- * @throws TransactionRequiredException
- */
- public function setLockMode($lockMode): self
- {
- if (in_array($lockMode, [LockMode::NONE, LockMode::PESSIMISTIC_READ, LockMode::PESSIMISTIC_WRITE], true)) {
- if (! $this->_em->getConnection()->isTransactionActive()) {
- throw TransactionRequiredException::transactionRequired();
- }
- }
- $this->setHint(self::HINT_LOCK_MODE, $lockMode);
- return $this;
- }
- /**
- * Get the current lock mode for this query.
- *
- * @return int|null The current lock mode of this query or NULL if no specific lock mode is set.
- */
- public function getLockMode()
- {
- $lockMode = $this->getHint(self::HINT_LOCK_MODE);
- if ($lockMode === false) {
- return null;
- }
- return $lockMode;
- }
- /**
- * Generate a cache id for the query cache - reusing the Result-Cache-Id generator.
- */
- protected function getQueryCacheId(): string
- {
- ksort($this->_hints);
- $platform = $this->getEntityManager()
- ->getConnection()
- ->getDatabasePlatform()
- ->getName();
- return md5(
- $this->getDQL() . serialize($this->_hints) .
- '&platform=' . $platform .
- ($this->_em->hasFilters() ? $this->_em->getFilters()->getHash() : '') .
- '&firstResult=' . $this->firstResult . '&maxResult=' . $this->maxResults .
- '&hydrationMode=' . $this->_hydrationMode . '&types=' . serialize($this->parsedTypes) . 'DOCTRINE_QUERY_CACHE_SALT'
- );
- }
- /**
- * {@inheritdoc}
- */
- protected function getHash()
- {
- return sha1(parent::getHash() . '-' . $this->firstResult . '-' . $this->maxResults);
- }
- /**
- * Cleanup Query resource when clone is called.
- *
- * @return void
- */
- public function __clone()
- {
- parent::__clone();
- $this->_state = self::STATE_DIRTY;
- }
- }
|