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

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