vendor/api-platform/core/src/Core/Bridge/Doctrine/Orm/Extension/PaginationExtension.php line 119

Open in your IDE?
  1. <?php
  2. /*
  3.  * This file is part of the API Platform project.
  4.  *
  5.  * (c) Kévin Dunglas <dunglas@gmail.com>
  6.  *
  7.  * For the full copyright and license information, please view the LICENSE
  8.  * file that was distributed with this source code.
  9.  */
  10. declare(strict_types=1);
  11. namespace ApiPlatform\Core\Bridge\Doctrine\Orm\Extension;
  12. use ApiPlatform\Core\Bridge\Doctrine\Orm\AbstractPaginator;
  13. use ApiPlatform\Core\Bridge\Doctrine\Orm\Paginator;
  14. use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryChecker;
  15. use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
  16. use ApiPlatform\Core\DataProvider\Pagination;
  17. use ApiPlatform\Core\Exception\InvalidArgumentException;
  18. use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
  19. use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
  20. use Doctrine\ORM\QueryBuilder;
  21. use Doctrine\ORM\Tools\Pagination\CountWalker;
  22. use Doctrine\ORM\Tools\Pagination\Paginator as DoctrineOrmPaginator;
  23. use Doctrine\Persistence\ManagerRegistry;
  24. use Symfony\Component\HttpFoundation\Request;
  25. use Symfony\Component\HttpFoundation\RequestStack;
  26. // Help opcache.preload discover always-needed symbols
  27. class_exists(AbstractPaginator::class);
  28. /**
  29.  * Applies pagination on the Doctrine query for resource collection when enabled.
  30.  *
  31.  * @author Kévin Dunglas <dunglas@gmail.com>
  32.  * @author Samuel ROZE <samuel.roze@gmail.com>
  33.  */
  34. final class PaginationExtension implements ContextAwareQueryResultCollectionExtensionInterface
  35. {
  36.     private $managerRegistry;
  37.     private $requestStack;
  38.     /**
  39.      * @var ResourceMetadataFactoryInterface
  40.      */
  41.     private $resourceMetadataFactory;
  42.     private $enabled;
  43.     private $clientEnabled;
  44.     private $clientItemsPerPage;
  45.     private $itemsPerPage;
  46.     private $pageParameterName;
  47.     private $enabledParameterName;
  48.     private $itemsPerPageParameterName;
  49.     private $maximumItemPerPage;
  50.     private $partial;
  51.     private $clientPartial;
  52.     private $partialParameterName;
  53.     /**
  54.      * @var Pagination|null
  55.      */
  56.     private $pagination;
  57.     /**
  58.      * @param ResourceMetadataFactoryInterface|RequestStack $resourceMetadataFactory
  59.      * @param Pagination|ResourceMetadataFactoryInterface   $pagination
  60.      */
  61.     public function __construct(ManagerRegistry $managerRegistry/* ResourceMetadataFactoryInterface */ $resourceMetadataFactory/* Pagination */ $pagination)
  62.     {
  63.         if ($resourceMetadataFactory instanceof RequestStack && $pagination instanceof ResourceMetadataFactoryInterface) {
  64.             @trigger_error(sprintf('Passing an instance of "%s" as second argument of "%s" is deprecated since API Platform 2.4 and will not be possible anymore in API Platform 3. Pass an instance of "%s" instead.'RequestStack::class, self::class, ResourceMetadataFactoryInterface::class), \E_USER_DEPRECATED);
  65.             @trigger_error(sprintf('Passing an instance of "%s" as third argument of "%s" is deprecated since API Platform 2.4 and will not be possible anymore in API Platform 3. Pass an instance of "%s" instead.'ResourceMetadataFactoryInterface::class, self::class, Pagination::class), \E_USER_DEPRECATED);
  66.             $this->requestStack $resourceMetadataFactory;
  67.             $resourceMetadataFactory $pagination;
  68.             $pagination null;
  69.             $args = \array_slice(\func_get_args(), 3);
  70.             $legacyPaginationArgs = [
  71.                 ['arg_name' => 'enabled''type' => 'bool''default' => true],
  72.                 ['arg_name' => 'clientEnabled''type' => 'bool''default' => false],
  73.                 ['arg_name' => 'clientItemsPerPage''type' => 'bool''default' => false],
  74.                 ['arg_name' => 'itemsPerPage''type' => 'int''default' => 30],
  75.                 ['arg_name' => 'pageParameterName''type' => 'string''default' => 'page'],
  76.                 ['arg_name' => 'enabledParameterName''type' => 'string''default' => 'pagination'],
  77.                 ['arg_name' => 'itemsPerPageParameterName''type' => 'string''default' => 'itemsPerPage'],
  78.                 ['arg_name' => 'maximumItemPerPage''type' => 'int''default' => null],
  79.                 ['arg_name' => 'partial''type' => 'bool''default' => false],
  80.                 ['arg_name' => 'clientPartial''type' => 'bool''default' => false],
  81.                 ['arg_name' => 'partialParameterName''type' => 'string''default' => 'partial'],
  82.             ];
  83.             foreach ($legacyPaginationArgs as $pos => $arg) {
  84.                 if (\array_key_exists($pos$args)) {
  85.                     @trigger_error(sprintf('Passing "$%s" arguments is deprecated since API Platform 2.4 and will not be possible anymore in API Platform 3. Pass an instance of "%s" as third argument instead.'implode('", "$'array_column($legacyPaginationArgs'arg_name')), Paginator::class), \E_USER_DEPRECATED);
  86.                     if (!((null === $arg['default'] && null === $args[$pos]) || \call_user_func("is_{$arg['type']}"$args[$pos]))) {
  87.                         throw new InvalidArgumentException(sprintf('The "$%s" argument is expected to be a %s%s.'$arg['arg_name'], $arg['type'], null === $arg['default'] ? ' or null' ''));
  88.                     }
  89.                     $value $args[$pos];
  90.                 } else {
  91.                     $value $arg['default'];
  92.                 }
  93.                 $this->{$arg['arg_name']} = $value;
  94.             }
  95.         } elseif (!$resourceMetadataFactory instanceof ResourceMetadataFactoryInterface) {
  96.             throw new InvalidArgumentException(sprintf('The "$resourceMetadataFactory" argument is expected to be an implementation of the "%s" interface.'ResourceMetadataFactoryInterface::class));
  97.         } elseif (!$pagination instanceof Pagination) {
  98.             throw new InvalidArgumentException(sprintf('The "$pagination" argument is expected to be an instance of the "%s" class.'Pagination::class));
  99.         }
  100.         $this->managerRegistry $managerRegistry;
  101.         $this->resourceMetadataFactory $resourceMetadataFactory;
  102.         $this->pagination $pagination;
  103.     }
  104.     public function applyToCollection(QueryBuilder $queryBuilderQueryNameGeneratorInterface $queryNameGeneratorstring $resourceClassstring $operationName null, array $context = [])
  105.     {
  106.         if (null === $pagination $this->getPagination($queryBuilder$resourceClass$operationName$context)) {
  107.             return;
  108.         }
  109.         [$offset$limit] = $pagination;
  110.         $queryBuilder
  111.             ->setFirstResult($offset)
  112.             ->setMaxResults($limit);
  113.     }
  114.     public function supportsResult(string $resourceClassstring $operationName null, array $context = []): bool
  115.     {
  116.         if ($context['graphql_operation_name'] ?? false) {
  117.             return $this->pagination->isGraphQlEnabled($resourceClass$operationName$context);
  118.         }
  119.         if (null === $this->requestStack) {
  120.             return $this->pagination->isEnabled($resourceClass$operationName$context);
  121.         }
  122.         if (null === $request $this->requestStack->getCurrentRequest()) {
  123.             return false;
  124.         }
  125.         return $this->isPaginationEnabled($request$this->resourceMetadataFactory->create($resourceClass), $operationName);
  126.     }
  127.     public function getResult(QueryBuilder $queryBuilderstring $resourceClass nullstring $operationName null, array $context = []): iterable
  128.     {
  129.         $query $queryBuilder->getQuery();
  130.         // Only one alias, without joins, disable the DISTINCT on the COUNT
  131.         if (=== \count($queryBuilder->getAllAliases())) {
  132.             $query->setHint(CountWalker::HINT_DISTINCTfalse);
  133.         }
  134.         $doctrineOrmPaginator = new DoctrineOrmPaginator($query$this->shouldDoctrinePaginatorFetchJoinCollection($queryBuilder$resourceClass$operationName$context));
  135.         $doctrineOrmPaginator->setUseOutputWalkers($this->shouldDoctrinePaginatorUseOutputWalkers($queryBuilder$resourceClass$operationName$context));
  136.         if (null === $this->requestStack) {
  137.             $isPartialEnabled $this->pagination->isPartialEnabled($resourceClass$operationName$context);
  138.         } else {
  139.             $isPartialEnabled $this->isPartialPaginationEnabled(
  140.                 $this->requestStack->getCurrentRequest(),
  141.                 null === $resourceClass null $this->resourceMetadataFactory->create($resourceClass),
  142.                 $operationName
  143.             );
  144.         }
  145.         if ($isPartialEnabled) {
  146.             return new class($doctrineOrmPaginator) extends AbstractPaginator {
  147.             };
  148.         }
  149.         return new Paginator($doctrineOrmPaginator);
  150.     }
  151.     /**
  152.      * @throws InvalidArgumentException
  153.      */
  154.     private function getPagination(QueryBuilder $queryBuilderstring $resourceClass, ?string $operationName, array $context): ?array
  155.     {
  156.         $request null;
  157.         if (null !== $this->requestStack && null === $request $this->requestStack->getCurrentRequest()) {
  158.             return null;
  159.         }
  160.         if (null === $request) {
  161.             if (!$this->pagination->isEnabled($resourceClass$operationName$context)) {
  162.                 return null;
  163.             }
  164.             if (($context['graphql_operation_name'] ?? false) && !$this->pagination->isGraphQlEnabled($resourceClass$operationName$context)) {
  165.                 return null;
  166.             }
  167.             $context $this->addCountToContext($queryBuilder$context);
  168.             return \array_slice($this->pagination->getPagination($resourceClass$operationName$context), 1);
  169.         }
  170.         $resourceMetadata $this->resourceMetadataFactory->create($resourceClass);
  171.         if (!$this->isPaginationEnabled($request$resourceMetadata$operationName)) {
  172.             return null;
  173.         }
  174.         $itemsPerPage $resourceMetadata->getCollectionOperationAttribute($operationName'pagination_items_per_page'$this->itemsPerPagetrue);
  175.         if ($request->attributes->getBoolean('_graphql'false)) {
  176.             $collectionArgs $request->attributes->get('_graphql_collections_args', []);
  177.             $itemsPerPage $collectionArgs[$resourceClass]['first'] ?? $itemsPerPage;
  178.         }
  179.         if ($resourceMetadata->getCollectionOperationAttribute($operationName'pagination_client_items_per_page'$this->clientItemsPerPagetrue)) {
  180.             $maxItemsPerPage $resourceMetadata->getCollectionOperationAttribute($operationName'maximum_items_per_page'nulltrue);
  181.             if (null !== $maxItemsPerPage) {
  182.                 @trigger_error('The "maximum_items_per_page" option has been deprecated since API Platform 2.5 in favor of "pagination_maximum_items_per_page" and will be removed in API Platform 3.', \E_USER_DEPRECATED);
  183.             }
  184.             $maxItemsPerPage $resourceMetadata->getCollectionOperationAttribute($operationName'pagination_maximum_items_per_page'$maxItemsPerPage ?? $this->maximumItemPerPagetrue);
  185.             $itemsPerPage = (int) $this->getPaginationParameter($request$this->itemsPerPageParameterName$itemsPerPage);
  186.             $itemsPerPage = (null !== $maxItemsPerPage && $itemsPerPage >= $maxItemsPerPage $maxItemsPerPage $itemsPerPage);
  187.         }
  188.         if ($itemsPerPage) {
  189.             throw new InvalidArgumentException('Item per page parameter should not be less than 0');
  190.         }
  191.         $page = (int) $this->getPaginationParameter($request$this->pageParameterName1);
  192.         if ($page) {
  193.             throw new InvalidArgumentException('Page should not be less than 1');
  194.         }
  195.         if (=== $itemsPerPage && $page) {
  196.             throw new InvalidArgumentException('Page should not be greater than 1 if itemsPerPage is equal to 0');
  197.         }
  198.         $firstResult = ($page 1) * $itemsPerPage;
  199.         if ($request->attributes->getBoolean('_graphql'false)) {
  200.             $collectionArgs $request->attributes->get('_graphql_collections_args', []);
  201.             if (isset($collectionArgs[$resourceClass]['after'])) {
  202.                 $after base64_decode($collectionArgs[$resourceClass]['after'], true);
  203.                 $firstResult = (int) $after;
  204.                 $firstResult false === $after $firstResult : ++$firstResult;
  205.             }
  206.         }
  207.         return [$firstResult$itemsPerPage];
  208.     }
  209.     private function isPartialPaginationEnabled(Request $request nullResourceMetadata $resourceMetadata nullstring $operationName null): bool
  210.     {
  211.         $enabled $this->partial;
  212.         $clientEnabled $this->clientPartial;
  213.         if ($resourceMetadata) {
  214.             $enabled $resourceMetadata->getCollectionOperationAttribute($operationName'pagination_partial'$enabledtrue);
  215.             if ($request) {
  216.                 $clientEnabled $resourceMetadata->getCollectionOperationAttribute($operationName'pagination_client_partial'$clientEnabledtrue);
  217.             }
  218.         }
  219.         if ($clientEnabled && $request) {
  220.             $enabled filter_var($this->getPaginationParameter($request$this->partialParameterName$enabled), \FILTER_VALIDATE_BOOLEAN);
  221.         }
  222.         return $enabled;
  223.     }
  224.     private function isPaginationEnabled(Request $requestResourceMetadata $resourceMetadatastring $operationName null): bool
  225.     {
  226.         $enabled $resourceMetadata->getCollectionOperationAttribute($operationName'pagination_enabled'$this->enabledtrue);
  227.         $clientEnabled $resourceMetadata->getCollectionOperationAttribute($operationName'pagination_client_enabled'$this->clientEnabledtrue);
  228.         if ($clientEnabled) {
  229.             $enabled filter_var($this->getPaginationParameter($request$this->enabledParameterName$enabled), \FILTER_VALIDATE_BOOLEAN);
  230.         }
  231.         return $enabled;
  232.     }
  233.     private function getPaginationParameter(Request $requeststring $parameterName$default null)
  234.     {
  235.         if (null !== $paginationAttribute $request->attributes->get('_api_pagination')) {
  236.             return \array_key_exists($parameterName$paginationAttribute) ? $paginationAttribute[$parameterName] : $default;
  237.         }
  238.         return $request->query->all()[$parameterName] ?? $default;
  239.     }
  240.     private function addCountToContext(QueryBuilder $queryBuilder, array $context): array
  241.     {
  242.         if (!($context['graphql_operation_name'] ?? false)) {
  243.             return $context;
  244.         }
  245.         if (isset($context['filters']['last']) && !isset($context['filters']['before'])) {
  246.             $context['count'] = (new DoctrineOrmPaginator($queryBuilder))->count();
  247.         }
  248.         return $context;
  249.     }
  250.     /**
  251.      * Determines the value of the $fetchJoinCollection argument passed to the Doctrine ORM Paginator.
  252.      */
  253.     private function shouldDoctrinePaginatorFetchJoinCollection(QueryBuilder $queryBuilderstring $resourceClass nullstring $operationName null, array $context = []): bool
  254.     {
  255.         if (null !== $resourceClass) {
  256.             $resourceMetadata $this->resourceMetadataFactory->create($resourceClass);
  257.             if (isset($context['collection_operation_name']) && null !== $fetchJoinCollection $resourceMetadata->getCollectionOperationAttribute($operationName'pagination_fetch_join_collection'nulltrue)) {
  258.                 return $fetchJoinCollection;
  259.             }
  260.             if (isset($context['graphql_operation_name']) && null !== $fetchJoinCollection $resourceMetadata->getGraphqlAttribute($operationName'pagination_fetch_join_collection'nulltrue)) {
  261.                 return $fetchJoinCollection;
  262.             }
  263.         }
  264.         /*
  265.          * "Cannot count query which selects two FROM components, cannot make distinction"
  266.          *
  267.          * @see https://github.com/doctrine/orm/blob/v2.6.3/lib/Doctrine/ORM/Tools/Pagination/WhereInWalker.php#L81
  268.          * @see https://github.com/doctrine/doctrine2/issues/2910
  269.          */
  270.         if (QueryChecker::hasRootEntityWithCompositeIdentifier($queryBuilder$this->managerRegistry)) {
  271.             return false;
  272.         }
  273.         if (QueryChecker::hasJoinedToManyAssociation($queryBuilder$this->managerRegistry)) {
  274.             return true;
  275.         }
  276.         // disable $fetchJoinCollection by default (performance)
  277.         return false;
  278.     }
  279.     /**
  280.      * Determines whether the Doctrine ORM Paginator should use output walkers.
  281.      */
  282.     private function shouldDoctrinePaginatorUseOutputWalkers(QueryBuilder $queryBuilderstring $resourceClass nullstring $operationName null, array $context = []): bool
  283.     {
  284.         if (null !== $resourceClass) {
  285.             $resourceMetadata $this->resourceMetadataFactory->create($resourceClass);
  286.             if (isset($context['collection_operation_name']) && null !== $useOutputWalkers $resourceMetadata->getCollectionOperationAttribute($operationName'pagination_use_output_walkers'nulltrue)) {
  287.                 return $useOutputWalkers;
  288.             }
  289.             if (isset($context['graphql_operation_name']) && null !== $useOutputWalkers $resourceMetadata->getGraphqlAttribute($operationName'pagination_use_output_walkers'nulltrue)) {
  290.                 return $useOutputWalkers;
  291.             }
  292.         }
  293.         /*
  294.          * "Cannot count query that uses a HAVING clause. Use the output walkers for pagination"
  295.          *
  296.          * @see https://github.com/doctrine/orm/blob/v2.6.3/lib/Doctrine/ORM/Tools/Pagination/CountWalker.php#L56
  297.          */
  298.         if (QueryChecker::hasHavingClause($queryBuilder)) {
  299.             return true;
  300.         }
  301.         /*
  302.          * "Cannot count query which selects two FROM components, cannot make distinction"
  303.          *
  304.          * @see https://github.com/doctrine/orm/blob/v2.6.3/lib/Doctrine/ORM/Tools/Pagination/CountWalker.php#L64
  305.          */
  306.         if (QueryChecker::hasRootEntityWithCompositeIdentifier($queryBuilder$this->managerRegistry)) {
  307.             return true;
  308.         }
  309.         /*
  310.          * "Paginating an entity with foreign key as identifier only works when using the Output Walkers. Call Paginator#setUseOutputWalkers(true) before iterating the paginator."
  311.          *
  312.          * @see https://github.com/doctrine/orm/blob/v2.6.3/lib/Doctrine/ORM/Tools/Pagination/LimitSubqueryWalker.php#L77
  313.          */
  314.         if (QueryChecker::hasRootEntityWithForeignKeyIdentifier($queryBuilder$this->managerRegistry)) {
  315.             return true;
  316.         }
  317.         /*
  318.          * "Cannot select distinct identifiers from query with LIMIT and ORDER BY on a column from a fetch joined to-many association. Use output walkers."
  319.          *
  320.          * @see https://github.com/doctrine/orm/blob/v2.6.3/lib/Doctrine/ORM/Tools/Pagination/LimitSubqueryWalker.php#L150
  321.          */
  322.         if (QueryChecker::hasMaxResults($queryBuilder) && QueryChecker::hasOrderByOnFetchJoinedToManyAssociation($queryBuilder$this->managerRegistry)) {
  323.             return true;
  324.         }
  325.         // Disable output walkers by default (performance)
  326.         return false;
  327.     }
  328. }