The League of Extraordinary Packages

Our Packages:

Presented by The League of Extraordinary Packages

Getting Started

Concepts

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:

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 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);

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.

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
		}
	}
}