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