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