vendor/doctrine/orm/src/AbstractQuery.php line 1213

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