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) }}
</div>
</div>
... 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->setMaxPerPage(5);
$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.

Tip

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

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

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

babdev_pagerfanta:
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
$pagerfanta->setCurrentPage($page);
... 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!

18
Login or Register to join the conversation
Michael M. Avatar
Michael M. Avatar Michael M. | posted 1 year ago

I got a new job where symfony is highly used framework. Your symfony videos helped me understand it.
I still think doctrine is a little bit confusing (compared to laravel eloquent) but I think I can get used to it.
Thanks! :)

2 Reply

Congratulations Michael M.!
We can't be happier to hear our tutorials helped you landing a new job! Cheers!

2 Reply
Matěj V. Avatar
Matěj V. Avatar Matěj V. | posted 1 year ago | edited

Hello, I have a small problem with the final pagination translation. I want to translate the Previous and Next to czech, but making pagerfanta.cs.yaml didn't work, I've also found the translations included with pagerfanta so it should translate by itself, but it's not.
My services.yaml is: parameters: locale: 'en' and translation.yaml:

framework:
    default_locale: 'cs'
    translator:
        default_path: '%kernel.project_dir%/translations'
        fallbacks:
            - '%locale%'```

I tried to edit it in twig like this `{{ pagerfanta(pager)|trans({'%Previous%' : 'Previous', '%Next%' : 'Next'}, 'pagerfanta')|raw}}` but this worked only partly, I got message about missing translation which I haven't before, but it expects the whole list with html to be translated, not just the text. 
Any idea what I got wrong? Thanks!
1 Reply

Hey Matěj V.!

Hmmm. I do NOT know what's going on, but let's dig in a little bit. First, I'd clear your cache manually - JUST in case - bin/console cache:clear. At one time, Symfony had a bug where it wouldn't see new translation directories until you cleared cache. So, do this just in case.

When you refresh this page, and look down in the web debug toolbar, you should see a little translation icon. If you click that, it should show you the "missing translations". Do you see "Previous" and "Next" as missing translations? Or does it look like it IS finding those translations? Have you successfully translated other strings (just normal stuff in your own code) on your site? I'm asking in case there is some problem with the locale itself.

I'm not sure what's going on here, but it must be something minor: I agree that you should be getting automatic translations from the bundle, even if you did nothing else.

Cheers!

Reply
Matěj V. Avatar
Matěj V. Avatar Matěj V. | weaverryan | posted 1 year ago | edited

Thanks for the reply weaverryan !
Clearing cache unfortunately didn't help.
Yes I don't see the translation icon, it behaves like there's nothing to translate on cs locale, only when i add |trans, it expects the whole html block of the pagination, not just for previous or next.
I already did add some translations before, like the security one from the security tutorial here and they worked without a prob. Should I call it in twig similarly
{{ pagerfanta(pager)|trans(pager.???, 'pagerfanta') }}? But it's the whole object and nothing instead of the question marks seemed to fit.
I've found a solution by just exchanging the prev/next_message in the DefaultTemplate found in the pagerfanta vendor, but that's not an ideal one :D.

Reply

Hey Matěj V.!

> Yes I don't see the translation icon

You don't see the translation icon at all? What if you, somewhere else in the main template, try to translate some new, invented string that is not in a translation file? Do you see the translation icon THEN? And does it just show the ONE missing translation (for the key you just invented) or other things?

The fact that you have translated other strings tells me the translator is installed and working (this seems like an obvious point, but a likely tripping point is not having the translator installed, in which case you don't get an error, but the translation system doesn't do anything). And so, it seems as if the strings just aren't being sent through the translator. It's possible this is a bug. I checked the pagerfanta templates, and the strings ARE run through |trans, but the templates are complex, including some templates in the bundle that override those in the main library. It's possible something got messed up and the wrong templates are being used. To test that theory, you could tweak some core pagerfanta templates to see what happens.

For example, if you find this template and change Next to TEST - https://github.com/BabDev/P... - if everything is working correctly, you should NOT see a change. That's because this template is overridden (or should be) by the bundle. IF you DO see a change, then there's a bug with how the bundle is not overriding this template. Here is the template that SHOULD be used, and where you can see the |trans filter: https://github.com/BabDev/P...

Cheers!

Reply
Tomáš K. Avatar
Tomáš K. Avatar Tomáš K. | weaverryan | posted 1 year ago | edited

Hi weaverryan !

I've been dealing with the exact same issue and I actually found the reason why is it not translating (and even not showing defined messages). The problem is in the bundle configuration of the <b>default_view</b>.

In the video, you are setting the value to <b>twitter_bootstrap5</b>. This resolves as pagerfanta core <b>\PagerFanta\View\TwitterBootstrap5View</b> class which uses <b>\PagerFanta\View\Template\TwitterBootstrap5Template</b>. Its method <b>getDefaultOptions</b> extends array from parent where both <b>prev_message</b> and <b>next_message</b> are hardcoded.

To render <b>pagerfanta/twig</b> templates you should use following configuration:


babdev_pagerfanta:
    default_view: twig
    default_twig_template: '@BabDevPagerfanta/twitter_bootstrap5.html.twig'

This uses <b>\PagerFanta\Twig\View\TwigView</b> which directly renders all the templates that resolve translations and these also show up in the web debug toolbar. The template can be specified using <b>default_twig_template</b> option.

Anyway, thanks a lot for the great work with the casts, this package always seemed too complex to me and thanks to this video I find it as easy as a breeze.

1 Reply

Great job!! I begun appliying the knowledge: a budget control for a construction company. I got a paginnated page when I edit something there would like to return to the same page I was, haven't been able to find a way to do it, any ideas

Reply

Hey alcb1310

In your Edit controller you can just return a Redirect reponse to the route he's supposed to return. If that's not useful for you, perhaps you can use the request referer

Cheers!

Reply

thanks, used the request referer and was a success

2 Reply
davidmintz Avatar
davidmintz Avatar davidmintz | posted 1 year ago

Great job once again on this toot. I have begun applying this knowledge to a personal project: a scheduling and billing system for my wife's psychology practice. Ambitious, I know.

Reply

Hey davidmintz

Thanks for the feedback, we are happy to know that you have where to apply knowledge!

Cheers!

Reply
Kevin Avatar

Hi, I still get the `Unknown "pagerfanta" function.` error after getting pagerfanta/twig. I am using the same version(3.3). Are there additional setup that I need to do? I saw this in the docs but am unsure: https://www.babdev.com/open...

Reply

Hey Ccc123!

Hm, what did you do? Are you trying to call pagerfanta() Twig function in your template? If so, it looks like the bundle isn't installed/enabled in your project. Make sure you installed it via Composer, Symony Flex should enable it for you in bundles.php files.

Cheers!

Reply

Hi Víctor, I have the same problem, I've been searching about the problem and I found that I need to add a bundle in bundles.php but what bundle I need to add?

Reply

Hey Temolzin,

First of all, what bundle did you install? :) I suppose you installed https://github.com/BabDev/PagerfantaBundle ? If so, please, make sure it's enabled in your bundles.php file. Specifically, check the docs: https://www.babdev.com/open-source/packages/pagerfantabundle/docs/3.x/installation

Also, try to clear the cache just in case :)

I hope this helps!

Cheers!

Reply
Kevin Avatar
Kevin Avatar Kevin | Victor | posted 1 year ago | edited

Hi Victor,

I followed the videos to the point where {{ pagerfanta(pager) }} was added to the template and then I did composer require pagerfanta/twig

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

This tutorial also works great for Symfony 6!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=8.1",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "babdev/pagerfanta-bundle": "^3.3", // v3.3.0
        "composer/package-versions-deprecated": "^1.11", // 1.11.99.3
        "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
    }
}
userVoice