Pagination


In this video we are going to add in the ability to Paginate the result sets that we retrieve when searching for wallpapers in a given category.

For complete clarity, as our site grows a category may come to contain 100's of images. As it stands currently we aren't doing any image processing, so when a visitor to our site browses any particular page, each image they see is a full size image. We're currently just shrinking that image down to fit the given container.

If each image is 1mb in size, imagine how slowly our page would load if we had 100 images to display. Horrendous.

But of course, we won't be returning the full size image to our site visitors when we're done. But even so, studies have shown that breaking up result sets such as this into smaller chunks is the preferred way to consume this type of content.

Anyway, learning how to paginate data is a useful skill to have. And fortunately, there's a Bundle for that!

Pagination in Symfony with KnpPaginatorBundle

Because pagination - whether in a Symfony environment or not - is such a common occurrence, it's unsurprising that there is a pre-existing Symfony bundle that adds in pagination functionality in a few short steps.

Maybe you've heard about Symfony's Bundles but aren't quite sure what they are. Think about them as "plugins" to your application. In much the same way that WordPress has its huge selection of plugins to meet most any need, so to does Symfony with the bundle concept.

The really nice part about using these third party bundles is that with very little additional effort on our part, we gain a huge amount of functionality. Often these bundles have been used by hundreds or thousands of other developers on other projects. This usually leads to a much more robust and reliable codebase than we could create given existing time pressures to get the wider project completed.

It is worth mentioning that a bundle is not "free". Yes, they are free in terms of being open source software that you do not need to directly exchange money to use. But they add an extra dependency to your project and you will be responsible for managing that dependency. As ever, it's a trade off.

Taking all this into consideration, at the time of writing / recording, KnpPaginatorBundle is at version 2.5.4.

It has had 363 commits from 103 contributors.

According to packagist, KnpPaginatorBundle has been installed 4,433,452 million times.

Given these facts, do I think I could write a better pagination implementation in one video as part of a wider course? The answer is hopefully obvious: no, of course I couldn't.

So that'll be KnpPaginatorBundle has been installed 4,433,453 million times then :)

Installing KnpPaginatorBundle

There are pre-existing installation instructions available in the official documentation for KnpPaginatorBundle.

For reference, add the bundle to your project using composer:

composer require knplabs/knp-paginator-bundle

When the bundle has downloaded, register the bundle entry in AppKernel.php:

// /app/AppKernel.php

class AppKernel extends Kernel
{
    public function registerBundles()
    {
        $bundles = [
            new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
            new Symfony\Bundle\SecurityBundle\SecurityBundle(),
            new Symfony\Bundle\TwigBundle\TwigBundle(),
            new Symfony\Bundle\MonologBundle\MonologBundle(),
            new Symfony\Bundle\SwiftmailerBundle\SwiftmailerBundle(),
            new Doctrine\Bundle\DoctrineBundle\DoctrineBundle(),
            new Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle(),

            new Knp\Bundle\PaginatorBundle\KnpPaginatorBundle(),

            new AppBundle\AppBundle(),
        ];

        // * snip *

I always follow this pattern. First I have all the bundles that were originally added when installing Symfony. This goes from FrameworkBundle down to SensioFrameworkExtraBundle.

I then put a new line or two, and add in any third party bundles - such as KnpPaginatorBundle. If I had other bundles added to the project, I would add them here in this second 'chunk', and for my own sanity, I like to sort them alphabetically also.

Lastly, I add in a third 'chunk' which typically these days only ever contains AppBundle. In the earlier days of Symfony 2.x, we would typically create many individual bundles in our own projects. This is no longer considered a good practice, and having made the change myself, I completely agree with this direction. If you are new to Symfony, you can ignore this entirely and consider it merely historical Symfony trivia.

Finally, we need to add some configuration to config.yml to ensure that KnpPaginatorBundle behaves itself:

# /app/config/config.yml

framework:
    #esi: ~
    translator: { fallbacks: ['%locale%'] } # uncomment this line

First up, the sneaky one. Make sure to uncomment the translator configuration entry. This simply means removing the hash (#) from the start of the line.

This one is not immediately documented on the KnpPaginatorBundle website.

What happens if you don't uncomment this? Well, when adding in the pagination display templates you will see label_previous instead of 'Previous', and label_next instead of 'Next'. It's a simple fix, and we've made it preemptively anyway.

Next, we need to add in some knp_paginator specific configuration. This is documented in the official docs. I'm going to start by copying everything over from their documentation. However, as we're using Bootstrap for styling, I'm going to change up the pagination template to use the Bootstrap-styled pagination component:

# /app/config/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

We don't actually need all this configuration though. We're accepting the defaults for everything except the template.pagination option. As a result, we could reduce our config down significantly:

# /app/config/config.yml

knp_paginator:
    template:
        pagination: 'KnpPaginatorBundle:Pagination:twitter_bootstrap_v3_pagination.html.twig'     # sliding pagination controls template

But you may be asking yourself - if we don't supply these options, where do these default options come from? Though not super intuitive, the defaults come as part of the bundle's configuration. This is a more advanced concept and not something you need to concern yourself with. But it's nice to know.

These extra options are not immediately useful to us anyway. We won't be concerning ourselves with sorting or filtering just yet.

Speaking of code, let's start writing some!

Using KnpPaginatorBundle In Our Code

The documentation lists a bunch of 'things' that the paginator can paginate over.

As we don't yet have a database-backed system, we are currently working with an array of data. Fortunately, KnpPaginatorBundle can work with arrays. Phew.

Now, there isn't an immediately available example on the official documentation as to how to use an array with the paginator. The example given is for paginating a Doctrine query. Don't be alarmed, working with an array is easier than the given example:

<?php

/src/AppBundle/Controller/DefaultController.php

namespace AppBundle\Controller;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;

class GalleryController extends Controller
{
    /**
     * @Route("/", name="gallery")
     */
    public function indexAction(Request $request)
    {
        $images = [
            'landscape-summer-beach.jpg',
            'landscape-summer-field.jpg',
            'landscape-summer-flowers.jpg',
            'landscape-summer-hill.jpg',
            'landscape-summer-mountain.png',
            'landscape-summer-sea.jpg',
            'landscape-summer-sky.jpg',
        ];

        $paginator  = $this->get('knp_paginator');
        $pagination = $paginator->paginate(
            $images,
            $request->query->getInt('page', 1) /*current page number*/,
            4 /*images per page*/
        );

        return $this->render('gallery/index.html.twig', [
            'images' => $pagination
        ]);
    }
}

Whilst in the previous video we worked with a single image which we duplicated multiple times to create our array entries, in this video I'm going to make things less repetitive by adding in a few different images. Conceptually however, the process is identical. In other words, so long as files with the given names exist in the /web/images directory we are good to go.

With an array of data, we now have something interesting we can paginate over.

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

We've added KnpPaginatorBundle as a dependency of our project. We've enabled it, and we've added a bit of config to make sure that when used, it behaves in-line with our expectations.

Now, we need to actually use it.

But what is 'it'?

Well, like most everything in our codebase, the paginator is an object.

We could new up an instance of the paginator ourselves which would involve making sure we provide all the right configuration options and then, ideally, extract this setup logic out so that if we want to use the paginator in other classes, we can call that shared location so we don't keep duplicating paginator setup everywhere.

But we don't need to do this.

At a high level, this is what the bundle is doing for us.

By adding the bundle and registering it with our Symfony application (via the entry in AppKernel.php from earlier), all of this hard work has been done for us. The paginator is available to us as a pre-configured Symfony service. All we need to do is get hold of that service, and start using it.

Whilst the documentation for any bundle should tell us exactly this kind of thing, if you are ever unsure what the service might be called, you can do a little code (or, in this case, configuration) diving and find out for yourself. Typically this file should live inside the bundle's Resources/config directory. The file name is not always the same, but there are rarely that many of them, so poking around in each isn't that difficult.

A quick glance inside the Resources/config/paginator.xml file reveals a bundle of service entries with id's starting with knp_paginator:

<!-- /vendor/knplabs/knp-paginator-bundle/Resources/config/paginator.xml -->

    <parameters>
        <parameter key="knp_paginator.class">Knp\Component\Pager\Paginator</parameter>
        <parameter key="knp_paginator.helper.processor.class">Knp\Bundle\PaginatorBundle\Helper\Processor</parameter>
    </parameters>

    <services>
        <service id="knp_paginator" class="%knp_paginator.class%">
            <argument type="service" id="event_dispatcher" />
        </service>

Knowing this, we can run a Symfony debug command against the container to find out more:

php bin/console debug:container knp_paginator

Information for Service "knp_paginator"
=======================================

 ------------------ -------------------------------
  Option             Value
 ------------------ -------------------------------
  Service ID         knp_paginator
  Class              Knp\Component\Pager\Paginator
  Tags               -
  Calls              setDefaultPaginatorOptions
  Public             yes
  Synthetic          no
  Lazy               no
  Shared             yes
  Abstract           no
  Autowired          no
  Autowiring Types   -
 ------------------ -------------------------------

To wrap up here, by calling get inside our GalleryController::indexAction, we are asking Symfony to look inside the container and find the service that has the id of knp_paginator:

use Symfony\Bundle\FrameworkBundle\Controller\Controller;

class GalleryController extends Controller
{
    /**
     * @Route("/", name="gallery")
     */
    public function indexAction(Request $request)
    {
        $images = [...];

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

get is available to us because we extends Controller, with Controller being Symfony\Bundle\FrameworkBundle\Controller\Controller.

If you use an IDE like PhpStorm, you can ctrl / cmd click the get method after which you should see:

    /**
     * Gets a container service by its id.
     *
     * @param string $id The service id
     *
     * @return object The service
     */
    protected function get($id)
    {
        return $this->container->get($id);
    }

Ok, so again, you don't need to know this stuff to use the paginator. In my opinion, it's just nice to demystify what the heck is happening behind the scenes, and to see where all this stuff is coming from.

Want to know more? Check out the official Symfony docs on the Service Container.

Paginating

We've gained access to the pagination service and assigned that to a variable named $paginator.

From that service we now want to call the paginate method.

As arguments to this method we need to provide:

  • The data to paginate over
  • The current 'page'
  • How many items to display per 'page'

To clarify, imagine we have a set of data with ten entries in it.

We want to split this into groups (or 'pages') of four.

10 / 4 doesn't quite go.

The paginator will handle this for us, splitting into chunks / groups / pages of 4, and then however many are left over on the last page.

In other words, we will get three pages.

The first page will have four items.

The second page will have the next four items.

The third and last page will have the last two items.

How would we write this in code?

$pagination = $paginator->paginate(
    $images, /* array of images */
    $request->query->getInt('page', 1), /*current page number*/
    4 /*images per page*/
);

This first and third argument are fairly self explanatory.

The second argument requires a touch more explanation.

We need a way to figure out which 'page' we are currently on. To do this, we can examine the current URI.

When we first load up the gallery page, our URI will look like this:

http://127.0.0.1:8000/gallery

Assuming we have 10 images in our result set ($images), then we should expect the paginator to have split the data into 3 pages.

We haven't yet added in the Twig template to allow this, but ignore this for the moment and think of this as the 'theory' portion.

To get the second page, we need a way to tell the controller that hey, we now want page 2.

We could do this by browsing to:

http://127.0.0.1:8000/gallery?page=2

The exact same controller action will be called. Only, this time when the page query parameter is inspected, the value of page will be 2:

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

In other words, from the current request, get me the integer value for the query parameter for page. If page is not set, default to 1.

Because of this default behaviour, that's why we don't need to initially browse to:

http://127.0.0.1:8000/gallery?page=1

But here's if we use the paginator component and browse back to page 1, then you will see the above URL is used.

If you'd like to know more about using the URL in this way, I have a free video that covers this topic in further depth.

One last thing, if you don't like the word page, then you can change it in the configuration we covered earlier:

# /app/config/config.yml

knp_paginator:
    default_options:
        page_name: chunk # page query parameter name

If you do this, be sure to update the code accordingly:

$pagination = $paginator->paginate(
    $images, /* array of images */
    $request->query->getInt('chunk', 1), /*current page number*/
    4 /*images per page*/
);

Let's See This In Action

Let's very quickly recap our controller action:

<?php

/src/AppBundle/Controller/DefaultController.php

namespace AppBundle\Controller;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;

class GalleryController extends Controller
{
    /**
     * @Route("/", name="gallery")
     */
    public function indexAction(Request $request)
    {
        $images = [
            'landscape-summer-beach.jpg',
            'landscape-summer-field.jpg',
            'landscape-summer-flowers.jpg',
            'landscape-summer-hill.jpg',
            'landscape-summer-mountain.png',
            'landscape-summer-sea.jpg',
            'landscape-summer-sky.jpg',
        ];

        $paginator  = $this->get('knp_paginator');
        $pagination = $paginator->paginate(
            $images,
            $request->query->getInt('page', 1) /*current page number*/,
            4 /*images per page*/
        );

        return $this->render('gallery/index.html.twig', [
            'images' => $pagination
        ]);
    }
}

Ok, so we have some data, it's been through the paginator, and now we need to show it to the site visitor:

return $this->render('gallery/index.html.twig', [
    'image' => $images
]);

We already have this Twig template:

{% extends 'base.html.twig' %}

{% block body %}

    <div class="row">
        {% for image in images %}
        <div class="col-sm-6 col-md-4">
            <div class="thumbnail">
                <img src="images/{{ image }}" alt="some text here">
                <div class="caption">
                    <h3>Thumbnail label</h3>
                    <p>...</p>
                    <p><a href="#" class="btn btn-primary" role="button">Button</a> <a href="#" class="btn btn-default" role="button">Button</a></p>
                </div>
            </div>
        </div>
        {% endfor %}
    </div>

{% endblock %}

All of this still stands. We just need to call the pagination helper function, and pass in the images:

{% extends 'base.html.twig' %}

{% block body %}

    <div class="row">
        {% for image in images %}
        <div class="col-sm-12 col-md-6">
            <div class="thumbnail">
                <img src="images/{{ image }}" alt="some text here">
                <div class="caption">
                    <h3>Thumbnail label</h3>
                    <p>...</p>
                    <p><a href="#" class="btn btn-primary" role="button">Button</a> <a href="#" class="btn btn-default" role="button">Button</a></p>
                </div>
            </div>
        </div>
        {% endfor %}
    </div>

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

Again, to be absolutely clear, knp_pagination_render is a Twig extension function made available to us because we are using the KnpPaginatorBundle.

Earlier we swapped out the default pagination template for the Bootstrap-styled one:

# /app/config/config.yml

knp_paginator:
    template:
        pagination: 'KnpPaginatorBundle:Pagination:twitter_bootstrap_v3_pagination.html.twig'     # sliding pagination controls template

It's at this point that this particular piece of configuration is actually used.

Anyway, there we go. We have implemented pagination over our images. Even though we have an array of images at this point, by using the KnpPaginatorBundle we have ensured that we won't need to do anything more drastic than change up the data we provide (a query instead of an array) to continue using this pagination setup once we get the database involved.

Code For This Course

Get the code for this course.

Code For This Video

Get the code for this video.

Episodes

# Title Duration
1 Introduction and Site Demo 02:14
2 Setup and a Basic Wallpaper Gallery 08:43
3 Pagination 08:24
4 Adding a Detail View 04:47
5 Creating a Home Page 11:14
6 Creating our Wallpaper Entity 07:50
7 Wallpaper Setup Command - Part 1 - Symfony Commands As a Service 05:57
8 Wallpaper Setup Command - Part 2 - Injection Is Easy 08:53
9 Wallpaper Setup Command - Part 3 - Doing It With Style 05:37
10 Doctrine Fixtures - Part 1 - Setup and Category Entity Creation 08:52
11 Doctrine Fixtures - Part 2 - Relating Wallpapers with Categories 05:56
12 EasyAdminBundle - Setup and Category Configuration 06:02
13 EasyAdminBundle - Wallpaper Setup and List View 07:46
14 EasyAdminBundle - Starting with Wallpaper Uploads 05:57
15 Testing with PhpSpec to Guide Our Implementation 03:39
16 Using PhpSpec to Test our FileMover 05:34
17 Symfony Dependency Testing with PhpSpec 08:47
18 Defensive Counter Measures 06:33
19 No Tests - Part 1 - Uploading Files in EasyAdminBundle 11:01
20 No Tests - Part 2 - Uploading Files in EasyAdminBundle 07:05
21 Don't Mock What You Don't Own 09:36
22 You've Got To Take The Power Back 07:36
23 Making Symfony Work For Us 08:56
24 Testing The Wallpaper File Path Helper 15:11
25 Finally, It Works! 14:56
26 Why I Prefer Not To Use Twig 16:51
27 Fixing The Fixtures 11:20
28 Untested Updates 14:30
29 Untested Updates Part Two - Now We Can Actually Update 06:33
30 Adding an Image Preview On Edit 12:31
31 Delete Should Remove The Wallpaper Image File 11:02
32 Getting Started Testing Wallpaper Updates 10:02
33 Doing The Little Before The Big 08:13
34 Tested Image Preview... Sort Of :) 07:36
35 Finishing Up With a Tested Wallpaper Update 10:41
36 Test Driven Wallpaper Delete - Part 1 11:06
37 Test Driven Wallpaper Delete - Part 2 11:57
38 EasyAdminBundle Login Form Tutorial 08:01