vendor/doctrine/orm/lib/Doctrine/ORM/Tools/Pagination/Paginator.php line 171

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