api-platform / core

The server component of API Platform: hypermedia and GraphQL APIs in minutes
https://api-platform.com
MIT License
2.45k stars 878 forks source link

Document how to introduce custom pagination headers #1548

Open sashaaro opened 6 years ago

sashaaro commented 6 years ago

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?!

dunglas commented 6 years ago

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 commented 6 years ago

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

norkunas commented 6 years ago

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',
        ];
    }
}
sashaaro commented 6 years ago

@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

dkarlovi commented 6 years ago

@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).

deivid11 commented 6 years ago

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

teamore commented 5 years ago

@norkunas: Easy solution. Thanks mate.

soullivaneuh commented 5 years ago

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 commented 5 years ago

@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:

er1z commented 5 years ago

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

soyuka commented 5 years ago

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

soullivaneuh commented 5 years ago

@soyuka Why not proposing an option for that?

norkunas commented 5 years ago

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

dunglas commented 5 years ago

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

FireLizard commented 5 years ago

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?

soyuka commented 5 years ago

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!

FireLizard commented 5 years ago

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)

Toflar commented 2 years ago

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;
    }
}
CaptainFalcon92 commented 2 years ago

@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 👋 🌟