Skip to content

Instantly share code, notes, and snippets.

@greg0ire
Last active May 20, 2018 16:42
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • 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();
}
@ostrolucky
Copy link

  1. grandTotal() is not needed in conjuction with hasNext(). Take Facebook as example. They won't show you how many results there are, because at some point that number is going to be useless for you. They should only tell you that there is no more posts available when you scroll to bottom of the list. Another example is EasyAdminBundle, which uses only previous and next button. For some reason I like that more than showing page numbers, although that bundle also shows total number of results - but having that shouldn't be required. Doing count(*) on lot of results is expensive, especially in PostgreSQL. It also prevents lazy load via generators, because trivial implementation will need to fetch all results to get the count. Although these are just technical reasons, I think UX wise it also makes interface easier to not bother user with numbers.

  2. currentOffset() - I didn't understand what is it for, sorry. Please better explanation.

  3. Paginated::hasNext is same as NeighbourAware::hasNextPage, no?

  4. previousPageNumber should be int, not bool

  5. Another methods I checked current paginators have you might have missed:
    haveToPaginate()
    pagesInRange()
    firstPageInRange()
    lastPageInRange()

  6. Regarding keyset pagination: I'm fan of such pagination, but I'm not a fan of suggested interface. For once, it might need goodies like grandTotal(), pageSize(), hasNext() too, it's not different in this regard. Second, what criteria? It only needs some ID of last item on the page/first item on next page. Rest of the stuff needed to build final criteria is stored in implementation, which might, or might not implement SortedPaginable.

  7. What is with those prefixes of those constants? DESC_ASCENDING and DESC_DESCENDING?

  8. I propose to simplify NeighbourAware interface. I think all implementations of hasPreviousPage and hasNextPage will look the same. Why not just make nextPageNumber and previousPageNumber nullable.

  9. I'm fan of countable in this context, instead of dedicated method. It allows you to push this to existing stuff expecting these interfaces. I guess that also means I'm in favor of suggested ISP, because otherwise it will not be obvious if \Countable returns result for total number of items, or items on current page, or even something else.

  10. Last one, I would to improve naming of those methods, here are my suggestions:
    grandTotal -> totalCountOfItems
    pageCount -> totalCountOfPages
    hasNext -> hasNextPage
    itemCount -> currentPageItemCount
    currentOffset -> ?

@greg0ire
Copy link
Author

Thanks for the very thorough review, I fixed the obvious mistake, and will think about many of your comments.

  1. Indeed, Paginable may be broken down even further, grandTotal could be moved out.
  2. I added a little sentence about that, it can be useful t o some people, not sure it should be in the main interface. I see that some people like to use offset pagination as opposed to page number pagination (they use offset as a get parameter in their links).
  3. yes, fixed by removing hasNext 😅
  4. fixed too, thanks!
  5. I added haveToPaginated, and have to check what the other methods are for
  6. I'm a bit unfamiliar with that method, but indeed, the ID of the last item should be enough, because it allows to get the criteria (i.e. for each sort field the value of the field for the last item). Not sure criteria is the best word here.
  7. I was really tired I guess 😅
  8. This one would be implemented in a decorator in the supporting library for the PSR, you would have a NeighbourAwareDecorator constructed from a Paginable. But it could be split into several interfaces, and there could be several decorators, if people want to cherry pick the interface they need.
  9. Yeah I tried to avoid \Countable for that reason in the first interface. In the Segregated version, I think it's ok to have it on Page though, because there is only one think you can count in a page: the elements.
  10. totalCountOfItems is a bit lengthy, I like grandTotal better for now. totalCountOfPages does not make much sense to me, because what would countOfPages be then?

@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