core icon indicating copy to clipboard operation
core copied to clipboard

Support offset/slice in pagination

Open tacman opened this issue 3 years ago • 1 comments

Description

I would like to use API Platform to get a slice of the results, not a page.

Example

/api/...?offset=422&items_per_page=81

A javascript library I'm using for infinite scrolling fetches more than a "page". Currently within API Platform, the offset is calculated from the page and returned, I'd like to override what's returned.

        $firstResult = ($page - 1) * $itemsPerPage;
// ...
       return [$firstResult, $itemsPerPage];

I considered extending the Doctrine ORM paginator class, but it's marked as final.

There's probably an elegant solution here somewhere, but I can't find it, so maybe it can be added. What I want is identical to the Doctrine ORM provider with pagination, so ideally I'd like to leverage the existing code and simply change the first result offset.

tacman avatar Mar 08 '22 15:03 tacman

@dunglas , what do you think of a PR that adds "offset" (or "starting_at") to the arguments, then

Currently the pagination extension that returns a slice of the results given page_number and records_per_page, e.g. page 3, 50 records, getPagination() return [100, 50]

// vendor/api-platform/core/src/Bridge/Doctrine/Orm/Extension/PaginationExtension.php:121

public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null, array $context = [])
    {
        if (null === $pagination = $this->getPagination($queryBuilder, $resourceClass, $operationName, $context)) {
            return;
        }

        [$offset, $limit] = $pagination;
        $queryBuilder
            ->setFirstResult($offset)
            ->setMaxResults($limit);
    }

What I want is just a slice, not really a paginator. I'm not sure if the best approach is to override the paginator, or create a custom data collector, but what about building it the paginator?

                ['arg_name' => 'offset', 'type' => 'int', 'default' => 0],
                ['arg_name' => 'offsetParameterName', 'type' => 'string', 'default' => 'offset'],
                ['arg_name' => 'enabledOffset', 'type' => 'boolean', 'default' => false],

Then if offset were enabled, the paginator would return the values passed in and not need to calculate the offset / limit as it does now given the page number.

    public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null, array $context = [])
    {
       // if offset it enabled, use it, otherwise call the paginator..
        [$offset, $limit] = [41, 220];
        $queryBuilder
            ->setFirstResult($offset)
            ->setMaxResults($limit);
    }

I've asked this on stackoverflow, too.

https://stackoverflow.com/questions/72583686/override-doctrine-pagination-to-perform-slice-in-api-platform-paginationextensio

tacman avatar Jun 11 '22 11:06 tacman

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

stale[bot] avatar Nov 04 '22 21:11 stale[bot]

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

stale[bot] avatar Jan 04 '23 01:01 stale[bot]

@tacman plz I need the same aspect, the param was added or not ?

maySaghira avatar Mar 17 '23 16:03 maySaghira

No, and I don't know how to override it. @dunglas , would you accept a PR if this were added? Or provide direction in how to implement it with a custom paginator?

tacman avatar Mar 21 '23 13:03 tacman

Hello! I've implement a workaround for that: 1- disable pagination. 2- use an extension on QueryCollectionExtensionInterface where get the param from request.

<?php

namespace App\Extension;

use ApiPlatform\Doctrine\Orm\Extension\QueryCollectionExtensionInterface;
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use ApiPlatform\Metadata\Operation;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface;
use ApiPlatform\Metadata\CollectionOperationInterface;

final class CurrentCollectionExtension implements QueryCollectionExtensionInterface
{
    public function __construct()
    {
    }
    public function applyToCollection(
        QueryBuilder $queryBuilder,
        QueryNameGeneratorInterface $queryNameGenerator,
        string $resourceClass,
        Operation $operation = null,
        array $context = []
    ): void {
        if ($operation instanceof CollectionOperationInterface) {
            $this->addOffsetLimit($queryBuilder, $context);
        }

    }

    private function addOffsetLimit(QueryBuilder $queryBuilder, array $context = []): void
    {
        $offset = $context['filters']['offset'] ?? 0;
        $limit = $context['filters']['limit'];

        $queryBuilder->setFirstResult($offset)
                     ->setMaxResults($limit);
    }
}

3- decorate OpenApiFactory to add limit and offset for params in query for every collection operation.

public function __invoke(array $context = []): OpenApi
 {
     $openApi = $this->decorated->__invoke($context);
     $openApiPaths = $openApi->getPaths();
     foreach (array_keys($openApiPaths->getPaths()) as $path) {
         $pathItem = $openApiPaths->getPath($path);
         $operation = $pathItem->getGet();

         if ($operation !== null && str_ends_with($operation->getOperationId(), '_get_collection')) {
             $openApiPaths->addPath(
                 $path,
                 $pathItem->withGet(
                     $operation->withParameters(array_merge(
                         [new Model\Parameter('limit', 'query', '', false, false, true, ['type' => 'integer']),
                         new Model\Parameter('offset', 'query', '', false, false, true, ['type' => 'integer'])],
                         $operation->getParameters()
                     ))
                 )
             );
         }
     }

     return $openApi;
 }

And do not forget to add the extension in the service config

App\Extension\CurrentCollectionExtension:
        tags:
            - { name: api_platform.collection_extension, priority: 100 }

Hope it helps.

maySaghira avatar May 22 '23 15:05 maySaghira