vendor/doctrine/orm/lib/Doctrine/ORM/AbstractQuery.php line 907

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. namespace Doctrine\ORM;
  4. use Countable;
  5. use Doctrine\Common\Cache\Psr6\CacheAdapter;
  6. use Doctrine\Common\Cache\Psr6\DoctrineProvider;
  7. use Doctrine\Common\Collections\ArrayCollection;
  8. use Doctrine\Common\Collections\Collection;
  9. use Doctrine\DBAL\Cache\QueryCacheProfile;
  10. use Doctrine\DBAL\Result;
  11. use Doctrine\Deprecations\Deprecation;
  12. use Doctrine\ORM\Cache\Exception\InvalidResultCacheDriver;
  13. use Doctrine\ORM\Cache\Logging\CacheLogger;
  14. use Doctrine\ORM\Cache\QueryCacheKey;
  15. use Doctrine\ORM\Cache\TimestampCacheKey;
  16. use Doctrine\ORM\Internal\Hydration\IterableResult;
  17. use Doctrine\ORM\Mapping\MappingException as ORMMappingException;
  18. use Doctrine\ORM\Query\Parameter;
  19. use Doctrine\ORM\Query\QueryException;
  20. use Doctrine\ORM\Query\ResultSetMapping;
  21. use Doctrine\Persistence\Mapping\MappingException;
  22. use LogicException;
  23. use Psr\Cache\CacheItemPoolInterface;
  24. use Traversable;
  25. use function array_map;
  26. use function array_shift;
  27. use function assert;
  28. use function count;
  29. use function is_array;
  30. use function is_numeric;
  31. use function is_object;
  32. use function is_scalar;
  33. use function iterator_count;
  34. use function iterator_to_array;
  35. use function ksort;
  36. use function method_exists;
  37. use function reset;
  38. use function serialize;
  39. use function sha1;
  40. /**
  41. * Base contract for ORM queries. Base class for Query and NativeQuery.
  42. *
  43. * @link www.doctrine-project.org
  44. */
  45. abstract class AbstractQuery
  46. {
  47. /* Hydration mode constants */
  48. /**
  49. * Hydrates an object graph. This is the default behavior.
  50. */
  51. public const HYDRATE_OBJECT = 1;
  52. /**
  53. * Hydrates an array graph.
  54. */
  55. public const HYDRATE_ARRAY = 2;
  56. /**
  57. * Hydrates a flat, rectangular result set with scalar values.
  58. */
  59. public const HYDRATE_SCALAR = 3;
  60. /**
  61. * Hydrates a single scalar value.
  62. */
  63. public const HYDRATE_SINGLE_SCALAR = 4;
  64. /**
  65. * Very simple object hydrator (optimized for performance).
  66. */
  67. public const HYDRATE_SIMPLEOBJECT = 5;
  68. /**
  69. * Hydrates scalar column value.
  70. */
  71. public const HYDRATE_SCALAR_COLUMN = 6;
  72. /**
  73. * The parameter map of this query.
  74. *
  75. * @var ArrayCollection|Parameter[]
  76. * @psalm-var ArrayCollection<int, Parameter>
  77. */
  78. protected $parameters;
  79. /**
  80. * The user-specified ResultSetMapping to use.
  81. *
  82. * @var ResultSetMapping|null
  83. */
  84. protected $_resultSetMapping;
  85. /**
  86. * The entity manager used by this query object.
  87. *
  88. * @var EntityManagerInterface
  89. */
  90. protected $_em;
  91. /**
  92. * The map of query hints.
  93. *
  94. * @psalm-var array<string, mixed>
  95. */
  96. protected $_hints = [];
  97. /**
  98. * The hydration mode.
  99. *
  100. * @var string|int
  101. * @psalm-var string|AbstractQuery::HYDRATE_*
  102. */
  103. protected $_hydrationMode = self::HYDRATE_OBJECT;
  104. /** @var QueryCacheProfile|null */
  105. protected $_queryCacheProfile;
  106. /**
  107. * Whether or not expire the result cache.
  108. *
  109. * @var bool
  110. */
  111. protected $_expireResultCache = false;
  112. /** @var QueryCacheProfile|null */
  113. protected $_hydrationCacheProfile;
  114. /**
  115. * Whether to use second level cache, if available.
  116. *
  117. * @var bool
  118. */
  119. protected $cacheable = false;
  120. /** @var bool */
  121. protected $hasCache = false;
  122. /**
  123. * Second level cache region name.
  124. *
  125. * @var string|null
  126. */
  127. protected $cacheRegion;
  128. /**
  129. * Second level query cache mode.
  130. *
  131. * @var int|null
  132. * @psalm-var Cache::MODE_*|null
  133. */
  134. protected $cacheMode;
  135. /** @var CacheLogger|null */
  136. protected $cacheLogger;
  137. /** @var int */
  138. protected $lifetime = 0;
  139. /**
  140. * Initializes a new instance of a class derived from <tt>AbstractQuery</tt>.
  141. */
  142. public function __construct(EntityManagerInterface $em)
  143. {
  144. $this->_em = $em;
  145. $this->parameters = new ArrayCollection();
  146. $this->_hints = $em->getConfiguration()->getDefaultQueryHints();
  147. $this->hasCache = $this->_em->getConfiguration()->isSecondLevelCacheEnabled();
  148. if ($this->hasCache) {
  149. $this->cacheLogger = $em->getConfiguration()
  150. ->getSecondLevelCacheConfiguration()
  151. ->getCacheLogger();
  152. }
  153. }
  154. /**
  155. * Enable/disable second level query (result) caching for this query.
  156. *
  157. * @param bool $cacheable
  158. *
  159. * @return $this
  160. */
  161. public function setCacheable($cacheable)
  162. {
  163. $this->cacheable = (bool) $cacheable;
  164. return $this;
  165. }
  166. /**
  167. * @return bool TRUE if the query results are enable for second level cache, FALSE otherwise.
  168. */
  169. public function isCacheable()
  170. {
  171. return $this->cacheable;
  172. }
  173. /**
  174. * @param string $cacheRegion
  175. *
  176. * @return $this
  177. */
  178. public function setCacheRegion($cacheRegion)
  179. {
  180. $this->cacheRegion = (string) $cacheRegion;
  181. return $this;
  182. }
  183. /**
  184. * Obtain the name of the second level query cache region in which query results will be stored
  185. *
  186. * @return string|null The cache region name; NULL indicates the default region.
  187. */
  188. public function getCacheRegion()
  189. {
  190. return $this->cacheRegion;
  191. }
  192. /**
  193. * @return bool TRUE if the query cache and second level cache are enabled, FALSE otherwise.
  194. */
  195. protected function isCacheEnabled()
  196. {
  197. return $this->cacheable && $this->hasCache;
  198. }
  199. /**
  200. * @return int
  201. */
  202. public function getLifetime()
  203. {
  204. return $this->lifetime;
  205. }
  206. /**
  207. * Sets the life-time for this query into second level cache.
  208. *
  209. * @param int $lifetime
  210. *
  211. * @return $this
  212. */
  213. public function setLifetime($lifetime)
  214. {
  215. $this->lifetime = (int) $lifetime;
  216. return $this;
  217. }
  218. /**
  219. * @return int|null
  220. * @psalm-return Cache::MODE_*|null
  221. */
  222. public function getCacheMode()
  223. {
  224. return $this->cacheMode;
  225. }
  226. /**
  227. * @param int $cacheMode
  228. * @psalm-param Cache::MODE_* $cacheMode
  229. *
  230. * @return $this
  231. */
  232. public function setCacheMode($cacheMode)
  233. {
  234. $this->cacheMode = (int) $cacheMode;
  235. return $this;
  236. }
  237. /**
  238. * Gets the SQL query that corresponds to this query object.
  239. * The returned SQL syntax depends on the connection driver that is used
  240. * by this query object at the time of this method call.
  241. *
  242. * @return string SQL query
  243. */
  244. abstract public function getSQL();
  245. /**
  246. * Retrieves the associated EntityManager of this Query instance.
  247. *
  248. * @return EntityManagerInterface
  249. */
  250. public function getEntityManager()
  251. {
  252. return $this->_em;
  253. }
  254. /**
  255. * Frees the resources used by the query object.
  256. *
  257. * Resets Parameters, Parameter Types and Query Hints.
  258. *
  259. * @return void
  260. */
  261. public function free()
  262. {
  263. $this->parameters = new ArrayCollection();
  264. $this->_hints = $this->_em->getConfiguration()->getDefaultQueryHints();
  265. }
  266. /**
  267. * Get all defined parameters.
  268. *
  269. * @return ArrayCollection The defined query parameters.
  270. * @psalm-return ArrayCollection<int, Parameter>
  271. */
  272. public function getParameters()
  273. {
  274. return $this->parameters;
  275. }
  276. /**
  277. * Gets a query parameter.
  278. *
  279. * @param mixed $key The key (index or name) of the bound parameter.
  280. *
  281. * @return Parameter|null The value of the bound parameter, or NULL if not available.
  282. */
  283. public function getParameter($key)
  284. {
  285. $key = Query\Parameter::normalizeName($key);
  286. $filteredParameters = $this->parameters->filter(
  287. static function (Query\Parameter $parameter) use ($key): bool {
  288. $parameterName = $parameter->getName();
  289. return $key === $parameterName;
  290. }
  291. );
  292. return ! $filteredParameters->isEmpty() ? $filteredParameters->first() : null;
  293. }
  294. /**
  295. * Sets a collection of query parameters.
  296. *
  297. * @param ArrayCollection|mixed[] $parameters
  298. * @psalm-param ArrayCollection<int, Parameter>|mixed[] $parameters
  299. *
  300. * @return $this
  301. */
  302. public function setParameters($parameters)
  303. {
  304. // BC compatibility with 2.3-
  305. if (is_array($parameters)) {
  306. /** @psalm-var ArrayCollection<int, Parameter> $parameterCollection */
  307. $parameterCollection = new ArrayCollection();
  308. foreach ($parameters as $key => $value) {
  309. $parameterCollection->add(new Parameter($key, $value));
  310. }
  311. $parameters = $parameterCollection;
  312. }
  313. $this->parameters = $parameters;
  314. return $this;
  315. }
  316. /**
  317. * Sets a query parameter.
  318. *
  319. * @param string|int $key The parameter position or name.
  320. * @param mixed $value The parameter value.
  321. * @param string|int|null $type The parameter type. If specified, the given value will be run through
  322. * the type conversion of this type. This is usually not needed for
  323. * strings and numeric types.
  324. *
  325. * @return $this
  326. */
  327. public function setParameter($key, $value, $type = null)
  328. {
  329. $existingParameter = $this->getParameter($key);
  330. if ($existingParameter !== null) {
  331. $existingParameter->setValue($value, $type);
  332. return $this;
  333. }
  334. $this->parameters->add(new Parameter($key, $value, $type));
  335. return $this;
  336. }
  337. /**
  338. * Processes an individual parameter value.
  339. *
  340. * @param mixed $value
  341. *
  342. * @return mixed[]|string|int|float|bool
  343. * @psalm-return array|scalar
  344. *
  345. * @throws ORMInvalidArgumentException
  346. */
  347. public function processParameterValue($value)
  348. {
  349. if (is_scalar($value)) {
  350. return $value;
  351. }
  352. if ($value instanceof Collection) {
  353. $value = iterator_to_array($value);
  354. }
  355. if (is_array($value)) {
  356. $value = $this->processArrayParameterValue($value);
  357. return $value;
  358. }
  359. if ($value instanceof Mapping\ClassMetadata) {
  360. return $value->name;
  361. }
  362. if (! is_object($value)) {
  363. return $value;
  364. }
  365. try {
  366. $value = $this->_em->getUnitOfWork()->getSingleIdentifierValue($value);
  367. if ($value === null) {
  368. throw ORMInvalidArgumentException::invalidIdentifierBindingEntity();
  369. }
  370. } catch (MappingException | ORMMappingException $e) {
  371. /* Silence any mapping exceptions. These can occur if the object in
  372. question is not a mapped entity, in which case we just don't do
  373. any preparation on the value.
  374. Depending on MappingDriver, either MappingException or
  375. ORMMappingException is thrown. */
  376. $value = $this->potentiallyProcessIterable($value);
  377. }
  378. return $value;
  379. }
  380. /**
  381. * If no mapping is detected, trying to resolve the value as a Traversable
  382. *
  383. * @param mixed $value
  384. *
  385. * @return mixed
  386. */
  387. private function potentiallyProcessIterable($value)
  388. {
  389. if ($value instanceof Traversable) {
  390. $value = iterator_to_array($value);
  391. $value = $this->processArrayParameterValue($value);
  392. }
  393. return $value;
  394. }
  395. /**
  396. * Process a parameter value which was previously identified as an array
  397. *
  398. * @param mixed[] $value
  399. *
  400. * @return mixed[]
  401. */
  402. private function processArrayParameterValue(array $value): array
  403. {
  404. foreach ($value as $key => $paramValue) {
  405. $paramValue = $this->processParameterValue($paramValue);
  406. $value[$key] = is_array($paramValue) ? reset($paramValue) : $paramValue;
  407. }
  408. return $value;
  409. }
  410. /**
  411. * Sets the ResultSetMapping that should be used for hydration.
  412. *
  413. * @return $this
  414. */
  415. public function setResultSetMapping(Query\ResultSetMapping $rsm)
  416. {
  417. $this->translateNamespaces($rsm);
  418. $this->_resultSetMapping = $rsm;
  419. return $this;
  420. }
  421. /**
  422. * Gets the ResultSetMapping used for hydration.
  423. *
  424. * @return ResultSetMapping|null
  425. */
  426. protected function getResultSetMapping()
  427. {
  428. return $this->_resultSetMapping;
  429. }
  430. /**
  431. * Allows to translate entity namespaces to full qualified names.
  432. */
  433. private function translateNamespaces(Query\ResultSetMapping $rsm): void
  434. {
  435. $translate = function ($alias): string {
  436. return $this->_em->getClassMetadata($alias)->getName();
  437. };
  438. $rsm->aliasMap = array_map($translate, $rsm->aliasMap);
  439. $rsm->declaringClasses = array_map($translate, $rsm->declaringClasses);
  440. }
  441. /**
  442. * Set a cache profile for hydration caching.
  443. *
  444. * If no result cache driver is set in the QueryCacheProfile, the default
  445. * result cache driver is used from the configuration.
  446. *
  447. * Important: Hydration caching does NOT register entities in the
  448. * UnitOfWork when retrieved from the cache. Never use result cached
  449. * entities for requests that also flush the EntityManager. If you want
  450. * some form of caching with UnitOfWork registration you should use
  451. * {@see AbstractQuery::setResultCacheProfile()}.
  452. *
  453. * @return $this
  454. *
  455. * @example
  456. * $lifetime = 100;
  457. * $resultKey = "abc";
  458. * $query->setHydrationCacheProfile(new QueryCacheProfile());
  459. * $query->setHydrationCacheProfile(new QueryCacheProfile($lifetime, $resultKey));
  460. */
  461. public function setHydrationCacheProfile(?QueryCacheProfile $profile = null)
  462. {
  463. if ($profile === null) {
  464. $this->_hydrationCacheProfile = null;
  465. return $this;
  466. }
  467. // DBAL 2
  468. if (! method_exists(QueryCacheProfile::class, 'setResultCache')) {
  469. if (! $profile->getResultCacheDriver()) {
  470. $defaultHydrationCacheImpl = $this->_em->getConfiguration()->getHydrationCache();
  471. if ($defaultHydrationCacheImpl) {
  472. $profile = $profile->setResultCacheDriver(DoctrineProvider::wrap($defaultHydrationCacheImpl));
  473. }
  474. }
  475. } elseif (! $profile->getResultCache()) {
  476. $defaultHydrationCacheImpl = $this->_em->getConfiguration()->getHydrationCache();
  477. if ($defaultHydrationCacheImpl) {
  478. $profile = $profile->setResultCache($defaultHydrationCacheImpl);
  479. }
  480. }
  481. $this->_hydrationCacheProfile = $profile;
  482. return $this;
  483. }
  484. /**
  485. * @return QueryCacheProfile|null
  486. */
  487. public function getHydrationCacheProfile()
  488. {
  489. return $this->_hydrationCacheProfile;
  490. }
  491. /**
  492. * Set a cache profile for the result cache.
  493. *
  494. * If no result cache driver is set in the QueryCacheProfile, the default
  495. * result cache driver is used from the configuration.
  496. *
  497. * @return $this
  498. */
  499. public function setResultCacheProfile(?QueryCacheProfile $profile = null)
  500. {
  501. if ($profile === null) {
  502. $this->_queryCacheProfile = null;
  503. return $this;
  504. }
  505. // DBAL 2
  506. if (! method_exists(QueryCacheProfile::class, 'setResultCache')) {
  507. if (! $profile->getResultCacheDriver()) {
  508. $defaultResultCacheDriver = $this->_em->getConfiguration()->getResultCache();
  509. if ($defaultResultCacheDriver) {
  510. $profile = $profile->setResultCacheDriver(DoctrineProvider::wrap($defaultResultCacheDriver));
  511. }
  512. }
  513. } elseif (! $profile->getResultCache()) {
  514. $defaultResultCache = $this->_em->getConfiguration()->getResultCache();
  515. if ($defaultResultCache) {
  516. $profile = $profile->setResultCache($defaultResultCache);
  517. }
  518. }
  519. $this->_queryCacheProfile = $profile;
  520. return $this;
  521. }
  522. /**
  523. * Defines a cache driver to be used for caching result sets and implicitly enables caching.
  524. *
  525. * @deprecated Use {@see setResultCache()} instead.
  526. *
  527. * @param \Doctrine\Common\Cache\Cache|null $resultCacheDriver Cache driver
  528. *
  529. * @return $this
  530. *
  531. * @throws InvalidResultCacheDriver
  532. */
  533. public function setResultCacheDriver($resultCacheDriver = null)
  534. {
  535. /** @phpstan-ignore-next-line */
  536. if ($resultCacheDriver !== null && ! ($resultCacheDriver instanceof \Doctrine\Common\Cache\Cache)) {
  537. throw InvalidResultCacheDriver::create();
  538. }
  539. return $this->setResultCache($resultCacheDriver ? CacheAdapter::wrap($resultCacheDriver) : null);
  540. }
  541. /**
  542. * Defines a cache driver to be used for caching result sets and implicitly enables caching.
  543. *
  544. * @return $this
  545. */
  546. public function setResultCache(?CacheItemPoolInterface $resultCache = null)
  547. {
  548. if ($resultCache === null) {
  549. if ($this->_queryCacheProfile) {
  550. $this->_queryCacheProfile = new QueryCacheProfile($this->_queryCacheProfile->getLifetime(), $this->_queryCacheProfile->getCacheKey());
  551. }
  552. return $this;
  553. }
  554. // DBAL 2
  555. if (! method_exists(QueryCacheProfile::class, 'setResultCache')) {
  556. $resultCacheDriver = DoctrineProvider::wrap($resultCache);
  557. $this->_queryCacheProfile = $this->_queryCacheProfile
  558. ? $this->_queryCacheProfile->setResultCacheDriver($resultCacheDriver)
  559. : new QueryCacheProfile(0, null, $resultCacheDriver);
  560. return $this;
  561. }
  562. $this->_queryCacheProfile = $this->_queryCacheProfile
  563. ? $this->_queryCacheProfile->setResultCache($resultCache)
  564. : new QueryCacheProfile(0, null, $resultCache);
  565. return $this;
  566. }
  567. /**
  568. * Returns the cache driver used for caching result sets.
  569. *
  570. * @deprecated
  571. *
  572. * @return \Doctrine\Common\Cache\Cache Cache driver
  573. */
  574. public function getResultCacheDriver()
  575. {
  576. if ($this->_queryCacheProfile && $this->_queryCacheProfile->getResultCacheDriver()) {
  577. return $this->_queryCacheProfile->getResultCacheDriver();
  578. }
  579. return $this->_em->getConfiguration()->getResultCacheImpl();
  580. }
  581. /**
  582. * Set whether or not to cache the results of this query and if so, for
  583. * how long and which ID to use for the cache entry.
  584. *
  585. * @deprecated 2.7 Use {@see enableResultCache} and {@see disableResultCache} instead.
  586. *
  587. * @param bool $useCache Whether or not to cache the results of this query.
  588. * @param int $lifetime How long the cache entry is valid, in seconds.
  589. * @param string $resultCacheId ID to use for the cache entry.
  590. *
  591. * @return $this
  592. */
  593. public function useResultCache($useCache, $lifetime = null, $resultCacheId = null)
  594. {
  595. return $useCache
  596. ? $this->enableResultCache($lifetime, $resultCacheId)
  597. : $this->disableResultCache();
  598. }
  599. /**
  600. * Enables caching of the results of this query, for given or default amount of seconds
  601. * and optionally specifies which ID to use for the cache entry.
  602. *
  603. * @param int|null $lifetime How long the cache entry is valid, in seconds.
  604. * @param string|null $resultCacheId ID to use for the cache entry.
  605. *
  606. * @return $this
  607. */
  608. public function enableResultCache(?int $lifetime = null, ?string $resultCacheId = null): self
  609. {
  610. $this->setResultCacheLifetime($lifetime);
  611. $this->setResultCacheId($resultCacheId);
  612. return $this;
  613. }
  614. /**
  615. * Disables caching of the results of this query.
  616. *
  617. * @return $this
  618. */
  619. public function disableResultCache(): self
  620. {
  621. $this->_queryCacheProfile = null;
  622. return $this;
  623. }
  624. /**
  625. * Defines how long the result cache will be active before expire.
  626. *
  627. * @param int|null $lifetime How long the cache entry is valid, in seconds.
  628. *
  629. * @return $this
  630. */
  631. public function setResultCacheLifetime($lifetime)
  632. {
  633. $lifetime = (int) $lifetime;
  634. if ($this->_queryCacheProfile) {
  635. $this->_queryCacheProfile = $this->_queryCacheProfile->setLifetime($lifetime);
  636. return $this;
  637. }
  638. $this->_queryCacheProfile = new QueryCacheProfile($lifetime);
  639. $cache = $this->_em->getConfiguration()->getResultCache();
  640. if (! $cache) {
  641. return $this;
  642. }
  643. // Compatibility for DBAL 2
  644. if (! method_exists($this->_queryCacheProfile, 'setResultCache')) {
  645. $this->_queryCacheProfile = $this->_queryCacheProfile->setResultCacheDriver(DoctrineProvider::wrap($cache));
  646. return $this;
  647. }
  648. $this->_queryCacheProfile = $this->_queryCacheProfile->setResultCache($cache);
  649. return $this;
  650. }
  651. /**
  652. * Retrieves the lifetime of resultset cache.
  653. *
  654. * @deprecated
  655. *
  656. * @return int
  657. */
  658. public function getResultCacheLifetime()
  659. {
  660. return $this->_queryCacheProfile ? $this->_queryCacheProfile->getLifetime() : 0;
  661. }
  662. /**
  663. * Defines if the result cache is active or not.
  664. *
  665. * @param bool $expire Whether or not to force resultset cache expiration.
  666. *
  667. * @return $this
  668. */
  669. public function expireResultCache($expire = true)
  670. {
  671. $this->_expireResultCache = $expire;
  672. return $this;
  673. }
  674. /**
  675. * Retrieves if the resultset cache is active or not.
  676. *
  677. * @return bool
  678. */
  679. public function getExpireResultCache()
  680. {
  681. return $this->_expireResultCache;
  682. }
  683. /**
  684. * @return QueryCacheProfile|null
  685. */
  686. public function getQueryCacheProfile()
  687. {
  688. return $this->_queryCacheProfile;
  689. }
  690. /**
  691. * Change the default fetch mode of an association for this query.
  692. *
  693. * $fetchMode can be one of ClassMetadata::FETCH_EAGER or ClassMetadata::FETCH_LAZY
  694. *
  695. * @param string $class
  696. * @param string $assocName
  697. * @param int $fetchMode
  698. *
  699. * @return $this
  700. */
  701. public function setFetchMode($class, $assocName, $fetchMode)
  702. {
  703. if ($fetchMode !== Mapping\ClassMetadata::FETCH_EAGER) {
  704. $fetchMode = Mapping\ClassMetadata::FETCH_LAZY;
  705. }
  706. $this->_hints['fetchMode'][$class][$assocName] = $fetchMode;
  707. return $this;
  708. }
  709. /**
  710. * Defines the processing mode to be used during hydration / result set transformation.
  711. *
  712. * @param string|int $hydrationMode Doctrine processing mode to be used during hydration process.
  713. * One of the Query::HYDRATE_* constants.
  714. * @psalm-param string|AbstractQuery::HYDRATE_* $hydrationMode
  715. *
  716. * @return $this
  717. */
  718. public function setHydrationMode($hydrationMode)
  719. {
  720. $this->_hydrationMode = $hydrationMode;
  721. return $this;
  722. }
  723. /**
  724. * Gets the hydration mode currently used by the query.
  725. *
  726. * @return string|int
  727. * @psalm-return string|AbstractQuery::HYDRATE_*
  728. */
  729. public function getHydrationMode()
  730. {
  731. return $this->_hydrationMode;
  732. }
  733. /**
  734. * Gets the list of results for the query.
  735. *
  736. * Alias for execute(null, $hydrationMode = HYDRATE_OBJECT).
  737. *
  738. * @param string|int $hydrationMode
  739. * @psalm-param string|AbstractQuery::HYDRATE_* $hydrationMode
  740. *
  741. * @return mixed
  742. */
  743. public function getResult($hydrationMode = self::HYDRATE_OBJECT)
  744. {
  745. return $this->execute(null, $hydrationMode);
  746. }
  747. /**
  748. * Gets the array of results for the query.
  749. *
  750. * Alias for execute(null, HYDRATE_ARRAY).
  751. *
  752. * @return mixed[]
  753. */
  754. public function getArrayResult()
  755. {
  756. return $this->execute(null, self::HYDRATE_ARRAY);
  757. }
  758. /**
  759. * Gets one-dimensional array of results for the query.
  760. *
  761. * Alias for execute(null, HYDRATE_SCALAR_COLUMN).
  762. *
  763. * @return mixed[]
  764. */
  765. public function getSingleColumnResult()
  766. {
  767. return $this->execute(null, self::HYDRATE_SCALAR_COLUMN);
  768. }
  769. /**
  770. * Gets the scalar results for the query.
  771. *
  772. * Alias for execute(null, HYDRATE_SCALAR).
  773. *
  774. * @return mixed[]
  775. */
  776. public function getScalarResult()
  777. {
  778. return $this->execute(null, self::HYDRATE_SCALAR);
  779. }
  780. /**
  781. * Get exactly one result or null.
  782. *
  783. * @param string|int|null $hydrationMode
  784. * @psalm-param string|AbstractQuery::HYDRATE_*|null $hydrationMode
  785. *
  786. * @return mixed
  787. *
  788. * @throws NonUniqueResultException
  789. */
  790. public function getOneOrNullResult($hydrationMode = null)
  791. {
  792. try {
  793. $result = $this->execute(null, $hydrationMode);
  794. } catch (NoResultException $e) {
  795. return null;
  796. }
  797. if ($this->_hydrationMode !== self::HYDRATE_SINGLE_SCALAR && ! $result) {
  798. return null;
  799. }
  800. if (! is_array($result)) {
  801. return $result;
  802. }
  803. if (count($result) > 1) {
  804. throw new NonUniqueResultException();
  805. }
  806. return array_shift($result);
  807. }
  808. /**
  809. * Gets the single result of the query.
  810. *
  811. * Enforces the presence as well as the uniqueness of the result.
  812. *
  813. * If the result is not unique, a NonUniqueResultException is thrown.
  814. * If there is no result, a NoResultException is thrown.
  815. *
  816. * @param string|int|null $hydrationMode
  817. * @psalm-param string|AbstractQuery::HYDRATE_*|null $hydrationMode
  818. *
  819. * @return mixed
  820. *
  821. * @throws NonUniqueResultException If the query result is not unique.
  822. * @throws NoResultException If the query returned no result.
  823. */
  824. public function getSingleResult($hydrationMode = null)
  825. {
  826. $result = $this->execute(null, $hydrationMode);
  827. if ($this->_hydrationMode !== self::HYDRATE_SINGLE_SCALAR && ! $result) {
  828. throw new NoResultException();
  829. }
  830. if (! is_array($result)) {
  831. return $result;
  832. }
  833. if (count($result) > 1) {
  834. throw new NonUniqueResultException();
  835. }
  836. return array_shift($result);
  837. }
  838. /**
  839. * Gets the single scalar result of the query.
  840. *
  841. * Alias for getSingleResult(HYDRATE_SINGLE_SCALAR).
  842. *
  843. * @return mixed The scalar result.
  844. *
  845. * @throws NoResultException If the query returned no result.
  846. * @throws NonUniqueResultException If the query result is not unique.
  847. */
  848. public function getSingleScalarResult()
  849. {
  850. return $this->getSingleResult(self::HYDRATE_SINGLE_SCALAR);
  851. }
  852. /**
  853. * Sets a query hint. If the hint name is not recognized, it is silently ignored.
  854. *
  855. * @param string $name The name of the hint.
  856. * @param mixed $value The value of the hint.
  857. *
  858. * @return $this
  859. */
  860. public function setHint($name, $value)
  861. {
  862. $this->_hints[$name] = $value;
  863. return $this;
  864. }
  865. /**
  866. * Gets the value of a query hint. If the hint name is not recognized, FALSE is returned.
  867. *
  868. * @param string $name The name of the hint.
  869. *
  870. * @return mixed The value of the hint or FALSE, if the hint name is not recognized.
  871. */
  872. public function getHint($name)
  873. {
  874. return $this->_hints[$name] ?? false;
  875. }
  876. /**
  877. * Check if the query has a hint
  878. *
  879. * @param string $name The name of the hint
  880. *
  881. * @return bool False if the query does not have any hint
  882. */
  883. public function hasHint($name)
  884. {
  885. return isset($this->_hints[$name]);
  886. }
  887. /**
  888. * Return the key value map of query hints that are currently set.
  889. *
  890. * @return array<string,mixed>
  891. */
  892. public function getHints()
  893. {
  894. return $this->_hints;
  895. }
  896. /**
  897. * Executes the query and returns an IterableResult that can be used to incrementally
  898. * iterate over the result.
  899. *
  900. * @deprecated 2.8 Use {@see toIterable} instead. See https://github.com/doctrine/orm/issues/8463
  901. *
  902. * @param ArrayCollection|mixed[]|null $parameters The query parameters.
  903. * @param string|int|null $hydrationMode The hydration mode to use.
  904. * @psalm-param string|AbstractQuery::HYDRATE_*|null $hydrationMode The hydration mode to use.
  905. *
  906. * @return IterableResult
  907. */
  908. public function iterate($parameters = null, $hydrationMode = null)
  909. {
  910. Deprecation::trigger(
  911. 'doctrine/orm',
  912. 'https://github.com/doctrine/orm/issues/8463',
  913. 'Method %s() is deprecated and will be removed in Doctrine ORM 3.0. Use toIterable() instead.',
  914. __METHOD__
  915. );
  916. if ($hydrationMode !== null) {
  917. $this->setHydrationMode($hydrationMode);
  918. }
  919. if (! empty($parameters)) {
  920. $this->setParameters($parameters);
  921. }
  922. $rsm = $this->getResultSetMapping();
  923. if ($rsm === null) {
  924. throw new LogicException('Uninitialized result set mapping.');
  925. }
  926. $stmt = $this->_doExecute();
  927. return $this->_em->newHydrator($this->_hydrationMode)->iterate($stmt, $rsm, $this->_hints);
  928. }
  929. /**
  930. * Executes the query and returns an iterable that can be used to incrementally
  931. * iterate over the result.
  932. *
  933. * @param ArrayCollection|array|mixed[] $parameters The query parameters.
  934. * @param string|int|null $hydrationMode The hydration mode to use.
  935. * @psalm-param ArrayCollection<int, Parameter>|mixed[] $parameters
  936. * @psalm-param string|AbstractQuery::HYDRATE_*|null $hydrationMode
  937. *
  938. * @return iterable<mixed>
  939. */
  940. public function toIterable(iterable $parameters = [], $hydrationMode = null): iterable
  941. {
  942. if ($hydrationMode !== null) {
  943. $this->setHydrationMode($hydrationMode);
  944. }
  945. if (
  946. ($this->isCountable($parameters) && count($parameters) !== 0)
  947. || ($parameters instanceof Traversable && iterator_count($parameters) !== 0)
  948. ) {
  949. $this->setParameters($parameters);
  950. }
  951. $rsm = $this->getResultSetMapping();
  952. if ($rsm === null) {
  953. throw new LogicException('Uninitialized result set mapping.');
  954. }
  955. if ($rsm->isMixed && count($rsm->scalarMappings) > 0) {
  956. throw QueryException::iterateWithMixedResultNotAllowed();
  957. }
  958. $stmt = $this->_doExecute();
  959. return $this->_em->newHydrator($this->_hydrationMode)->toIterable($stmt, $rsm, $this->_hints);
  960. }
  961. /**
  962. * Executes the query.
  963. *
  964. * @param ArrayCollection|mixed[]|null $parameters Query parameters.
  965. * @param string|int|null $hydrationMode Processing mode to be used during the hydration process.
  966. * @psalm-param ArrayCollection<int, Parameter>|mixed[]|null $parameters
  967. * @psalm-param string|AbstractQuery::HYDRATE_*|null $hydrationMode
  968. *
  969. * @return mixed
  970. */
  971. public function execute($parameters = null, $hydrationMode = null)
  972. {
  973. if ($this->cacheable && $this->isCacheEnabled()) {
  974. return $this->executeUsingQueryCache($parameters, $hydrationMode);
  975. }
  976. return $this->executeIgnoreQueryCache($parameters, $hydrationMode);
  977. }
  978. /**
  979. * Execute query ignoring second level cache.
  980. *
  981. * @param ArrayCollection|mixed[]|null $parameters
  982. * @param string|int|null $hydrationMode
  983. * @psalm-param ArrayCollection<int, Parameter>|mixed[]|null $parameters
  984. * @psalm-param string|AbstractQuery::HYDRATE_*|null $hydrationMode
  985. *
  986. * @return mixed
  987. */
  988. private function executeIgnoreQueryCache($parameters = null, $hydrationMode = null)
  989. {
  990. if ($hydrationMode !== null) {
  991. $this->setHydrationMode($hydrationMode);
  992. }
  993. if (! empty($parameters)) {
  994. $this->setParameters($parameters);
  995. }
  996. $setCacheEntry = static function ($data): void {
  997. };
  998. if ($this->_hydrationCacheProfile !== null) {
  999. [$cacheKey, $realCacheKey] = $this->getHydrationCacheId();
  1000. $cache = $this->getHydrationCache();
  1001. $cacheItem = $cache->getItem($cacheKey);
  1002. $result = $cacheItem->isHit() ? $cacheItem->get() : [];
  1003. if (isset($result[$realCacheKey])) {
  1004. return $result[$realCacheKey];
  1005. }
  1006. if (! $result) {
  1007. $result = [];
  1008. }
  1009. $setCacheEntry = static function ($data) use ($cache, $result, $cacheItem, $realCacheKey): void {
  1010. $cache->save($cacheItem->set($result + [$realCacheKey => $data]));
  1011. };
  1012. }
  1013. $stmt = $this->_doExecute();
  1014. if (is_numeric($stmt)) {
  1015. $setCacheEntry($stmt);
  1016. return $stmt;
  1017. }
  1018. $rsm = $this->getResultSetMapping();
  1019. if ($rsm === null) {
  1020. throw new LogicException('Uninitialized result set mapping.');
  1021. }
  1022. $data = $this->_em->newHydrator($this->_hydrationMode)->hydrateAll($stmt, $rsm, $this->_hints);
  1023. $setCacheEntry($data);
  1024. return $data;
  1025. }
  1026. private function getHydrationCache(): CacheItemPoolInterface
  1027. {
  1028. assert($this->_hydrationCacheProfile !== null);
  1029. // Support for DBAL 2
  1030. if (! method_exists($this->_hydrationCacheProfile, 'getResultCache')) {
  1031. $cacheDriver = $this->_hydrationCacheProfile->getResultCacheDriver();
  1032. assert($cacheDriver !== null);
  1033. return CacheAdapter::wrap($cacheDriver);
  1034. }
  1035. $cache = $this->_hydrationCacheProfile->getResultCache();
  1036. assert($cache !== null);
  1037. return $cache;
  1038. }
  1039. /**
  1040. * Load from second level cache or executes the query and put into cache.
  1041. *
  1042. * @param ArrayCollection|mixed[]|null $parameters
  1043. * @param string|int|null $hydrationMode
  1044. * @psalm-param ArrayCollection<int, Parameter>|mixed[]|null $parameters
  1045. * @psalm-param string|AbstractQuery::HYDRATE_*|null $hydrationMode
  1046. *
  1047. * @return mixed
  1048. */
  1049. private function executeUsingQueryCache($parameters = null, $hydrationMode = null)
  1050. {
  1051. $rsm = $this->getResultSetMapping();
  1052. if ($rsm === null) {
  1053. throw new LogicException('Uninitialized result set mapping.');
  1054. }
  1055. $queryCache = $this->_em->getCache()->getQueryCache($this->cacheRegion);
  1056. $queryKey = new QueryCacheKey(
  1057. $this->getHash(),
  1058. $this->lifetime,
  1059. $this->cacheMode ?: Cache::MODE_NORMAL,
  1060. $this->getTimestampKey()
  1061. );
  1062. $result = $queryCache->get($queryKey, $rsm, $this->_hints);
  1063. if ($result !== null) {
  1064. if ($this->cacheLogger) {
  1065. $this->cacheLogger->queryCacheHit($queryCache->getRegion()->getName(), $queryKey);
  1066. }
  1067. return $result;
  1068. }
  1069. $result = $this->executeIgnoreQueryCache($parameters, $hydrationMode);
  1070. $cached = $queryCache->put($queryKey, $rsm, $result, $this->_hints);
  1071. if ($this->cacheLogger) {
  1072. $this->cacheLogger->queryCacheMiss($queryCache->getRegion()->getName(), $queryKey);
  1073. if ($cached) {
  1074. $this->cacheLogger->queryCachePut($queryCache->getRegion()->getName(), $queryKey);
  1075. }
  1076. }
  1077. return $result;
  1078. }
  1079. private function getTimestampKey(): ?TimestampCacheKey
  1080. {
  1081. assert($this->_resultSetMapping !== null);
  1082. $entityName = reset($this->_resultSetMapping->aliasMap);
  1083. if (empty($entityName)) {
  1084. return null;
  1085. }
  1086. $metadata = $this->_em->getClassMetadata($entityName);
  1087. return new Cache\TimestampCacheKey($metadata->rootEntityName);
  1088. }
  1089. /**
  1090. * Get the result cache id to use to store the result set cache entry.
  1091. * Will return the configured id if it exists otherwise a hash will be
  1092. * automatically generated for you.
  1093. *
  1094. * @return string[] ($key, $hash)
  1095. * @psalm-return array{string, string} ($key, $hash)
  1096. */
  1097. protected function getHydrationCacheId()
  1098. {
  1099. $parameters = [];
  1100. foreach ($this->getParameters() as $parameter) {
  1101. $parameters[$parameter->getName()] = $this->processParameterValue($parameter->getValue());
  1102. }
  1103. $sql = $this->getSQL();
  1104. $queryCacheProfile = $this->getHydrationCacheProfile();
  1105. $hints = $this->getHints();
  1106. $hints['hydrationMode'] = $this->getHydrationMode();
  1107. ksort($hints);
  1108. assert($queryCacheProfile !== null);
  1109. return $queryCacheProfile->generateCacheKeys($sql, $parameters, $hints);
  1110. }
  1111. /**
  1112. * Set the result cache id to use to store the result set cache entry.
  1113. * If this is not explicitly set by the developer then a hash is automatically
  1114. * generated for you.
  1115. *
  1116. * @param string|null $id
  1117. *
  1118. * @return $this
  1119. */
  1120. public function setResultCacheId($id)
  1121. {
  1122. if (! $this->_queryCacheProfile) {
  1123. return $this->setResultCacheProfile(new QueryCacheProfile(0, $id));
  1124. }
  1125. $this->_queryCacheProfile = $this->_queryCacheProfile->setCacheKey($id);
  1126. return $this;
  1127. }
  1128. /**
  1129. * Get the result cache id to use to store the result set cache entry if set.
  1130. *
  1131. * @deprecated
  1132. *
  1133. * @return string|null
  1134. */
  1135. public function getResultCacheId()
  1136. {
  1137. return $this->_queryCacheProfile ? $this->_queryCacheProfile->getCacheKey() : null;
  1138. }
  1139. /**
  1140. * Executes the query and returns a the resulting Statement object.
  1141. *
  1142. * @return Result|int The executed database statement that holds
  1143. * the results, or an integer indicating how
  1144. * many rows were affected.
  1145. */
  1146. abstract protected function _doExecute();
  1147. /**
  1148. * Cleanup Query resource when clone is called.
  1149. *
  1150. * @return void
  1151. */
  1152. public function __clone()
  1153. {
  1154. $this->parameters = new ArrayCollection();
  1155. $this->_hints = [];
  1156. $this->_hints = $this->_em->getConfiguration()->getDefaultQueryHints();
  1157. }
  1158. /**
  1159. * Generates a string of currently query to use for the cache second level cache.
  1160. *
  1161. * @return string
  1162. */
  1163. protected function getHash()
  1164. {
  1165. $query = $this->getSQL();
  1166. $hints = $this->getHints();
  1167. $params = array_map(function (Parameter $parameter) {
  1168. $value = $parameter->getValue();
  1169. // Small optimization
  1170. // Does not invoke processParameterValue for scalar value
  1171. if (is_scalar($value)) {
  1172. return $value;
  1173. }
  1174. return $this->processParameterValue($value);
  1175. }, $this->parameters->getValues());
  1176. ksort($hints);
  1177. return sha1($query . '-' . serialize($params) . '-' . serialize($hints));
  1178. }
  1179. /** @param iterable<mixed> $subject */
  1180. private function isCountable(iterable $subject): bool
  1181. {
  1182. return $subject instanceof Countable || is_array($subject);
  1183. }
  1184. }