Pagination with Twig and KnpPaginatorBundle


In this video we are going to add in Pagination into our Twig CRUD implementation. We will be re-using the code from the previous course where we implemented a CRUD app using:

  • Symfony 3 with Twig
  • Symfony 3 as an API
  • Angular 1.5
  • React 15.2

We will cover how to add in Pagination, Filtering, and Sorting to each of these implementations.

For the moment, we are only concerning ourselves with adding Pagination into the Twig implementation. However, what you are about to learn will also apply - in the vast majority - to every other implementation. This is because all of this happens on the server side.

If you would like to jump right in and haven't yet completed the previous course then you can clone the repository from GitHub (the master branch) and follow along.

Installing The Paginator Bundle

There are broadly two schools of thought, as best I can tell, when it comes to development.

There are those who like to make everything for themselves.

And there are those (like me) who like to make use of existing components where-ever possible.

Personally, a large part of making the commitment to learn and use a framework like Symfony is to enable me to stand on the shoulders of giants - I mean, why not leverage the ecosystem surrounding your framework? Sure, you gain extra dependencies, but the trade off is you just saved yourself a whole bunch of time.

With that in mind, the way in which we are going to implement pagination, filtering, and sorting is through KNP Paginator Bundle.

You are - of course - completely free to roll your own, or use a different bundle, or pursue whatever other option you desire.

One thing to note, the current docs for KNP Paginator (at the time of writing) say that this is a paginator for Symfony2. It is, but it's also compatible with Symfony 3.

Installing the bundle is largely copy / paste from the documentation:

composer require knplabs/knp-paginator-bundle

Follow the instructions to add the PaginatorBundle into AppKernel.php:

// app/AppKernel.php
public function registerBundles()
{
    return array(
        // ...
        new Knp\Bundle\PaginatorBundle\KnpPaginatorBundle(),
        // ...
    );
}

And also copy / paste the config into config.yml:

knp_paginator:
    page_range:                 5          # default page range used in pagination control
    default_options:
        page_name:              page       # page query parameter name
        sort_field_name:        sort       # sort field query parameter name
        sort_direction_name:    direction  # sort direction query parameter name
        distinct:               true       # ensure distinct results, useful when ORM queries are using GROUP BY statements
    template:
        pagination: KnpPaginatorBundle:Pagination:twitter_bootstrap_v3_pagination.html.twig     # sliding pagination controls template
        sortable: KnpPaginatorBundle:Pagination:sortable_link.html.twig                         # sort link template

Note that I have made one tweak to the config above - changing the template from the default sliding pagination, to the Bootstrap 3 equivalent. This is completely integrated into KNP Paginator Bundle, and as we are using Bootstrap 3, it makes sense to make use of whats there.

At this stage, we are almost good to go.

Thank you KNP! Your bundles rock.

Now one last point to note before we continue. We are going to need to do this process again when configuring pagination inside our API. I just want to make you aware that we won't need the template section at that point - but it's interesting to note what that will change for us. Keep an eye out for this in the next few videos.

Using KNP Paginator

Now that the KNP Paginator Bundle is set up in our project, we can go ahead and use the service inside our controller(s).

We will only need to implement pagination into one of our actions - the listAction. However, you can use it as many times as you need, paginating results as required.

By adding the bundle to our project and providing the necessary configuration, a new service will have been registered for us:

knp_paginator

The first step is to get access to this service inside our controller. Being Symfony, there are multiple ways to do this. The easiest way is using the service locator:

$paginator  = $this->get('knp_paginator');

But if you prefer, you can set up your controller as a service and then inject your specific dependencies instead.

And once we have the paginator, we can use it really easily by calling paginate, and passing in the 'thing' to paginate, and optionally the page we want, the amount of results per page, and some options - which we won't need to use.

It's important to that we pass in a Query to our paginator, rather than a result.

This is because we want KNP Paginator to tweak our query to make it efficient. Let's quickly cover what the differences would be here:

Let's pretend we have a database table with 1 million rows.

We want to get a paginated result where we have 100 rows per page, and we want page 2.

