vendor/doctrine/orm/src/Tools/Pagination/Paginator.php line 96

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. namespace Doctrine\ORM\Tools\Pagination;
  4. use ArrayIterator;
  5. use Countable;
  6. use Doctrine\Common\Collections\Collection;
  7. use Doctrine\ORM\Internal\SQLResultCasing;
  8. use Doctrine\ORM\NoResultException;
  9. use Doctrine\ORM\Query;
  10. use Doctrine\ORM\Query\Parameter;
  11. use Doctrine\ORM\Query\Parser;
  12. use Doctrine\ORM\Query\ResultSetMapping;
  13. use Doctrine\ORM\QueryBuilder;
  14. use IteratorAggregate;
  15. use Traversable;
  16. use function array_key_exists;
  17. use function array_map;
  18. use function array_sum;
  19. use function assert;
  20. use function is_string;
  21. /**
  22.  * The paginator can handle various complex scenarios with DQL.
  23.  *
  24.  * @template-covariant T
  25.  * @implements IteratorAggregate<array-key, T>
  26.  */
  27. class Paginator implements CountableIteratorAggregate
  28. {
  29.     use SQLResultCasing;
  30.     public const HINT_ENABLE_DISTINCT 'paginator.distinct.enable';
  31.     private readonly Query $query;
  32.     private bool|null $useOutputWalkers null;
  33.     private int|null $count             null;
  34.     /** @param bool $fetchJoinCollection Whether the query joins a collection (true by default). */
  35.     public function __construct(
  36.         Query|QueryBuilder $query,
  37.         private readonly bool $fetchJoinCollection true,
  38.     ) {
  39.         if ($query instanceof QueryBuilder) {
  40.             $query $query->getQuery();
  41.         }
  42.         $this->query $query;
  43.     }
  44.     /**
  45.      * Returns the query.
  46.      */
  47.     public function getQuery(): Query
  48.     {
  49.         return $this->query;
  50.     }
  51.     /**
  52.      * Returns whether the query joins a collection.
  53.      *
  54.      * @return bool Whether the query joins a collection.
  55.      */
  56.     public function getFetchJoinCollection(): bool
  57.     {
  58.         return $this->fetchJoinCollection;
  59.     }
  60.     /**
  61.      * Returns whether the paginator will use an output walker.
  62.      */
  63.     public function getUseOutputWalkers(): bool|null
  64.     {
  65.         return $this->useOutputWalkers;
  66.     }
  67.     /**
  68.      * Sets whether the paginator will use an output walker.
  69.      *
  70.      * @return $this
  71.      */
  72.     public function setUseOutputWalkers(bool|null $useOutputWalkers): static
  73.     {
  74.         $this->useOutputWalkers $useOutputWalkers;
  75.         return $this;
  76.     }
  77.     public function count(): int
  78.     {
  79.         if ($this->count === null) {
  80.             try {
  81.                 $this->count = (int) array_sum(array_map('current'$this->getCountQuery()->getScalarResult()));
  82.             } catch (NoResultException) {
  83.                 $this->count 0;
  84.             }
  85.         }
  86.         return $this->count;
  87.     }
  88.     /**
  89.      * {@inheritDoc}
  90.      *
  91.      * @psalm-return Traversable<array-key, T>
  92.      */
  93.     public function getIterator(): Traversable
  94.     {
  95.         $offset $this->query->getFirstResult();
  96.         $length $this->query->getMaxResults();
  97.         if ($this->fetchJoinCollection && $length !== null) {
  98.             $subQuery $this->cloneQuery($this->query);
  99.             if ($this->useOutputWalker($subQuery)) {
  100.                 $subQuery->setHint(Query::HINT_CUSTOM_OUTPUT_WALKERLimitSubqueryOutputWalker::class);
  101.             } else {
  102.                 $this->appendTreeWalker($subQueryLimitSubqueryWalker::class);
  103.                 $this->unbindUnusedQueryParams($subQuery);
  104.             }
  105.             $subQuery->setFirstResult($offset)->setMaxResults($length);
  106.             $foundIdRows $subQuery->getScalarResult();
  107.             // don't do this for an empty id array
  108.             if ($foundIdRows === []) {
  109.                 return new ArrayIterator([]);
  110.             }
  111.             $whereInQuery $this->cloneQuery($this->query);
  112.             $ids          array_map('current'$foundIdRows);
  113.             $this->appendTreeWalker($whereInQueryWhereInWalker::class);
  114.             $whereInQuery->setHint(WhereInWalker::HINT_PAGINATOR_HAS_IDStrue);
  115.             $whereInQuery->setFirstResult(0)->setMaxResults(null);
  116.             $whereInQuery->setCacheable($this->query->isCacheable());
  117.             $databaseIds $this->convertWhereInIdentifiersToDatabaseValues($ids);
  118.             $whereInQuery->setParameter(WhereInWalker::PAGINATOR_ID_ALIAS$databaseIds);
  119.             $result $whereInQuery->getResult($this->query->getHydrationMode());
  120.         } else {
  121.             $result $this->cloneQuery($this->query)
  122.                 ->setMaxResults($length)
  123.                 ->setFirstResult($offset)
  124.                 ->setCacheable($this->query->isCacheable())
  125.                 ->getResult($this->query->getHydrationMode());
  126.         }
  127.         return new ArrayIterator($result);
  128.     }
  129.     private function cloneQuery(Query $query): Query
  130.     {
  131.         $cloneQuery = clone $query;
  132.         $cloneQuery->setParameters(clone $query->getParameters());
  133.         $cloneQuery->setCacheable(false);
  134.         foreach ($query->getHints() as $name => $value) {
  135.             $cloneQuery->setHint($name$value);
  136.         }
  137.         return $cloneQuery;
  138.     }
  139.     /**
  140.      * Determines whether to use an output walker for the query.
  141.      */
  142.     private function useOutputWalker(Query $query): bool
  143.     {
  144.         if ($this->useOutputWalkers === null) {
  145.             return (bool) $query->getHint(Query::HINT_CUSTOM_OUTPUT_WALKER) === false;
  146.         }
  147.         return $this->useOutputWalkers;
  148.     }
  149.     /**
  150.      * Appends a custom tree walker to the tree walkers hint.
  151.      *
  152.      * @psalm-param class-string $walkerClass
  153.      */
  154.     private function appendTreeWalker(Query $querystring $walkerClass): void
  155.     {
  156.         $hints $query->getHint(Query::HINT_CUSTOM_TREE_WALKERS);
  157.         if ($hints === false) {
  158.             $hints = [];
  159.         }
  160.         $hints[] = $walkerClass;
  161.         $query->setHint(Query::HINT_CUSTOM_TREE_WALKERS$hints);
  162.     }
  163.     /**
  164.      * Returns Query prepared to count.
  165.      */
  166.     private function getCountQuery(): Query
  167.     {
  168.         $countQuery $this->cloneQuery($this->query);
  169.         if (! $countQuery->hasHint(CountWalker::HINT_DISTINCT)) {
  170.             $countQuery->setHint(CountWalker::HINT_DISTINCTtrue);
  171.         }
  172.         if ($this->useOutputWalker($countQuery)) {
  173.             $platform $countQuery->getEntityManager()->getConnection()->getDatabasePlatform(); // law of demeter win
  174.             $rsm = new ResultSetMapping();
  175.             $rsm->addScalarResult($this->getSQLResultCasing($platform'dctrn_count'), 'count');
  176.             $countQuery->setHint(Query::HINT_CUSTOM_OUTPUT_WALKERCountOutputWalker::class);
  177.             $countQuery->setResultSetMapping($rsm);
  178.         } else {
  179.             $this->appendTreeWalker($countQueryCountWalker::class);
  180.             $this->unbindUnusedQueryParams($countQuery);
  181.         }
  182.         $countQuery->setFirstResult(0)->setMaxResults(null);
  183.         return $countQuery;
  184.     }
  185.     private function unbindUnusedQueryParams(Query $query): void
  186.     {
  187.         $parser            = new Parser($query);
  188.         $parameterMappings $parser->parse()->getParameterMappings();
  189.         /** @var Collection|Parameter[] $parameters */
  190.         $parameters $query->getParameters();
  191.         foreach ($parameters as $key => $parameter) {
  192.             $parameterName $parameter->getName();
  193.             if (! (isset($parameterMappings[$parameterName]) || array_key_exists($parameterName$parameterMappings))) {
  194.                 unset($parameters[$key]);
  195.             }
  196.         }
  197.         $query->setParameters($parameters);
  198.     }
  199.     /**
  200.      * @param mixed[] $identifiers
  201.      *
  202.      * @return mixed[]
  203.      */
  204.     private function convertWhereInIdentifiersToDatabaseValues(array $identifiers): array
  205.     {
  206.         $query $this->cloneQuery($this->query);
  207.         $query->setHint(Query::HINT_CUSTOM_OUTPUT_WALKERRootTypeWalker::class);
  208.         $connection $this->query->getEntityManager()->getConnection();
  209.         $type       $query->getSQL();
  210.         assert(is_string($type));
  211.         return array_map(static fn ($id): mixed => $connection->convertToDatabaseValue($id$type), $identifiers);
  212.     }
  213. }