Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

Pagination

Nobody shows a thousand posts on one page: it is slow and awkward. Records are shown in portions with links to neighbouring pages below. Doctrine has the Paginator class for this.

Querying one page

// src/Repository/PostRepository.php
use Doctrine\ORM\Tools\Pagination\Paginator;
 
public function paginate(int $page, int $perPage = 10): Paginator
{
    $query = $this->createQueryBuilder('p')
        ->orderBy('p.id', 'DESC')
        ->setFirstResult(($page - 1) * $perPage)
        ->setMaxResults($perPage)
        ->getQuery();
 
    return new Paginator($query);
}

setFirstResult() is how many records to skip, setMaxResults() how many to take. Paginator also counts the total.

The controller

The page number comes from the address — /blog?page=2:

use Symfony\Component\HttpKernel\Attribute\MapQueryParameter;
 
#[Route('/blog', name: 'blog_index')]
public function index(PostRepository $posts, #[MapQueryParameter] int $page = 1): Response
{
    $page = max(1, $page);
    $perPage = 10;
 
    $paginator = $posts->paginate($page, $perPage);
 
    return $this->render('blog/index.html.twig', [
        'posts' => $paginator,
        'page' => $page,
        'pages' => (int) ceil(count($paginator) / $perPage),
    ]);
}

count($paginator) returns the number of all records, not only those on the current page.

The template

{% for post in posts %}
    <h2>{{ post.title }}</h2>
{% endfor %}
 
<nav>
    {% if page > 1 %}
        <a href="{{ path('blog_index', {page: page - 1}) }}">← Back</a>
    {% endif %}
 
    Page {{ page }} of {{ pages }}
 
    {% if page < pages %}
        <a href="{{ path('blog_index', {page: page + 1}) }}">Next →</a>
    {% endif %}
</nav>

A parameter that is not part of the route's address is added by path() after the ? sign.

A ready-made package

KnpPaginatorBundle takes over the arithmetic and the links:

composer require knplabs/knp-paginator-bundle
use Knp\Component\Pager\PaginatorInterface;
 
#[Route('/blog', name: 'blog_index')]
public function index(
    PostRepository $posts,
    PaginatorInterface $paginator,
    #[MapQueryParameter] int $page = 1,
): Response {
    $query = $posts->createQueryBuilder('p')->orderBy('p.id', 'DESC');
 
    return $this->render('blog/index.html.twig', [
        'posts' => $paginator->paginate($query, $page, 10),
    ]);
}
{% for post in posts %}
    <h2>{{ post.title }}</h2>
{% endfor %}
 
{{ knp_pagination_render(posts) }}

Keeping filters

If the page has a search, its parameters are passed into the links, otherwise the filter is lost on page two:

<a href="{{ path('blog_index', {page: page + 1, q: app.request.query.get('q')}) }}">Next →</a>

Common mistake

The page number comes from the visitor. With page=0 or page=-5 the offset becomes negative, and the database returns an error. Limit the value — max(1, $page) — and do not forget ordering: without orderBy() the order of records is undefined, and one record may land on two pages.

Thanks for reading!

Donate to the site — we work hard for you.