Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Themed Pagination Links

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Pagerfanta is now controlling the query for this page and returning only the first 5 results. So... how do we get to page 2? How can we render some pagination links?

Pagerfanta makes this delightfully easy. Scroll down. After the endfor, render the pagination links with {{ pagerfanta(pager) }}.

... lines 1 - 9
<div class="container">
... lines 11 - 15
<div class="row">
{% for question in pager %}
... lines 18 - 47
{% endfor %}
{{ pagerfanta(pager) }}
... lines 53 - 55

Let's try it! Refresh and... bah!

Unknown pagerfanta() Function

Installing the Pagerfanta Twig Library

This is another feature that's a - sort of- "plugin" for Pagerfanta. Click back to the library's docs and go to "Installation and set up". It lists a bunch of different adapters... and also one other special package if you want Twig support for Pagerfanta. Copy that package name, find your terminal, and install it:

composer require pagerfanta/twig

Once that finishes, try the homepage again. This time... it works! Those links are pretty ugly... but we'll fix that in a minute.

Setting the Current Page

If you hover over the links, each adds a different ?page=. There's 4 pages because we have 20 total questions. So these links are smart: they render the correct number based on how many results we have and how many we're showing per page.

Go to page 2. Hmm... I think this is actually the same results as page 1. And if we look down at the links... even though you see ?page=2 on the URL, Pagerfanta still highlights that we're on page 1. Why?

Because... we need to help Pagerfanta know which page we're on: we need to read the ?q= and pass it to Pagerfanta.

Back in the controller, to read the query parameter, we need the request object. Add a $request argument type-hinted with the Request class from HttpFoundation. Then, below, add $pagerfanta->setCurrentPage() passing $request->query->get('page', 1) so that this returns 1 if there is no ?q= on the URL.

... lines 1 - 17
class QuestionController extends AbstractController
... lines 20 - 32
public function homepage(QuestionRepository $repository, Request $request)
... lines 35 - 36
$pagerfanta = new Pagerfanta(new QueryAdapter($queryBuilder));
$pagerfanta->setCurrentPage($request->query->get('page', 1));
... lines 40 - 43
... lines 45 - 86

One small word of warning. At the time of recording, you can't switch these two lines. You need to set the max for the page and then the current page. If you swap them, weird things happen. This may get fixed, but to be safe, put the lines in this order.

Anyways, when we refresh now... beautiful! It sees that we're on page 2... and the results look different. If we go to page 3... that works too! Woo!

Customizing the Pagerfanta "View"

So let's talk about making these links prettier. You can totally customize them as much as you want, including with a custom template. But there are several built-in, sort of "themes"... including one for Bootstrap 5.

Back on the bundle documentation, click on "Default Configuration". This bundle has a default_view key... and one of the built-in views is called twitter_bootstrap5.

So... where do we make this config change? When we installed the bundle, it did not create a configuration file. And... that's fine! The bundle works great with the default config, so the author chose not to ship a config file. So now that we do want to configure the bundle, we'll create one ourselves.

Copy this babdev_pagerfanta key. Then, in config/packages/, create a new file called babdev_pagerfanta.yaml. Now technically, this file could be called anything: there's no significance to the filenames in this directory. But this name makes sense.

Inside, paste the root key, then set default_view: to twitter_bootstrap5. Before recording, I dug into the documentation to discover that this is one of the valid values.


If you need your pagination links to be translated, try this config in pagerfanta.yaml:

# config/packages/pagerfanta.yaml
    default_view: twig
    default_twig_template: '@BabDevPagerfanta/twitter_bootstrap5.html.twig'

Hat-tip to Tomas in the comments for figuring that out!

default_view: twitter_bootstrap5

Let's check it! Refresh and... huh... nothing changes: it's still rendering exactly like before. I wonder if Symfony didn't see my new config file. Let's manually clear the cache to be sure:

php bin/console cache:clear

Refresh again and... got it! You should not normally need to clear Symfony's cache while developing... that's super rare. But if you're ever not sure, it's safe to try. The point is, this now renders with Bootstrap 5 markup and it looks much better.

Putting the {page} Into the Route

