Une pagination performante avec Symfony 2

I. Introduction à Symfony 2

Symfony 2 est un framework de développement PHP puissant, avec lequel on peut faire beaucoup de choses. Ce tutoriel d'adresse aux personnes qui connaissent déjà Symfony 2, car un peu technique ;-)

Lorsque vous souhaitez créer une pagination, pour aller aux pages précédentes et suivantes de vos listes (d'articles par exemple), vous trouvez assez vite le bundle KnpPaginatorBundle qui est fait pour cela. Celui-ci est très utile bien entendu, mais il manque un peu de performance lorsque vous souhaitez paginer des listes énormes, puisqu'il charge tous les objets de votre liste complète en mémoire.

C'est un peu dommage au niveau des performances. Si vous développez un blog qui contient 10 000 articles, cela signifie que vous allez charger 10 000 objets pour n'en afficher que 10 ou 15 à la fois, par page.

La plupart des Bundles ou bout de code de pagination fonctionnent de la même manière, ils chargent tout, car c'est plus simple au niveau du développement et plus propre de ne pas faire trop de logique dans la vue.
Dans ce tutoriel, je vais essayer de vous expliquer comment on peut procéder pour ne charger que le objets qui seront réellement affichés, et donc faire une pagination performante.

II. Explications

1. Le Model

Comme je ne vais pas pouvoir m'éloigner trop du sujet (ce serait le sujet d'autres tutoriels), je suppose que vous savez ce qu'est le MVC et les EntityRepository de Symfony 2.

Partant de là, nous allons prendre l'exemple d'une liste d'articles que vous souhaitez afficher sur un blog, disons que vous souhaitez en afficher 10 par page...
Vous aurez donc une classe ArticleRepository pour y placer votre méthode "getList()".

Nous allons prévoir quelques paramètres à cette méthode afin de pouvoir créer une requête qui ne retourne que les 10 articles de la page affichée à la fois.
    /**
     * Get the paginated list of published articles
     *
     * @param int $page
     * @param int $maxperpage
     * @param string $sortby
     * @return Paginator
     */
    public function getList($page=1, $maxperpage=10)
    {
        $q = $this->_em->createQueryBuilder()
            ->select('article')
            ->from('SimaDemoBundle:Article','article')
        ;

        $q->setFirstResult(($page-1) * $maxperpage)
            ->setMaxResults($maxperpage);

        return new Paginator($q);
    }
Notez que nous fournissons des valeurs par défaut aux paramètres $page et $maxperpage, que nous ne sélectionnons que les 10 articles qui nous intéressent, et que nous renvoyons un objet de type "Paginator" fournit par Doctrine.

Le paramètre $marperpage n'a que peu d'intérêt si vous fournissez toujours la même valeur, mais je l'ai mis volontairement ici car nous allons essayer de faire une pagination assez générique. Si jamais je réutilise cette pagination pour afficher la liste des articles à plusieurs endroits différents, et que je veux afficher 20 articles par page quand ça me plaît, je pourrai le faire.

2. Le Controller

Maintenant, comment utiliser dans notre contrôleur qui affiche les articles ?

Et bien, pensez tout d'abord à créer la Route qui va bien, avec la valeur 1 par défaut pour le numéro de la page à afficher :

article_list:
    path:  /article/list/{page}
    defaults: { _controller: SimaDemoBundle:Article:list, page: 1 }

Ensuite, prévoyez une méthode qui compte votre nombre d'articles total dans votre ArticleRepository, et créez le contrôleur :
    public function ListAction($page)
    {
        $maxArticles = $this->container->getParameter('max_articles_per_page');
        $articles_count = $this->getDoctrine()
                ->getRepository('SimaDemoBundle:Article')
                ->countPublishedTotal();
        $pagination = array(
            'page' => $page,
            'route' => 'article_list',
            'pages_count' => ceil($articles_count / $maxArticles),
            'route_params' => array()
        );

         $articles = $this->getDoctrine()->getRepository('SimaDemoBundle:Article')
                ->getList($page, $maxArticles);

        return $this->render('SimaDemoBundle:Article:list.html.twig', array(
            'articles' => $articles,
            'pagination' => $pagination
        ));
    }
Dans ce contrôleur, vous allez récupérer les liste des articles pour la page courante dans $articles, ainsi que quelques informations pour la pagination dans $pagination, et les passer au template list.html.twig

3. la Vue

Maintenant, ce que vous allez faire dans le template "list" appelé est très simple. Là où vous besoin de la pagination, il vous suffira d'inclure le template "pagination" que nous allons créer par la suite, en faisant ceci :
{% include 'SimaDemoBundle:Default:pagination.html.twig' %}
La variable $pagination que nous avions passée au template "list" via notre contrôleur sera automatiquement passée à notre template "pagination" (c'est la fonction include de Twig qui fait cela).

Cela nous simplifie grandement la tâche, car nous n'avons plus qu'à manipuler les informations que nous avons dans le tableau $pagination.

Sur cet exemple, nous allons afficher un lien pour revenir au tout début de la pagination, un lien vers les pages suivantes et précédentes, ainsi qu'un lien pour aller à la dernière page.
Et au milieu de tout cela, nous allons afficher les numéros de pages, en nous limitant aux 4 pages avant et après la page courante, afin de garder une bonne lisibilité.

Nous allons générer les routes, en fusionnant les paramètres de routes afin d'avoir un système assez dynamique. Actuellement nous ne passons qu'un numéro de page à notre route, mais le jour où nous passerons une catégorie d'article ou autre, cela fonctionnera toujours (il suffira de passer les paramètres dans le tableau $pagination['route_params']).

Voilà le template "pagination" :
<div class="pagination">
    <div class="pagination-buttons">
        {% if pagination.page>1 %}
            <a href="{{ path(pagination.route, 
pagination.route_params|merge({'page': 1})) }}"><<</a>
            <a href="{{ path(pagination.route, 
pagination.route_params|merge({'page': pagination.page-1})) }}"><</a>
        {% endif %}
        {#display p numbers only from p-4 to p+4 but don't go <1 or >pages_count#}
        {% for p in range(max(pagination.page-4, 1), 
min(pagination.page+4, pagination.pages_count)) %}
            <a{% if p == pagination.page %} class="current-page"{% endif %} 
href="{{ path(pagination.route, 
pagination.route_params|merge({'page': p})) }}">{{ p }}</a>
        {% endfor %}
        {% if pagination.page<pagination.pages_count %}
            <a href="{{ path(pagination.route, 
pagination.route_params|merge({'page': pagination.page+1})) }}">></a>
            <a href="{{ path(pagination.route, 
pagination.route_params|merge({'page': pagination.pages_count})) }}">>></a>
        {% endif %}
    </div>
</div>
Et voilà le résultat après avoir ajouté un peu de CSS pour mettre en forme les liens :

Une pagination performante avec Symfony 2 image 0

III. Conclusion du tutorial

Amusez-vous bien avec Symfony 2 et la pagination.
En espérant que ma manière de paginer avec Symfony vous aura inspiré et vous sera utile pour vos projets. Et si vous avez trouvé plus simple et efficace, n'hésitez pas à venir nous en parler sur le forum ;-)