Skip to content

Instantly share code, notes, and snippets.

@greg0ire
Last active May 20, 2018 16:42
Show Gist options
  • Save greg0ire/621245c7e1112787f9c3e319e5718bc6 to your computer and use it in GitHub Desktop.
Save greg0ire/621245c7e1112787f9c3e319e5718bc6 to your computer and use it in GitHub Desktop.
Pagination

Pagination

Pagination is the process of dividing a long list of results into easier to consume pages. By only loading one page at a time, memory is preserved. This applies to RDBMS result sets, collections of serialized items in an API, or listings in list views of simple CRUD applications. Although the needs are often the same, many libraries define their own objects, or even use plain arrays to represent a paginated result. As a result, projects consuming these libraries often end up defining many adapters for those many libraries. This is an effort to define the needs clients of those libraries have, and to let a common interface emerge. Keep in mind that I'm not great at naming, and that any suggestions for alternative names or anything else are welcome.

So what is needed when asking for a page? What shall be the core interface?

  • Results, obviously, so, an iterable, at the very least.
  • Next, the number of total items that could be returned for the query.
  • The number of items per page is a necessary information too, because it is not necessarily specified by the client.
  • Finally the number of the current page helps although always specified.
interface Paginable extends \Traversable
{
    public function grandTotal(): int;
    public function pageSize(): int;
    public function currentPageNumber(): int
}

From those 3 numbers, many other convenient pieces of information can be computed, letting one or several interfaces emerge.

  • the number of pages
  • whether there is a next page or a previous page
  • if this is the last page, the number of items on the page (which may differ from the page size in this particular case)
  • what the offset of first item of the current page is, if for some reason you want to use offsets to generate pagination links.
  • whether it is necessary to display pagination
interface Paginated extends \Paginable
{
    public function pageCount(): int;
    public function currentPageItemCount(): int;
    public function currentOffset(): int;
    public function haveToPaginate(): bool;
    public function hasItems(): bool;
}

interface NeighbourAware extends \Paginable
{
    public function hasPreviousPage(): bool;
    public function hasNextPage(): bool;
    public function nextPageNumber(): int;
    public function previousPageNumber(): int;
}

One goal of this PSR could be to provide one or several decorators that would provide this or these other interfaces when given any implementation of the first one.

Segregated interfaces

This looks a bit messy, because representing the current page and representing the entire potential result are 2 responsabilities you might want to segregate.

Also, not everyone wants to count all items as it might be expensive. A Paginable may be viewed as a collection of pages, which may be countable, and since pages are collections of items, a Paginable should be a collection of items too. And those items might be counted. \Countable should be avoided because of this ambiguity.

And finally, you could want to traverse a collection of pages, if for some reason you need to do some batch processing on pages returned by some webservice.

Consider this:

interface Paginable /* implementations may or may not implement \Traversable */
{
    public function pageSize(): int;
    public function currentPageNumber(): int;
    public function currentPage(): Page;
}

interface CountablePageCollection extends \Paginable
{
    public function pageCount();
}

interface CountableItemCollection extends \Paginable
{
    public function grandTotal();
}

interface Page extends \Traversable /* implementations may or may not implement \Countable */
{
}

or maybe this version, that moves the knowledge of the page number to the page itself:

interface Paginable
{
    public function pageSize(): int;
    public function currentPage(): Page;
}

interface Page extends \Traversable /* implementations may or may not implement \Countable */
{
    public function number(): int;
}

Does it look better? It might be harder to reconcile with existing implementations, that tend to implement both responsabilites in the same object.

Sort specification

A paginable might want to indicate what sorting method was used to generate it, especially if several of them are supported.

interface SortedPaginable
{
    public const SORT_ASCENDING = 'asc';
    public const SORT_DESCENDING = 'desc';

    /**
     * @return array a hash that associates a field (any string) to a sort
     *               direction. The order of fields inside the array matters.
     */
    public function sortSpecification(): array;
}

Keyset pagination

If you are a fan of keyset pagination, you probably need a different interface, as your pagination looks different.

interface KeysetPaginable /** may or may not implement \Countable */
{
}

interface Page extends \Traversable
{
    public function lastElementIdentifier();
    public function hasNextPage();
    public function number();
}
@folliked
Copy link

folliked commented Dec 5, 2017

I have the impression that the semantics are not harmonious because on one side you use this term pageCount for CountablePageCollection and on the other hand you use grandTotal for CountableItemCollection => why not itemCount ? or totalPages and totalItems ?

@greg0ire
Copy link
Author

greg0ire commented Dec 7, 2017

@folliked you're right, something is wrong here, I'm not sure yet whether it's the method naming or the interface naming.

@garak
Copy link

garak commented May 20, 2018

@greg0ire well, I think that it should be far more simpler than that. Just like PaginationInterface and PaginatorInterface

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment