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

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