core icon indicating copy to clipboard operation
core copied to clipboard

Document how to introduce custom pagination headers

Open sashaaro opened this issue 7 years ago • 19 comments

I noted seems totalItems value returns only in format Hydra collection.
What about idea add X-Total, X-Current-Page, X-Page-Count, X-Limit to headers for any pagination request particulary json format?!

sashaaro avatar Dec 06 '17 09:12 sashaaro

Why not if it's an opt-in feature. Do you know some kind of standard for those headers (using the X- prefix is deprecated)?

dunglas avatar Dec 06 '17 09:12 dunglas

OData has something, there is also the Range HTTP header: http://otac0n.com/blog/2012/11/21/range-header-i-choose-you.html

dunglas avatar Dec 06 '17 09:12 dunglas

We are using headers for pagination in json format:

<?php

declare(strict_types=1);

namespace AppBundle\EventSubscriber;

use ApiPlatform\Core\Bridge\Doctrine\Orm\Paginator;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;

final class AddPaginationHeaders implements EventSubscriberInterface
{
    public function addHeaders(FilterResponseEvent $event): void
    {
        $request = $event->getRequest();

        if (($data = $request->attributes->get('data')) && $data instanceof Paginator) {
            $from = $data->count() ? ($data->getCurrentPage() - 1) * $data->getItemsPerPage() : 0;
            $to = $data->getCurrentPage() < $data->getLastPage() ? $data->getCurrentPage() * $data->getItemsPerPage() : $data->getTotalItems();

            $response = $event->getResponse();
            $response->headers->add([
                'Accept-Ranges' => 'items',
                'Range-Unit' => 'items',
                'Content-Range' => \sprintf('%u-%u/%u', $from, $to, $data->getTotalItems()),
            ]);
        }
    }

    /**
     * {@inheritdoc}
     */
    public static function getSubscribedEvents(): array
    {
        return [
            KernelEvents::RESPONSE => 'addHeaders',
        ];
    }
}

norkunas avatar Dec 06 '17 09:12 norkunas

@norkunas nice! I need try this. @dunglas good if we decide foramt how return that data and introduce to platform from out of box feature

sashaaro avatar Dec 06 '17 09:12 sashaaro

@dunglas when I suggested it, you've said there's no reason to introduce a custom hypermedia standard if you support Hydra, glad you've changed your mind. :+1:

While I like @norkunas approach, I feel like it would need to hook into the doc generator since you're exposing information which might be used for the SDK generator (Swagger).

dkarlovi avatar Dec 22 '17 10:12 dkarlovi

Looking for this! I needed just a simple application/json format that counts items.

deivid11 avatar Jan 18 '18 04:01 deivid11

@norkunas: Easy solution. Thanks mate.

teamore avatar Nov 26 '18 13:11 teamore

Another sample with GitHub API: https://developer.github.com/v3/#pagination

They use Link header to display the next page and the last page.

I'm not sure there is any convention about this, you may choose a default one and add the possibility to easily change it.

soullivaneuh avatar Dec 06 '18 14:12 soullivaneuh

@dunglas According to this comment, the range header may break some cli supports.

EDIT: Plus this one telling the range is just for bytes.

I'm not sure it's the best solution. :thinking:

soullivaneuh avatar Dec 06 '18 15:12 soullivaneuh

Link is pretty common for next/prev/etc (it exists for years), but harder to parse.

er1z avatar Dec 07 '18 07:12 er1z

Range is definitely for partial content serving. IMHO we just need to add @norkunas snippet to the documentation to close this issue.

soyuka avatar Dec 07 '18 10:12 soyuka

@soyuka Why not proposing an option for that?

soullivaneuh avatar Dec 11 '18 09:12 soullivaneuh

@soyuka @dunglas may I propose to add the opt-in listener to the core?

norkunas avatar Sep 11 '19 04:09 norkunas

