If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeEventually, 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.
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( | |
$adapter, | |
1, | |
9 | |
); | |
... 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 %} | |
</div> | |
... 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!
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) }} | |
</div> | |
... 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.
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
.
babdev_pagerfanta: | |
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.
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 :).
Cheers!
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!
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.
Cheers!
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!
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.
Cheers!
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!
Cheers!
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);
Cheers!
// 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
}
}
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:
In this case, Twig still counts the non-published activities, it only hides it.
Many thanks in advance!