Open sashaaro opened 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)?
OData has something, there is also the Range
HTTP header: http://otac0n.com/blog/2012/11/21/range-header-i-choose-you.html
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 nice! I need try this. @dunglas good if we decide foramt how return that data and introduce to platform from out of box feature
@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).
Looking for this! I needed just a simple application/json format that counts items.
@norkunas: Easy solution. Thanks mate.
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.
@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:
Link
is pretty common for next/prev/etc (it exists for years), but harder to parse.
Range is definitely for partial content serving. IMHO we just need to add @norkunas snippet to the documentation to close this issue.
@soyuka Why not proposing an option for that?
@soyuka @dunglas may I propose to add the opt-in listener to the core?
Yes we can add a new OData sub-namespace and progressively start to add support for some OData features.
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?
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!
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)
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;
}
}
@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 👋 🌟
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?!