Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support offset/slice in pagination #4672

Closed
tacman opened this issue Mar 8, 2022 · 6 comments
Closed

Support offset/slice in pagination #4672

tacman opened this issue Mar 8, 2022 · 6 comments

Comments

@tacman
Copy link
Contributor

tacman commented Mar 8, 2022

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
Copy link
Contributor Author

tacman commented Jun 11, 2022

@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

@stale
Copy link

stale bot commented Nov 4, 2022

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 stale bot added the wontfix label Nov 4, 2022
@stale
Copy link

stale bot commented Jan 4, 2023

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 stale bot added the stale label Jan 4, 2023
@stale stale bot closed this as completed Jan 11, 2023
@maySaghira
Copy link

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

@tacman
Copy link
Contributor Author

tacman commented Mar 21, 2023

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?

@maySaghira
Copy link

maySaghira commented May 22, 2023

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants