Pagination

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 $10.00

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

Login Subscribe

On production - because The SpaceBar is going to be a huge hit with a lot of thoughtful comments - this list will eventually become, way long. Not only will this page become hard to use, it will quickly slow down until it stops working. If you ever need to query and render more than 100 Doctrine entities, you're going to have slow loading times. If you try to print 1000, your page probably just won't load.

But no problem! Printing that many results is a total bummer for usability anyways! And that's why the Internet has a tried-and-true solution for this: pagination. Doctrine itself doesn't come with any pagination features. But, it doesn't need to: there are a few great libraries that do.

Search for KnpPaginatorBundle. As usual, my disclaimer is that I did nothing to help build this bundle, I just think it's great. Find the composer require line, copy that, go to your terminal and paste:

composer require knplabs/knp-paginator-bundle

While that's installing, go back to its documentation. As I love to tell you, over and over again, the main thing a bundle gives you is new services. And that's 100% true for this bundle.

But before we talk more about that, notice that this has some details about enabling your bundle. That happens automatically in Symfony 4 thanks to Flex. So, ignore it.

Paginator Usage and the Autowiring Alias

Anyways, look down at the Usage example. Hmm, from a controller, it says to use $this->get('knp_paginator') to get some paginator service. Then, you pass that a query, the current page number, read from a ?page query parameter, and the number of items you want per page. The paginator handles the rest! If you want 10 results per page and you're on page 3, the paginator will fetch only the results you need by adding a LIMIT and OFFSET to your query.

The one tricky thing is that the documentation is a little bit out of date. The $this->get() method - which is the same as saying $this->container->get() - is the historic way to fetch a service out of the container by using its id. Depending on your setup, that may or may not even be possible in Symfony 4. And, in general, it's no longer considered a best-practice. Instead, you should use dependency injection, which almost always means, autowiring.

But, hmm, it doesn't say anything about autowiring here. That's a problem: the bundle needs to tell us what class or interface we can use to autowire the paginator service. Ah, don't worry: we can figure it out on our own!

Go back to your terminal Excellent! The install finished. Now run:

php bin/console debug:autowiring

Search for pager. Boom! Apparently there is a PaginatorInterface we can use to get that exact service. We are in business!

Using the Paginator

Back in CommentAdminController, add that as the 3rd argument: PaginatorInterface. Make sure to auto-complete this to get the use statement. Call the arg $paginator:

... lines 1 - 5
use Knp\Component\Pager\PaginatorInterface;
... lines 7 - 10
class CommentAdminController extends Controller
{
... lines 13 - 15
public function index(CommentRepository $repository, Request $request, PaginatorInterface $paginator)
{
... lines 18 - 30
}
}

Next, go back to the docs and copy the $pagination = section, return, and paste:

... lines 1 - 10
class CommentAdminController extends Controller
{
... lines 13 - 15
public function index(CommentRepository $repository, Request $request, PaginatorInterface $paginator)
{
... lines 18 - 21
$pagination = $paginator->paginate(
$queryBuilder, /* query NOT result */
$request->query->getInt('page', 1)/*page number*/,
10/*limit per page*/
);
... lines 27 - 30
}
}

Ok, so what should we use for $query? When you use a paginator, there is an important, practical change: we are no longer responsible for actually executing the query. Nope, we're now only responsible for building a query and passing it to the paginator. This $query variable should be a QueryBuilder.

So, back in CommentRepository, let's refactor this method to return that instead. Remove the @returns and, instead, use a QueryBuilder return type:

... lines 1 - 16
class CommentRepository extends ServiceEntityRepository
{
... lines 19 - 31
/**
* @param string|null $term
*/
public function getWithSearchQueryBuilder(?string $term): QueryBuilder
{
... lines 37 - 49
}
... lines 51 - 79
}

Next, at the bottom, remove the getQuery() and getResults() lines:

... lines 1 - 16
class CommentRepository extends ServiceEntityRepository
{
... lines 19 - 31
/**
* @param string|null $term
*/
public function getWithSearchQueryBuilder(?string $term): QueryBuilder
{
... lines 37 - 46
return $qb
->orderBy('c.createdAt', 'DESC')
;
}
... lines 51 - 79
}

Finally, rename the method to getWithSearchQueryBuilder():

... lines 1 - 16
class CommentRepository extends ServiceEntityRepository
{
... lines 19 - 34
public function getWithSearchQueryBuilder(?string $term): QueryBuilder
{
... lines 37 - 49
}
... lines 51 - 79
}

Perfect! Back in the controller, add $queryBuilder = $repository->getWithSearchQueryBuilder($q). Pass this below:

... lines 1 - 10
class CommentAdminController extends Controller
{
... lines 13 - 15
public function index(CommentRepository $repository, Request $request, PaginatorInterface $paginator)
{
... lines 18 - 19
$queryBuilder = $repository->getWithSearchQueryBuilder($q);
$pagination = $paginator->paginate(
$queryBuilder, /* query NOT result */
$request->query->getInt('page', 1)/*page number*/,
10/*limit per page*/
);
... lines 27 - 30
}
}

Finally, instead of passing comments into the template, pass this pagination variable:

