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

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