Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine


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

Eventually, this page is going to get super long. By the time we have a thousand mixes, it probably won't even load! We can fix this by adding pagination. Does Doctrine have the ability to paginate results? It does! Though, I usually install another library that adds more features on top of those from Doctrine.

Find your terminal and run:

composer require babdev/pagerfanta-bundle pagerfanta/doctrine-orm-adapter

This installs a Pagerfanta bundle, which is a wrapper around a really nice library called Pagerfanta. Pagerfanta can paginate lots of things, like Doctrine results, results from Elasticsearch, and much more. We also installed its Doctrine ORM adapter, which will give us everything we need to paginate our Doctrine results. In this case, when we run

git status

it added a bundle, but the recipe didn't need to do anything else. Cool! So how does this library work?

Open up src/Controller/VinylController and find the browse() action. Instead of querying for all of the mixes, like we're doing now, we're going to tell the Pagerfanta library which page the user is currently on, how many results to show per page, and then it will query for the correct results for us.

Returning a QueryBuilder

To get this working, instead of calling findAllOrderedByVotes() and getting back all of the results, we need to call a method on our repository that returns a QueryBuilder. Open src/Repository/VinylMixRepository and scroll down to findAllOrderedByVotes(). We're only using this method right here at the moment, so rename it to createOrderedByVotesQueryBuilder()... and this will now return a QueryBuilder - the one from Doctrine ORM. I'll remove the PHP documentation on top... and the only thing we need to do down here is remove getQuery() and getResult() so that we're just returning $queryBuilder.

... lines 1 - 6
use Doctrine\ORM\QueryBuilder;
... lines 8 - 17
class VinylMixRepository extends ServiceEntityRepository
... lines 20 - 42
public function createOrderedByVotesQueryBuilder(string $genre = null): QueryBuilder
... lines 45 - 51
return $queryBuilder;
... lines 54 - 70

Over in VinylController, change this to $queryBuilder = $mixRepository->createOrderedByVotesQueryBuilder($slug)

... lines 1 - 12
class VinylController extends AbstractController
... lines 15 - 38
public function browse(VinylMixRepository $mixRepository, string $slug = null): Response
... lines 41 - 42
$queryBuilder = $mixRepository->createOrderedByVotesQueryBuilder($slug);
... lines 44 - 54

Initializing Pagerfanta is two lines. First, create the adapter - $adapter = new QueryAdapter() and pass it $queryBuilder. Then create the Pagerfanta object with $pagerfanta = Pagerfanta::createForCurrentPageWithMaxPerPage()

That's a mouthful. Pass this the $adapter, the current page - right now, I'm going to hardcode 1 - and finally the max results per page that we want. Let's use 9 since our mixes show up in three columns.

