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

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