Let's try one more thing. What if, instead of having ?page=2 on the URL, we wanted a URL like /2. So where the page is inside the main part of the URL.

That's... no problem. Over in QuestionController, add a new {page} to the URL. Now we need to be very careful because this is a wildcard. And so, if there are any other URLs on the site that are just /something, this route could break those if it matches first.

To avoid that, let's make this route only match if the {page} part of the URL is a number. Do that by adding a requirement - <> - with a regular expression inside: \d+.

So: only match this route if {page} is a digit of any length. If we go to /foo, this route won't match. Give the controller an int $page argument and default it to 1. This will allow the user to go to just /... and $page will be 1.

... lines 1 - 17
class QuestionController extends AbstractController
... lines 20 - 29
* @Route("/{page<\d+>}", name="app_homepage")
public function homepage(QuestionRepository $repository, int $page = 1)
... lines 35 - 43
... lines 45 - 86

Below, pass the $page variable in directly. And... we don't need the request object at all anymore.

... lines 1 - 17
class QuestionController extends AbstractController
... lines 20 - 29
* @Route("/{page<\d+>}", name="app_homepage")
public function homepage(QuestionRepository $repository, int $page = 1)
... lines 35 - 38
... lines 40 - 43
... lines 45 - 86

Phew! Let's try it! Refresh. It jumped back to page 1 because we're not reading the page from the query parameter anymore. Click page 2. Yes! It's /2... then /3! So cool!

Ok team! Congratulations on finishing both Doctrine courses! Big team high five! Doctrine is one of the most important parts of Symfony and it will unlock you for almost anything else you do. So let us know what cool stuff you're building and, if you have any questions or ideas, we're here for you down in the comments.

Alright friends, seeya next time!

Leave a comment!

This tutorial also works great for Symfony 6!

What PHP libraries does this tutorial use?

// composer.json
    "require": {
        "php": "^7.4.1 || ^8.0.0",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "babdev/pagerfanta-bundle": "^3.3", // v3.3.0
        "composer/package-versions-deprecated": "^1.11", //
        "doctrine/doctrine-bundle": "^2.1", // 2.4.2
        "doctrine/doctrine-migrations-bundle": "^3.0", // 3.1.1
        "doctrine/orm": "^2.7", // 2.9.5
        "knplabs/knp-markdown-bundle": "^1.8", // 1.9.0
        "knplabs/knp-time-bundle": "^1.11", // v1.16.1
        "pagerfanta/doctrine-orm-adapter": "^3.3", // v3.3.0
        "pagerfanta/twig": "^3.3", // v3.3.0
        "sensio/framework-extra-bundle": "^6.0", // v6.2.1
        "stof/doctrine-extensions-bundle": "^1.4", // v1.6.0
        "symfony/asset": "5.3.*", // v5.3.4
        "symfony/console": "5.3.*", // v5.3.7
        "symfony/dotenv": "5.3.*", // v5.3.7
        "symfony/flex": "^1.3.1", // v1.17.5
        "symfony/framework-bundle": "5.3.*", // v5.3.7
        "symfony/monolog-bundle": "^3.0", // v3.7.0
        "symfony/runtime": "5.3.*", // v5.3.4
        "symfony/stopwatch": "5.3.*", // v5.3.4
        "symfony/twig-bundle": "5.3.*", // v5.3.4
        "symfony/validator": "5.3.*", // v5.3.14
        "symfony/webpack-encore-bundle": "^1.7", // v1.12.0
        "symfony/yaml": "5.3.*", // v5.3.6
        "twig/extra-bundle": "^2.12|^3.0", // v3.3.1
        "twig/string-extra": "^3.3", // v3.3.1
        "twig/twig": "^2.12|^3.0" // v3.3.2
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.3", // 3.4.0
        "symfony/debug-bundle": "5.3.*", // v5.3.4
        "symfony/maker-bundle": "^1.15", // v1.33.0
        "symfony/var-dumper": "5.3.*", // v5.3.7
        "symfony/web-profiler-bundle": "5.3.*", // v5.3.5
        "zenstruck/foundry": "^1.1" // v1.13.1