This course is still being released! Check back later for more chapters.

Get Notified About this Course!

We will send you messages regarding this course only
and nothing else, we promise.
You can unsubscribe anytime by emailing us at:
privacy@symfonycasts.com
Login to bookmark this video
Buy Access to Course
09.

Pagination

|

Share this awesome video!

|

Keep on Learning!

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

Login Subscribe

Foundry helped us add 20 ships. That makes our app look more realistic. But on production, we might have thousands of starships. This page would be gigantic and unusable. It'd probably also take a long time to load, time during which we are likely to be assimilated!

The solution? Paginate the results: show a few at a time - or per page.

Install Pagerfanta

To do this, we'll use a library called Pagerfanta - what a cool name! It's a generic pagination library but has great Doctrine integration! Add the two required packages:

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

Scroll up to see what this installed. pagerfanta/doctrine-orm-adapter is the glue between Pagerfanta and Doctrine.

Paginate a Query

On our homepage, we're using findIncomplete() from StarshipRepository. Open that up and find the method. Change the return type to Pagerfanta: an object with pagination-related superpowers. But you can loop over this object like an array, so leave the docblock as is:

67 lines | src/Repository/StarshipRepository.php
// ... lines 1 - 14
class StarshipRepository extends ServiceEntityRepository
{
// ... lines 17 - 21
/**
* @return Starship[]
*/
public function findIncomplete(): Pagerfanta
{
// ... lines 27 - 34
}
// ... lines 36 - 65
}

Now, a super important thing to remember when paginating a query is to have a predictable order. Add ->orderBy('e.arrivedAt', 'DESC'):

67 lines | src/Repository/StarshipRepository.php
// ... lines 1 - 14
class StarshipRepository extends ServiceEntityRepository
{
// ... lines 17 - 24
public function findIncomplete(): Pagerfanta
// ... lines 26 - 28
->orderBy('s.arrivedAt', 'DESC')
// ... lines 30 - 34
}
// ... lines 36 - 65
}

But instead of returning, add this to a variable called $query, then remove getResult(): our job changes from executing the query to simpy building it. Pagerfanta will handle the actual execution. Return new Pagerfanta(new QueryAdapter($query)) and be sure to import these two classes:

67 lines | src/Repository/StarshipRepository.php
// ... lines 1 - 14
class StarshipRepository extends ServiceEntityRepository
{
// ... lines 17 - 24
public function findIncomplete(): Pagerfanta
// ... line 26
$query = $this->createQueryBuilder('s')
// ... lines 28 - 30
->getQuery()
;
// ... line 33
return new Pagerfanta(new QueryAdapter($query));
}
// ... lines 36 - 65
}

Configure the Page

Back in MainController, $ship is now a Pagerfanta object. To use it, we need to tell it 2 things: how many ships we want on each page - $ships->setMaxPerPage(5) - and which page the user is currently on: use $ships->setCurrentPage(1) for now. Oh and make sure to call setCurrentPage() after setMaxPerPage() or weird time travel stuff will happen:

28 lines | src/Controller/MainController.php
// ... lines 1 - 12
public function homepage(
// ... line 14
): Response {
$ships = $repository->findIncomplete();
$ships->setMaxPerPage(5);
$ships->setCurrentPage(1);
// ... lines 19 - 25
}
}

Move over... refresh... and look! We're only showing 5 items: the first page.

Back over change to setCurrentPage(2):

28 lines | src/Controller/MainController.php
// ... lines 1 - 12
public function homepage(
// ... line 14
): Response {
// ... lines 16 - 17
$ships->setCurrentPage(2);
// ... lines 19 - 25
}
}

and refresh again.

Still 5 ships, but different ships: the second page. Let's peek at the query. There are multiple! One to count the total number of results and another to fetch only the ones for this page. Pretty darn cool.

Instead of hardcoding the page to 1 or 2 - a temporary and lame solution - let's read it dynamically from the URL, like with?page=1 or ?page=2.

Current Page from Request

To do that, autowire Request $request - the one from HttpFoundation - and change the setCurrentPage() argument to $request->query->get('page', 1) to read that value and default to 1 if it's missing:

30 lines | src/Controller/MainController.php
// ... lines 1 - 10
class MainController extends AbstractController
{
// ... line 13
public function homepage(
// ... line 15
Request $request,
): Response {
// ... lines 18 - 19
$ships->setCurrentPage($request->query->get('page', 1));
// ... lines 21 - 27
}
}

Head back over and refresh. This is page 1 because there is no page param. Add ?page=2 to the URL and... we're on page 2!

Ok, what else would be cool? How about showing the total number of ships, total number of pages, and the current page number?

Display Pagination Info

Back in the controller, Cmd + Click homepage.html.twig to open that up.

Put this info below the <h1>. I'll change the bottom margin and add a new <div> (with a bit of styling). Inside, write {{ ships.nbResults }}. Then: Page {{ ships.currentPage }} of {{ ships.nbPages }}:

61 lines | templates/main/homepage.html.twig
// ... lines 1 - 4
{% block body %}
<main class="flex flex-col lg:flex-row">
// ... lines 7 - 8
<div class="px-12 pt-10 w-full">
<h1 class="text-4xl font-semibold mb-3">
// ... line 11
</h1>
// ... line 13
<div class="text-slate-400 mb-4">
{{ ships.nbResults }} ships (Page {{ ships.currentPage }} of {{ ships.nbPages }})
</div>
// ... lines 17 - 57
</div>
</main>
{% endblock %}

Spin back over and refresh. Perfect! We have 14 total incomplete ships, and we're on page 1 of 3. Your numbers may vary depending on how many of your 20 ships are randomly set to an incomplete status.

Ok! What's missing? How about some links to navigate between pages? Below the list, I'm going to paste in some code. First, if ships.haveToPaginate: no links needed if there is only one page. Then, if ships.hasPreviousPage, lets add a link to the previous page if one exists, there wouldn't be a previous page if we're on page 1. Inside, generate a URL to this page: app_homepage. But pass a parameter: page set to ships.getPreviousPage. Since page isn't defined in the route, it'll be added as a page query parameter. That's exactly what we want! Repeat for the Next link: if ships.hasNextPage and ships.getNextPage:

72 lines | templates/main/homepage.html.twig
// ... lines 1 - 4
{% block body %}
<main class="flex flex-col lg:flex-row">
// ... lines 7 - 8
<div class="px-12 pt-10 w-full">
// ... lines 10 - 51
</div>
// ... line 53
{% if ships.haveToPaginate %}
<div class="flex justify-around mt-3 underline font-semibold">
{% if ships.hasPreviousPage %}
<a href="{{ path('app_homepage', {page: ships.getPreviousPage}) }}">&lt; Previous</a>
{% endif %}
{% if ships.hasNextPage %}
<a href="{{ path('app_homepage', {page: ships.getNextPage}) }}">Next &gt;</a>
{% endif %}
</div>
{% endif %}
// ... lines 64 - 68
</div>
</main>
{% endblock %}

Refresh, scroll down, and sweet! We see a Next link! Click it... and now we're on page 2 of 3, and the URL has ?page=2. Below, our widget has both Previous and Next links. Click Next again... page 3 of 3, then Previous, back to page 2 of 3. Pagination perfection!

We built these links by hand, which gives us unlimited power to customize. But Pagerfanta does that can generate this for us. If you want to see how, check out the Pagerfanta docs. The downside is that customizing the HTML is a bit more difficult.

Next, let's add more fields to our Starship entity. The best part? Seeing how easy it is to add that column to the database. Let's do it!