Pagination
When working with a large data set it obviously makes sense to offer pagination options to the endpoint, otherwise that data can get very slow. To avoid writing your own pagination output into every endpoint, Fractal provides you with two solutions:
- Paginator
- Cursor
Using Paginators
Paginators offer more information about your result-set including total, and have next/previous links which will only show if there is more data available. This intelligence comes at the cost of having to count the number of entries in a database on each call.
For some data sets this might not be an issue, but for some it certainly will. If pure speed is an issue, consider using Cursors instead.
Paginator objects are created, and must implement League\Fractal\Pagination\PaginatorInterface
and its specified methods. The instantiated object must then be passed to the League\Fractal\Resource\Collection::setPaginator()
method.
Fractal currently ships with the following adapters:
- Laravel’s
illuminate/pagination
package asLeague\Fractal\Pagination\IlluminatePaginatorAdapter
- The
pagerfanta/pagerfanta
package asLeague\Fractal\Pagination\PagerfantaPaginatorAdapter
- The Phalcon Paginator (
phalcon/cphalcon
) asLeague\Fractal\Pagination\PhalconFrameworkPaginatorAdapter
- Zend Framework’s
zendframework/zend-paginator
package asLeague\Fractal\Pagination\ZendFrameworkPaginatorAdapter
Laravel Pagination
As an example, you can use Laravel’s Eloquent or Query Builder method paginate()
to achieve the following:
use League\Fractal\Resource\Collection;
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
use Acme\Model\Book;
use Acme\Transformer\BookTransformer;
$paginator = Book::paginate();
$books = $paginator->getCollection();
$resource = new Collection($books, new BookTransformer());
$resource->setPaginator(new IlluminatePaginatorAdapter($paginator));
Phalcon Pagination
As an example, you can use the result of the getPaginate()
method on a Phalcon Paginator object or one that implements the Phalcon\Paginator\AdapterInterface
:
use League\Fractal\Resource\Collection;
use League\Fractal\Pagination\PhalconFrameworkPaginatorAdapter;
use Phalcon\Mvc\Model\Query\Builder;
use Phalcon\Paginator\Adapter\QueryBuilder;
use Acme\Model\Book;
use Acme\Transformer\BookTransformer;
$builder = new Builder();
$builder->addFrom(Book::class);
$params = [
'builder' => $builder,
'limit' => 10,
'page' => 2,
];
$paginator = new QueryBuilder($params);
$books = $paginator->getPaginate();
/** $books->items has the data */
$resource = new Collection($books->items, new BookTransformer());
$resource->setPaginator(new PhalconFrameworkPaginatorAdapter($books));
Symfony Pagination
Below is an example of pagination using the Pagerfanta Paginator with a collection of objects obtained from Doctrine.
$doctrineAdapter = new DoctrineCollectionAdapter($allItems);
$paginator = new Pagerfanta($doctrineAdapter);
$filteredResults = $paginator->getCurrentPageResults();
$paginatorAdapter = new PagerfantaPaginatorAdapter($paginator, function(int $page) use (Request $request, RouterInterface $router) {
$route = $request->attributes->get('_route');
$inputParams = $request->attributes->get('_route_params');
$newParams = array_merge($inputParams, $request->query->all());
$newParams['page'] = $page;
return $router->generate($route, $newParams, 0);
});
$resource = new Collection($filteredResults, new BookTransformer());
$resource->setPaginator($paginatorAdapter);
Including existing query string values in pagination links
In the example above, previous and next pages will be provided simply with ?page=#
ignoring all other
existing query strings. To include all query string values automatically in these links we can replace
the last line above with:
use Acme\Model\Book;
$year = Input::get('year');
$paginator = Book::where('year', '=', $year)->paginate(20);
$queryParams = array_diff_key($_GET, array_flip(['page']));
$paginator->appends($queryParams);
$paginatorAdapter = new IlluminatePaginatorAdapter($paginator);
$resource->setPaginator($paginatorAdapter);
Using Cursors
When we have large sets of data and running a SELECT COUNT(*) FROM whatever
isn’t really an option, we need a proper
way of fetching results. One of the approaches is to use cursors that will indicate to your backend where to start
fetching results. You can set a new cursor on your collections using the
League\Fractal\Resource\Collection::setCursor()
method.
The cursor must implement League\Fractal\Pagination\CursorInterface
and its specified methods.
Fractal currently ships with a very basic adapter: League\Fractal\Pagination\Cursor
. It’s really easy to use:
use Acme\Model\Book;
use Acme\Transformer\BookTransformer;
use League\Fractal\Pagination\Cursor;
use League\Fractal\Resource\Collection;
$currentCursor = Input::get('cursor', null);
$previousCursor = Input::get('previous', null);
$limit = Input::get('limit', 10);
if ($currentCursor) {
$books = Book::where('id', '>', $currentCursor)->take($limit)->get();
} else {
$books = Book::take($limit)->get();
}
$newCursor = $books->last()->id;
$cursor = new Cursor($currentCursor, $previousCursor, $newCursor, $books->count());
$resource = new Collection($books, new BookTransformer);
$resource->setCursor($cursor);
These examples are for Laravel’s illuminate\database
package, but you can do it however you like. The cursor
also happens to be constructed from the id
field, but it could just as easily be an offset number. Whatever
is picked to represent a cursor, maybe consider using base64_encode()
and base64_decode()
on the values to make sure
API users do not try and do anything too clever with them. They just need to pass the cursor to the new URL, not do any maths.
Example Cursor Usage
GET /books?cursor=5&limit=5
{
"books": [
{ "id": 6 },
{ "id": 7 },
{ "id": 8 },
{ "id": 9 },
{ "id": 10 }
],
"meta": {
"cursor": {
"previous": null,
"current": 5,
"next": 10,
"count": 5
}
}
}
On the next request, we move the cursor forward.
- Set
cursor
tonext
from the last response - Set
previous
tocurrent
from the last response limit
is optional * You can set it tocount
from the previous request to maintain the same limit
GET /books?cursor=10&previous=5&limit=5
{
"books": [
{ "id": 11 },
{ "id": 12 },
{ "id": 13 },
{ "id": 14 },
{ "id": 15 }
],
"meta": {
"cursor": {
"previous": 5,
"current": 10,
"next": 15,
"count": 5
}
}
}