... lines 1 - 10
class CommentAdminController extends Controller
{
... lines 13 - 15
public function index(CommentRepository $repository, Request $request, PaginatorInterface $paginator)
{
... lines 18 - 19
$queryBuilder = $repository->getWithSearchQueryBuilder($q);
$pagination = $paginator->paginate(
$queryBuilder, /* query NOT result */
$request->query->getInt('page', 1)/*page number*/,
10/*limit per page*/
);
return $this->render('comment_admin/index.html.twig', [
'pagination' => $pagination,
]);
}
}

Open index.html.twig so we can make changes there. First, at the top, let's print the total number of comments because we will now only show 10 on each page. To do that, go back to the docs. Ah, this is perfect. Use: pagination.getTotalItemCount():

... lines 1 - 6
{% block content_body %}
<div class="row">
<div class="col-sm-12">
<h1>Manage Comments ({{ pagination.getTotalItemCount }})</h1>
... lines 11 - 66
</div>
</div>
{% endblock %}

Next, down in the loop, update this to for comment in pagination:

... lines 1 - 6
{% block content_body %}
<div class="row">
<div class="col-sm-12">
<h1>Manage Comments ({{ pagination.getTotalItemCount }})</h1>
... lines 11 - 28
<table class="table table-striped">
... lines 30 - 37
<tbody>
{% for comment in pagination %}
... lines 40 - 61
{% endfor %}
</tbody>
</table>
... lines 65 - 66
</div>
</div>
{% endblock %}

Yes, pagination is an object. But, you can loop over it to get the comments for the current page only.

Oh, and at the bottom, we need some navigation to help the user go to the other pages. That's really easy: on the docs, copy the knp_pagination_render() line and, paste!

... lines 1 - 6
{% block content_body %}
<div class="row">
<div class="col-sm-12">
<h1>Manage Comments ({{ pagination.getTotalItemCount }})</h1>
... lines 11 - 28
<table class="table table-striped">
... lines 30 - 37
<tbody>
{% for comment in pagination %}
... lines 40 - 61
{% endfor %}
</tbody>
</table>
{{ knp_pagination_render(pagination) }}
</div>
</div>
{% endblock %}

Phew! Let's go check it out! Yes! 100 total results, but only 10 on this page. We can click to page 2, then 3 and so-on. Heck, the search even works! Try something really common, like est. The URL has the ?q= query parameter. And, if you change pages, it stays: this is page 2 of that search. Dang, that's awesome.

Using the Bootstrap Pager Navigation Theme

Of course, there's one super minor problem... um... dang, that navigation looks horrible. But, we can fix that! The bundle comes with a bunch of different themes for the navigation. Scroll back up to the configuration example. Obviously, you don't need to configure anything on this bundle. But, there are several options. The most important one is this: template.pagination. This determines which template is used to build the navigation links. And, it ships with one for Bootstrap 4, which is what we're using. Booya!

So, first question: where should this configuration live? Sometimes, a recipe will create a file for us, like stof_doctrine_extensions.yaml. But in this case, that didn't happen. And that's ok! Not every bundle needs to give you a config file. Just create it by hand: knp_paginator.yaml.

As usual, the filename matches the root config key. But, we know from previous tutorials that the filename is actually meaningless. Next, copy the config down to the pagination line, move over, paste, then remove all the stuff we don't need. Finally, copy the bootstrap v4 template name and, paste:

knp_paginator:
template:
pagination: '@KnpPaginator/Pagination/twitter_bootstrap_v4_pagination.html.twig'

We're ready! Move back and refresh. Boom! It still works! It's beautiful! We rock! Our pagination is awesome! I'm super happy!

Now that this is perfect, let's turn to our last big topic: generating a totally new, ManyToMany relationship.

Leave a comment!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3",
        "ext-iconv": "*",
        "knplabs/knp-markdown-bundle": "^1.7", // 1.7.0
        "knplabs/knp-paginator-bundle": "^2.7", // v2.7.2
        "knplabs/knp-time-bundle": "^1.8", // 1.8.0
        "nexylan/slack-bundle": "^2.0,<2.2.0", // v2.0.0
        "php-http/guzzle6-adapter": "^1.1", // v1.1.1
        "sensio/framework-extra-bundle": "^5.1", // v5.1.4
        "stof/doctrine-extensions-bundle": "^1.3", // v1.3.0
        "symfony/asset": "^4.0", // v4.0.4
        "symfony/console": "^4.0", // v4.0.14
        "symfony/flex": "^1.0", // v1.2.7
        "symfony/framework-bundle": "^4.0", // v4.0.14
        "symfony/lts": "^4@dev", // dev-master
        "symfony/orm-pack": "^1.0", // v1.0.6
        "symfony/twig-bundle": "^4.0", // v4.0.4
        "symfony/web-server-bundle": "^4.0", // v4.0.4
        "symfony/yaml": "^4.0", // v4.0.14
        "twig/extensions": "^1.5" // v1.5.1
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.0", // 3.0.2
        "easycorp/easy-log-handler": "^1.0.2", // v1.0.4
        "fzaninotto/faker": "^1.7", // v1.7.1
        "symfony/debug-bundle": "^3.3|^4.0", // v4.0.4
        "symfony/dotenv": "^4.0", // v4.0.14
        "symfony/maker-bundle": "^1.0", // v1.4.0
        "symfony/monolog-bundle": "^3.0", // v3.1.2
        "symfony/phpunit-bridge": "^3.3|^4.0", // v4.0.4
        "symfony/profiler-pack": "^1.0", // v1.0.3
        "symfony/var-dumper": "^3.3|^4.0" // v4.0.4
    }
}