... lines 1 - 5
use Pagerfanta\Doctrine\ORM\QueryAdapter;
use Pagerfanta\Pagerfanta;
... lines 8 - 12
class VinylController extends AbstractController
... lines 15 - 38
public function browse(VinylMixRepository $mixRepository, string $slug = null): Response
... lines 41 - 43
$adapter = new QueryAdapter($queryBuilder);
$pagerfanta = Pagerfanta::createForCurrentPageWithMaxPerPage(
... lines 50 - 54

Now that we have this Pagerfanta object, we're going to pass that into the template instead of mixes. Replace this with a new variable called pager set to $pagerfanta.

... lines 1 - 38
public function browse(VinylMixRepository $mixRepository, string $slug = null): Response
... lines 41 - 50
return $this->render('vinyl/browse.html.twig', [
... line 52
'pager' => $pagerfanta,
... lines 56 - 57

The cool thing about this $pagerfanta object is that you can loop over it. And as soon as you do, it will execute the correct query to get just this pages results. In templates/vinyl/browse.html.twig, instead of {% for mix in mixes %}, say {% for mix in pager %}.

... lines 1 - 2
{% block body %}
... lines 4 - 27
<div class="row">
{% for mix in pager %}
... lines 30 - 44
{% endfor %}
... lines 47 - 48
{% endblock %}

That's it. Each result in the loop will still be a VinylMix object.

If we go over and reload... got it! It shows nine results: the results for Page 1!

Linking to the Next Page

What we need now are links to the next and previous pages... and this library can help with that too. Back at your terminal, run:

composer require pagerfanta/twig

One of the trickiest things about the Pagerfanta library is, instead of it being one giant library that has everything you need, it's broken down into a bunch of smaller libraries. So if you want the ORM adapter support, you need to install it like we did earlier. If you want Twig support for adding links, you need to install that too. Once you do though, it's pretty simple.

Back in our template, find the {% endfor %}, and right after, say {{ pagerfanta() }}, passing it the pager object.

... lines 1 - 2
{% block body %}
... lines 4 - 26
<h2 class="mt-5">Mixes</h2>
<div class="row">
... lines 29 - 46
{{ pagerfanta(pager) }}
... lines 49 - 50
{% endblock %}

Check it out! When we refresh... we have links at the bottom! They're... ugly, but we'll fix that in a minute.

Reading the Current Page

If you click the "Next" link, up in our URL, we see ?page=2. Though... the results don't actually change. We're still seeing the same results from Page 1. And... that makes sense. Remember, back in VinylController, I hardcoded the current page to 1. So even though we have ?page=2 up here, Pagerfanta still thinks we're on Page 1.

What we need to do is read this query parameter and pass it as this second argument. No problem! How do we read query parameters? Well, that's information from the request, so we need the Request object.

Right before our optional argument, add a new $request argument type-hinted with Request: the one from HttpFoundation. Now, down here, instead of 1, say $request->query (that's how you get query parameters), with ->get('page')... and default this to 1 if there is no ?page= on the URL.

... lines 1 - 8
use Symfony\Component\HttpFoundation\Request;
... lines 10 - 13
class VinylController extends AbstractController
... lines 16 - 39
public function browse(VinylMixRepository $mixRepository, Request $request, string $slug = null): Response
... lines 42 - 45
$pagerfanta = Pagerfanta::createForCurrentPageWithMaxPerPage(
... line 47
$request->query->get('page', 1),
... line 49
... lines 51 - 55

By the way, if you want, you can also add {page} up here. This way, Pagerfanta will automatically put the page number inside the URL instead of setting it as a query parameter.

If we head over and refresh... right now, we have ?page=2. Down here... it knows we're on Page 2! If we go to the next page... yes! We see a different set of results!

Though, this is still super ugly. Fortunately, the bundle does give us a way to control the markup that's used for the pagination links. And it even comes with automatic support for Bootstrap CSS-friendly markup. We just need to tell the bundle to use that.

So... we need to configure the bundle. But... the bundle didn't give us any new config files when it was installed. That's okay! Not all new bundles give us config files. But as soon as you need one, create one! Since this bundle's called BabdevPagerfantaBundle, I'm going to create a new file called babdev_pagerfanta.yaml. As we learned in the last tutorial, the name of these files aren't important. What's important is the root key, which should be babdev_pagerfanta. To change how the pagination renders, add default_view: twig and then default_twig_template set to @BabDevPagerfanta/twitter_bootstrap5.html.twig.

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

Like any other config, there's no way you would know that this is the correct configuration just by guessing. You need to check out the docs.

If we go back and refresh... huh, nothing changed. This is a little bug that you sometimes run into in Symfony when you create a new configuration file. Symfony didn't notice it... and so it didn't know it needed to rebuild its cache. This is a super rare situation, but if you ever think it might be happening, it's easy enough to manually clear the cache by running:

php bin/console cache:clear

And... oh... it explodes. You probably noticed why. I love this error!

There is no extension able to load the configuration for "baberdev_pagerfanta"

It's supposed to be babdev_pagerfanta. Whoops! And now... perfect! It's happy. And when we refresh... it sees it! In a real project, we'll probably want to add some extra CSS to make this "dark mode"... but we've got it.

Okay team, we're basically done! As a bonus, we're going to refactor this pagination into a JavaScript-powered forever scroll... except plot twist! We're going to do that without writing a single line of JavaScript. That's next.

Leave a comment!

Login or Register to join the conversation
Akili Avatar

Hey SymfonyCast:

My Windows 10 machine was hit with the dreaded OS_BOOT_FAILURE resulting in the loss of a lot of my data. All my files and folders I used for this mixed_vinyl tutorial were stored inside V.S. Code's file explorer and wiped clean. My mixed_vinyl tutorial was stored in a mixed_vinyl_tutorial Github repo I created. As a result of the OS_BOOT_FAILURE, I lost my administrative access privileges to my mixed_vinyl_tutorial repo. I tried cloning my repo, but discovered I don't have my original administrative rights and privileges as the owner of the repo would have. I was wondering if SynfonyCast would be able to help me figure this out. Is there a way for me get my administrative privileges back without having to start over from the beginning or creating a new repository? Can I transfer all the code that I stored in my Github repo back to my V.S. Code file explorer and regain ownership of my original administrative privileges? This would be a lifesaver for me and would save me a lot of time! if you have more questions please contact me via this thread or email thanks!


Hey Akili,

I'm sorry to hear you lost your data! That's the worst :/ Unfortunately, the SymfonyCasts support team does not have the bandwidth to solve customer's personal project issues. Moreover, we don't have specialists who could help you with such tasks, unfortunately. Most of our developers are using Mac/Linux OS. You probably need to find a recovery data company locally that would help with it, you would need to bring your physical HDD there I suppose. If you had some backups on GitHub but you lost access to it now - I think you need to contact GitHub support directly to ask about some ways to recover, I think they may help you with it too.

I hope that helps!


Akili Avatar
Akili Avatar Akili | Victor | posted 3 months ago | edited

@Victor :

Thank you for your timely response Victor. communicating with githubs support team will be too much of a rabbit hole to crawl through :/ I don't want to waste more time than I already have. It looks like I have not other choice but to revisit the whole course a second time. I'll look at it as a refresher course.


Hey Akili,

Well, re-watching the course is always a good idea as it will help you to remember the material better, and probably you will notice some minor details you have missed in the first tun.

Unless you lost some important unique data that you need to recover - you can totally ignore learning course projects, not a big deal probably.



Thanks for this awesome tutorial. I am wondering if there is any way to adapte Pagerfanta with Twig orders. (I know how to custmize it by QueryBuilder)
For example:
Let's say I need to use "If statements" like this:

{% for activity in pager %}  
	{% if activity.published == true %}
		bla bla bla ...
	{% endif %}
{% endfor %}

In this case, Twig still counts the non-published activities, it only hides it.

Many thanks in advance!


Hi @Lubna!

Interesting! You want to hide certain results, but still want them to be counted in the total item count and also in the pagination? What I mean is, if you are showing 10 items per page, and there are 100 items, but only 50 are published, you would still want Pagerfanta to show 10 pages. Is that correct?

If so, I might be missing some detail. The code you showed above looks great, of course: you are able to loop over the 10 items on the page and use an if statement to hide (for example) 5 of them. Then, naturally, when you use pagerfanta, it will not know about this, so it will still think there are 10 pages. I'm guessing this isn't quite what you want - but let me know - I'm missing a piece to your requirement!

Sorry I couldn't be more immediately helpful - but let me know what detail I'm missing :).


1 Reply

Thanks @weaverryan for your reply and appreciate your time!

Not exactly, when I'm showing 10 items per page, and there are 100 items, but only 50 are published, Pagerfanta still show 10 pages.

Seems there is no solution, because Pagerfanta can't control the data reicieved of QuieryBuilder.
Have a nice day :)


Hi @Lubna!

I understand! Yes, the solution for this would always need to be to "go back and modify the original query" - which I know you were already aware of :).

Good luck!

1 Reply

Hey there,

I have on single page two lists of items (for example active and archive messages). I need to have pagination for both of lists. But the parameter page is in collision with each other.

How I can specify for each pagerfanta name of parameter for pagination? For example to have activePage parameter for active messages parameter and archivePage for archived messages? So URL should be .../messages/list?activePage=2&archivePage=4 - this means that active messages are at page 2 and archive messages are at page 4.

Thanks everyone for advices.


Hey skocdopolet!

That's an excellent question! Here's how to do this, on your template:

{{ pagerfanta(pager, 'default', {'pageParameter': '[activePage]'}) }}

Ref: bottom of https://www.babdev.com/open-source/packages/pagerfantabundle/docs/3.x/rendering-pagerfantas

Then, of course, you'll read $request->query->get('activePage') in your controller when fetching the current page.

I hope this helps!

1 Reply

Hey weaverryan!

Thank you for your reply. Its worked for me very well!

I have another question. Is it possible to omit prev and next links while rendering pagerfanta? I need only the page numbers...

I am searching in the documentation and I was try to inspect source code too, but I was not successfull.

I solve this requirement by CSS and display:none, but I am searching better solution.



Hey skocdopolet!

Yea... interesting question. As best I can see, this is not supported by a simple option. Instead, I think you'll need to override the pagination template. So, for example, in this tutorial we're using the @BabDevPagerfanta/twitter_bootstrap5.html.twig template, which should be this one: https://github.com/BabDev/Pagerfanta/blob/3.x/lib/Twig/templates/twitter_bootstrap5.html.twig... which really just extends this one: https://github.com/BabDev/Pagerfanta/blob/3.x/lib/Twig/templates/twitter_bootstrap4.html.twig

Anyways, I believe if you just copied that template into your code, customized it, then updated the pagerfanta config to point at it, you'd be in business. Specifically, you would override all of the previous_page_link and next_page_link blocks (including the disabled versions) and render nothing.

I'd love to know if this works, so let me know if you have a chance to try it!


2 Reply
ssi-anik Avatar
ssi-anik Avatar ssi-anik | posted 1 year ago

You can use ($page = intval($request->query->get('page', 1))) > 0 ? $page : 1 to make sure that page always an integer otherwise the first page. Because the type for the parameter $currentPage (the second argument) is set to int and it'll break your code and throw 500.


Hey there,

Yes, is a good idea to make sure that you get an int out of the request param, and there's a new fancy way to do that in a modern Symfony app
$request->query->getInt('page', 1);


3 Reply
Cat in space

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

What PHP libraries does this tutorial use?

// composer.json
    "require": {
        "php": ">=8.1",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "babdev/pagerfanta-bundle": "^3.7", // v3.7.0
        "doctrine/doctrine-bundle": "^2.7", // 2.7.0
        "doctrine/doctrine-migrations-bundle": "^3.2", // 3.2.2
        "doctrine/orm": "^2.12", // 2.12.3
        "knplabs/knp-time-bundle": "^1.18", // v1.19.0
        "pagerfanta/doctrine-orm-adapter": "^3.6", // v3.6.1
        "pagerfanta/twig": "^3.6", // v3.6.1
        "sensio/framework-extra-bundle": "^6.2", // v6.2.6
        "stof/doctrine-extensions-bundle": "^1.7", // v1.7.0
        "symfony/asset": "6.1.*", // v6.1.0
        "symfony/console": "6.1.*", // v6.1.2
        "symfony/dotenv": "6.1.*", // v6.1.0
        "symfony/flex": "^2", // v2.2.2
        "symfony/framework-bundle": "6.1.*", // v6.1.2
        "symfony/http-client": "6.1.*", // v6.1.2
        "symfony/monolog-bundle": "^3.0", // v3.8.0
        "symfony/proxy-manager-bridge": "6.1.*", // v6.1.0
        "symfony/runtime": "6.1.*", // v6.1.1
        "symfony/twig-bundle": "6.1.*", // v6.1.1
        "symfony/ux-turbo": "^2.0", // v2.3.0
        "symfony/webpack-encore-bundle": "^1.13", // v1.15.1
        "symfony/yaml": "6.1.*", // v6.1.2
        "twig/extra-bundle": "^2.12|^3.0", // v3.4.0
        "twig/twig": "^2.12|^3.0" // v3.4.1
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.2
        "symfony/debug-bundle": "6.1.*", // v6.1.0
        "symfony/maker-bundle": "^1.41", // v1.44.0
        "symfony/stopwatch": "6.1.*", // v6.1.0
        "symfony/web-profiler-bundle": "6.1.*", // v6.1.2
        "zenstruck/foundry": "^1.21" // v1.21.0