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 Countable, IteratorAggregate
  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.      * @phpstan-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.      * @phpstan-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_WALKER, LimitSubqueryOutputWalker::class);
  124.             } else {
  125.                 $this->appendTreeWalker($subQuery, LimitSubqueryWalker::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($whereInQuery, WhereInWalker::class);
  137.             $whereInQuery->setHint(WhereInWalker::HINT_PAGINATOR_HAS_IDS, true);
  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.      * @param class-string $walkerClass
  176.      */
  177.     private function appendTreeWalker(Query $query, string $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.         /*
  192.             As opposed to using self::cloneQuery, the following code does not transfer
  193.             a potentially existing result set mapping (either set directly by the user,
  194.             or taken from the parser result from a previous invocation of Query::parse())
  195.             to the new query object. This is fine, since we are going to completely change the
  196.             select clause, so a previously existing result set mapping (RSM) is probably wrong anyway.
  197.             In the case of using output walkers, we are even creating a new RSM down below.
  198.             In the case of using a tree walker, we want to have a new RSM created by the parser.
  199.         */
  200.         $countQuery = new Query($this->query->getEntityManager());
  201.         $countQuery->setDQL($this->query->getDQL());
  202.         $countQuery->setParameters(clone $this->query->getParameters());
  203.         $countQuery->setCacheable(false);
  204.         foreach ($this->query->getHints() as $name => $value) {
  205.             $countQuery->setHint($name, $value);
  206.         }
  207.         if (! $countQuery->hasHint(CountWalker::HINT_DISTINCT)) {
  208.             $countQuery->setHint(CountWalker::HINT_DISTINCT, true);
  209.         }
  210.         if ($this->useOutputWalker($countQuery)) {
  211.             $platform = $countQuery->getEntityManager()->getConnection()->getDatabasePlatform(); // law of demeter win
  212.             $rsm = new ResultSetMapping();
  213.             $rsm->addScalarResult($this->getSQLResultCasing($platform, 'dctrn_count'), 'count');
  214.             $countQuery->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, CountOutputWalker::class);
  215.             $countQuery->setResultSetMapping($rsm);
  216.         } else {
  217.             $this->appendTreeWalker($countQuery, CountWalker::class);
  218.             $this->unbindUnusedQueryParams($countQuery);
  219.         }
  220.         $countQuery->setFirstResult(0)->setMaxResults(null);
  221.         return $countQuery;
  222.     }
  223.     private function unbindUnusedQueryParams(Query $query): void
  224.     {
  225.         $parser            = new Parser($query);
  226.         $parameterMappings = $parser->parse()->getParameterMappings();
  227.         /** @var Collection|Parameter[] $parameters */
  228.         $parameters = $query->getParameters();
  229.         foreach ($parameters as $key => $parameter) {
  230.             $parameterName = $parameter->getName();
  231.             if (! (isset($parameterMappings[$parameterName]) || array_key_exists($parameterName, $parameterMappings))) {
  232.                 unset($parameters[$key]);
  233.             }
  234.         }
  235.         $query->setParameters($parameters);
  236.     }
  237.     /**
  238.      * @param mixed[] $identifiers
  239.      *
  240.      * @return mixed[]
  241.      */
  242.     private function convertWhereInIdentifiersToDatabaseValues(array $identifiers): array
  243.     {
  244.         $query = $this->cloneQuery($this->query);
  245.         $query->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, RootTypeWalker::class);
  246.         $connection = $this->query->getEntityManager()->getConnection();
  247.         $type       = $query->getSQL();
  248.         assert(is_string($type));
  249.         return array_map(static function ($id) use ($connection, $type) {
  250.             return $connection->convertToDatabaseValue($id, $type);
  251.         }, $identifiers);
  252.     }
  253. }