KNP Paginator allows us to pass in many different 'things' to paginate over. If we had the following code:

    /**
     * @Route("/", name="list")
     */
    public function listAction()
    {
        $em = $this->getDoctrine()->getManager();

        $allOurBlogPosts = $em->getRepository('AppBundle:BlogPost')->findAll();

        $paginator  = $this->get('knp_paginator');

        $blogPosts = $paginator->paginate(
            $allOurBlogPosts, 
            2 /*page number*/,
            100 /*limit per page*/
        );

        return $this->render('BlogPosts/list.html.twig', [
            'blog_posts' => $blogPosts,
        ]);
    }

This would go off and pull back the entire dataset - all 1 million rows - then pass them into the paginator which would slice and dice them and give us the tiny portion of records we actually wanted.

This is incredibly inefficient, and the problem gets worse the more the table grows.

Instead, if we tweak this only slightly, and rather than pass in the result to our paginator, we pass in the as-yet-unrun query, then KNP Paginator can alter our query to ensure it only requests the tiny sliver of data we really wanted:

    /**
     * @Route("/", name="list")
     */
    public function listAction(Request $request)
    {
        $em = $this->getDoctrine()->getManager();

        $queryBuilder = $em->getRepository('AppBundle:BlogPost')->createQueryBuilder('bp');

        $query = $queryBuilder->getQuery();

        $paginator  = $this->get('knp_paginator');

        $blogPosts = $paginator->paginate(
            $query, /* query NOT result */
            2 /*page number*/,
            100 /*limit per page*/
        );

        return $this->render('BlogPosts/list.html.twig', [
            'blog_posts' => $blogPosts,
        ]);
    }

If you are unsure about creating queries using DQL or the Query Builder, then I would recommend you watch this Doctrine tutorial series where all of this is covered in much greater depth.

Whilst it may not be immediately obvious:

        $queryBuilder = $em->getRepository('AppBundle:BlogPost')->createQueryBuilder('bp');

        $query = $queryBuilder->getQuery();

Those two lines effectively created a SELECT * FROM blog_posts type query. That's why this continues to work in the same way as the more obvious ->findAll(); approach.

Removing The Hard Coding

We've set the page and limit to hardcoded values - 2 and 100 respectively.

Rather than do this, which is going to severely restrict the usefulness of our site, we can make use of Symfony's Request object to pull out interesting parts of the URL and use them as variables in our code:

$request->query->getInt('page', 1)

This is telling Symfony to look at the URL and pull out the page part of the query string. The 1 here is a default - as in, if the URL doesn't have a page parameter then default the result to 1.

This can be a little confusing, so I will elaborate slightly. Apologies if you already know all this. You may also already know this, but not know the terminology.

A URL with a query string might look like this:

http://mysite.com/?page=3&colour=purple

The query string part is:

?page=3&colour=purple

That's why we use $request->query. In this case, query is a ParameterBag which is a fancy name for simply a container of key / values.

You typically use $request->query when working with GET requests.

You may also see $request->request, which is very similar, but would more commonly be used with POST, or PATCH, or PUT requests.

The parameter bags have a bunch of helper methods (e.g. getInt) which allow us to pull out values from the incoming request, or go with defaults if the requested parameter didn't exist.

Knowing all this, we can change up our code to use params from the request:

        $blogPosts = $paginator->paginate(
            $query, /* query NOT result */
            $request->query->getInt('page', 2)/*page number*/,
            $request->query->getInt('limit', 100)/*limit per page*/
        );

And we should now be able to play around the with URL in order to get back the exact data set we want.

With this complete, we can add the paginator template to our page, which is going to use the exact concepts from above to pull all this together.

Paginator Template

The last step is to add in the Twig helper function to render out the paginator on the page:

    <div class="navigation text-center">
        {{ knp_pagination_render(blog_posts) }}
    </div>

Note that blog_posts matches up with the variable name we passed in to the render function in our controller action:

        return $this->render('BlogPosts/list.html.twig', [
            'blog_posts' => $blogPosts,
        ]);

If you change this variable name, be sure to update your knp_pagination_render call accordingly.

And thanks to the fact that we set up our configuration to use the Bootstrap 3 template, we have a nicely styled paginator added to our page with the very minimum amount of fuss, effort, and energy.

Awesome.

Code For This Video

Get the code for this video.

Episodes