Yes we can add a new OData sub-namespace and progressively start to add support for some OData features.

dunglas avatar Sep 11 '19 06:09 dunglas

Another sample with GitHub API: https://developer.github.com/v3/#pagination

They use Link header to display the next page and the last page.

I'm not sure there is any convention about this, you may choose a default one and add the possibility to easily change it.

Is there any effort to implement the Link header in the future?

FireLizard avatar Sep 20 '19 08:09 FireLizard

We already do: https://github.com/api-platform/core/blob/345612c913e1aca6da4f4aa1cd885421ca6385ff/src/Hydra/EventListener/AddLinkHeaderListener.php but maybe it's missing some informations? Also note that you can easily add your own listener that adds your own headers!

soyuka avatar Sep 20 '19 09:09 soyuka

Thank you :+1: Is there any consensus to use Link header over hypermedia by representation format (like HAL, json:api, ...)? My question is: should we using a protocol-agnostic format or using a format-agnostic protocol to make our app hypermedia-driven?

(Sorry for asking here. Maybe there's a discussion forum that I didn't found yet)

FireLizard avatar Sep 20 '19 09:09 FireLizard

This is the solution I'm currently using. It provides all the information about the pagination as well as Link header information for first and last pages and if available also next or prev. It also works for the PartialPagniatorInterface in which case there's only the info for the current page and the items per page. The parameter you want to inject is %api_platform.collection.pagination.page_parameter_name%.

Not sure if that's still desirable in core but it might be a nice addition no matter if you're using hydra or not.

class PaginationHeadersListener implements EventSubscriberInterface
{
    public function __construct(private string $paginationParameterName)
    {
    }

    public function onKernelResponse(ResponseEvent $event): void
    {
        if (HttpKernelInterface::MAIN_REQUEST !== $event->getRequestType()) {
            return;
        }

        $request = $event->getRequest();
        $response = $event->getResponse();
        $data = $request->attributes->get('data');

        if (!$data instanceof PartialPaginatorInterface) {
            return;
        }

        $currentPage = (int) $data->getCurrentPage();

        $response->headers->set('Pagination-Current-Page', $currentPage);
        $response->headers->set('Pagination-Items-Per-Page', (int) $data->getItemsPerPage());

        if (!$data instanceof PaginatorInterface) {
            return;
        }

        $lastPage = (int) $data->getLastPage();

        $response->headers->set('Pagination-Last-Page', $lastPage);
        $response->headers->set('Pagination-Total-Items', (int) $data->getTotalItems());

        $linkProvider = $request->attributes->get('_links', new GenericLinkProvider());

        foreach ($this->collectLinks($request, $currentPage, $lastPage) as $rel => $url) {
            $linkProvider = $linkProvider->withLink(new Link($rel, $url));
        }

        $request->attributes->set('_links', $linkProvider);
    }

    public static function getSubscribedEvents(): array
    {
        return [
            KernelEvents::RESPONSE => ['onKernelResponse'],
        ];
    }

    private function collectLinks(Request $request, int $currentPage, int $lastPage): array
    {
        $links = [];
        $parsed = IriHelper::parseIri($request->getUri(), $this->paginationParameterName);

        $links['first'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->paginationParameterName, 1);
        $links['last'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->paginationParameterName, $lastPage);

        if (1 !== $currentPage) {
            $links['prev'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->paginationParameterName, $currentPage - 1);
        }

        if ($currentPage !== $lastPage) {
            $links['next'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->paginationParameterName, $currentPage + 1);
        }

        return $links;
    }
}

Toflar avatar Nov 24 '21 12:11 Toflar

@norkunas commented on Dec 6, 2017

I love it ! that is exactly how i wanted to implement pagination. I wonder if you also managed to read pagination instruction from range header ? I'm looking to have it work both ways.

I'll keep looking. Bye 👋 🌟

CaptainFalcon92 avatar Nov 25 '21 22:11 CaptainFalcon92