Custom pagination in PHP and Symfony

Add comment TR@SOE Jul 16, 2014

Pagination is probably the most widely used feature in any website. For PHP developers, there are already quite a number of libraries out there for us to pick and use in our own site -- if you choose not to develop one from scratch, that is.

Though DRY (Don't Repeat Yourself) is a good motto to follow in software engineering, I recommend that you develop your own PHP pagination implementation. Why? Here are a few reasons:

  • Pagination is missing in some popular frameworks (Symfony is just one).
  • It's sometimes impossible to find a third-party library that fits your needs exactly. They can either be too complicated, requiring time to master and customize, or too barebones and time consuming to extend.
  • It's an advanced, yet simple project for any mid-level PHP developer, especially when they understand database, template, framework and MVC basics.
  • Most of all, creating one by yourself and for yourself can create a great sense of accomplishment!

In this tutorial, we'll create a PHP pagination class and use it with the Symfony framework. This article assumes you're familiar with Symfony and know about project and database schema creation, sample data dumping and its MVC structures.

Key concepts

A pagination class contains the following key concepts, which will be explained in this article:

  • Getting the total number of pages
  • Retrieving the data set corresponding to the current page
  • Passing in the search keyword?
  • Constructing and displaying pagination links

NOTE: This isn't the end-all and be-all of pagination classes. Readers are encouraged to expand it to fit your case.

The Paginator class

Let's see the complete code of this class first and then get to the explanation.

<?php

namespace lib;

class Paginator
{
    private $totalPages;
    private $page;
    private $rpp;

    public function __construct($page, $totalcount, $rpp)
    {
        $this->rpp=$rpp;
        $this->page=$page;

        $this->totalPages=$this->setTotalPages($totalcount, $rpp);
    }

    /*
     * var recCount: the total count of records
     * var $rpp: the record per page
     */

    private function setTotalPages($totalcount, $rpp)
    {
        if ($rpp == 0)
        {
            $rpp = 20; // In case we did not provide a number for $rpp
        }

        $this->totalPages=ceil($totalcount / $rpp);
        return $this->totalPages;
    }

    public function getTotalPages()
    {
        return $this->totalPages;
    }

    public function getPagesList()
    {
        $pageCount = 5;
        if ($this->totalPages <= $pageCount) //Less than total 5 pages
            return array(1, 2, 3, 4, 5);

        if($this->page <=3)
            return array(1,2,3,4,5);

        $i = $pageCount;
        $r=array();
        $half = floor($pageCount / 2);
        if ($this->page + $half > $this->totalPages) // Close to end
        {
            while ($i >= 1)
            {
                $r[] = $this->totalPages - $i + 1;
                $i--;
            }
            return $r;
        } else
        {
            while ($i >= 1)
            {
                $r[] = $this->page - $i + $half + 1;
                $i--;
            }
            return $r;
        }
    }

}

?>

The first thing to point out is that we don't interfere with database operations in this class.

We don't want to mix our pagination class with database manipulation because pagination is essentially not an operation on database. Rather, it's an operation on a data set, which can come from a database query, a JSON API call or a text file. It doesn't care about the data itself. It only concerns itself with the record numbers and total pages. With this separation, we have more flexibility when using the class. And this makes programming the class much simpler.

The class provides three "helper" functions:

  • setTotalPages uses the number of records ($totalcount) and records per page ($rpp) to determine how many pages there are for the current data set. It uses PHP's built-in ceil function to calculate the page total.
  • getTotalPages simply returns the total pages calculated by the above function.
  • getPagesList provides a page index list, and tries to put the current page in the middle of the list. So if current page is 7, and there are a total 20 pages, the list returned will be 5 6 7 8 9. But if the current page is 15 and there are only 16 pages, the list returned will be 12 13 14 15 16. The list returned here will be used later in the tutorial.

Integrating with Symfony

We've finished the class coding and it's time to put it into Symfony so we can use it in our controllers.

First, copy the Paginator.php file to Your Symfony Project root\src\lib directory. There are other ways to register this class, but I am using the simplest one. By doing this, we can use this class in our code like this:

paginator = new \lib\Paginator($page, $totalcount, $rpp);

Unit testing

The Paginator class has some functions which involve quite a few algorithms. So, to make sure this class functions well before we really start to use it, we need to run a unit test under the Symfony environment.

We'll use PHPUnit to see if our class is working correctly.

First, create a PaginatorTest.php file under Symfony Project Root/src/Your BundleName/Tests/lib. In my case, I'm running Windows 8.1 so the directory becomes: f:\www\rsywx.chn\src\tr\rsywxBundle\Tests\lib\.

<?php 
    namespace tr\rsywxBundle\Tests\lib;

    use \lib\Paginator;

    class PaginatorTest extends \PHPUnit_Framework_TestCase
    {
        public function testgetPageList()
        {
            $paginator=new Paginator(2, 101, 10);
            $pages=$paginator->getTotalPages();
            $this->assertEquals($pages, 11);
            $list=$paginator->getPagesList();
            $this->assertEquals($list, array(1,2,3,4,5));

            $paginator=new Paginator(7, 101, 10);
            $list=$paginator->getPagesList();
            $this->assertEquals($list, array(5,6,7,8,9));

            $paginator=new Paginator(10, 101, 10);
            $list=$paginator->getPagesList();
            $this->assertEquals($list, array(7,8,9,10,11));
        }
    }

?>

Run php phpunit.phar -c app/ in your project root directory. The terminal should prompt a success notice (saying OK (1 test, 4 assertions)).

Bingo! We now have the confidence to use this class in our application.

In a controller/repository

As I explained earlier, this pagination class doesn't involve database operations. So, it 's our responsibility to do these in our controller/repository so that the correct parameters can be passed to the class.

Pagination normally appears in a list action, so we'll create a listAction in our controller:

public function listAction($page, $key, $type)
{
    $em = $this->getDoctrine()->getManager();
    $rpp = $this->container->getParameter('books_per_page');

    $repo = $em->getRepository('trrsywxBundle:BookBook');

    list($res, $totalcount) = $repo->getResultAndCount($page, $rpp, $key, $type);

    $paginator = new \lib\Paginator($page, $totalcount, $rpp);
    $pagelist = $paginator->getPagesList();

    return $this->render('trrsywxBundle:Books:List.html.twig', array('res' => $res, 'paginator' => $pagelist, 'cur' => $page, 'total' => $paginator->getTotalPages(), 'key'=>$key, 'type'=>$type));
}

NOTE: We should create a corresponding route for this action too:

book_list:
  pattern: /books/list/{type}/{page}/{key}
  defaults: 
    page: 1
    key: all
    type: title
    _controller: trrsywxBundle:Book:list

In this controller, we're doing a few things:

  1. From our configuration file, we retrieve the "records per page" parameter (books_per_page).
  2. We get the corresponding data set and total record count of the table by calling the repository method getResultAndCount with the current page, records per page, the key and the search type as parameters.
  3. We then create a Paginator instance and get necessary properties from that class to render our template.

The getResultAndCount function is the key for our pagination to work. Let's take a quick look at its implementation in pseudo-code:

public function getResultAndCount($page, $rpp, $key='all', $type='title')
{
    if type is "title" // we are searching for a book by its title
        if key is "all"
            construct a "select count()" query without "where" clause
        construct the "where" clause with the key provided
        execute the query and get total record count

        construct the "select" query to retrieve data 
        based on the same process above 
        using rpp and page 
        to return total "rpp" records fall in the "page"

        return the results and total count
    else if type is "tag"
        do the same thing for "title" but the criteria will be applied on "tag" field 
}

We used two separate queries with the same where condition (determined by key) to get the total result count of a table and the result set corresponding to current page, respectively.

Also, with the "key" parameter presented in the route/URI and passed into the controller, we managed to show a paginated result based on certain criteria. What that boils down to is that our navigation across pages won't lose the criteria specified.

In a template

Our final step is to display the pagination along with the dataset in a template.

There are two ways to display the pagination links (or navigation links in some template terms):

  1. To display only the navigation buttons ("First", "Previous", "Next", "Last").
  2. To display a list of pages for quicker navigation.

It's rather straightforward to implement either or both in our application. We can further tack on some CSS boiler plates to create a consistent look with other elements in our application.

I'll leave this to your creativity and enthusiasm to settle on which you prefer. Below is a working demo of this class when integrated with Symfony, using Bootstrap CSS:

PHP Pagination with Symfony

Conclusion

Now that you've made it through this tutorial, you should have a solid pagination class that you can extend how you wish. I do encourage you to expand and customize this class, as well as use it in your real-world applications.

Feel free to comment and tell me your thoughts on this tutorial!

0 comments


Or enter your name and Email
No comments have been posted yet.