Dans le cadre de la création d’un blog, je me suis documenté sur quelques approches basiques pour réaliser une pagination en PHP – MySQL afin de lister des articles.
une fois le principe de base compris, j’ai décidé par la suite de « personnaliser » un tantinet cette pagination et l’afficher avec Twig dans un autre projet utilisant le framework Symfony.
– J’utilise par exemple un « entity service layer » qui est une classe PHP qui sert ici de passerelle entre un Repository Doctrine et une Action/Controller.
Je prends en compte également un affichage descendant ou ascendant « $order » qui est bien sûr facultatif car on peut faire plus simple !
L’objectif est de faire une requête SQL en utilisant les paramètres OFFSET ET LIMIT – ici grâce au Repository avec findByLimitOffsetWithOrder(…) dans la méthode getFilteredList(…) – pour obtenir les posts (articles) propres à la page courante « currentPage » définie dans getPaginationParameters(…).
Les principales méthodes personnelles nécessaires de cet « entity service layer » que j’utilisent sont présentées ci-dessous :
<?php declare(strict_types = 1); namespace App\Service; // Used classes are not declared to simplify code demo! // use ... // use ... class PostServiceLayer { // Traits, properties and constructor with dependency injection and other previous methods // ... /** * Count all posts without filter. * * @return int * * @throws \Doctrine\ORM\NonUniqueResultException * @throws \UnexpectedValueException */ public function countAll() : int { $result = $this->repository->countAll(); if (\is_null($result)) { throw new \UnexpectedValueException('Post total count error: list can not be generated!'); } return $result; } /** * Get filtered post list depending on parameters. * * @param int|null $offset * @param int $limit * @param string $order * * @return array * * @throws \Doctrine\ORM\NonUniqueResultException */ public function getFilteredList( int $offset = null, int $limit = Post::POST_NUMBER_PER_LOADING, string $order = Post::POST_LOADING_MODE ) : array { // Init value to define starting rank $init = ('DESC' === $order) ? $this->countAll() : -1; // Offset starts at 0 (i.e. the 15th Post rank has a value of 14) $start = $offset; $end = $offset + $limit; return $this->repository->findByLimitOffsetWithOrder($order, $init, $start, $end); } /** * Get default parameters to show a post list. * * (e.g. sort direction, post number for "load more", ...) * * @return array */ public function getListDefaultParameters() : array { return [ 'loadingMode' => Post::POST_LOADING_MODE, 'numberPerLoading' => Post::POST_NUMBER_PER_LOADING, 'numberPerPage' => Post::POST_NUMBER_PER_PAGE ]; } /** * Get pagination parameters to manage page links. * * This is used on complete post list accessible on "posts" page. * * @param int $pageIndex * * @return array|null * * @throws \Doctrine\ORM\NonUniqueResultException */ public function getPaginationParameters(int $pageIndex) : ?array { $countAll = $this->countAll(); $listDefaultParameters = $this->getListDefaultParameters(); $postNumberPerPage = $listDefaultParameters['numberPerPage']; $pageCount = $countAll % $postNumberPerPage == 0 ? $countAll / $postNumberPerPage : (int) floor($countAll / $postNumberPerPage) + 1; $loadingMode = $listDefaultParameters['loadingMode']; if ($pageIndex <= 0 || $pageIndex > $pageCount) { return null; } if ('DESC' === $loadingMode) { $offset = $countAll - $pageIndex * $postNumberPerPage < 0 ? 0 : $countAll - $pageIndex * $postNumberPerPage; $limit = $offset === 0 ? $countAll % $postNumberPerPage : $postNumberPerPage; } else { $offset = $pageIndex === 1 ? 0 : ($pageIndex - 1) * $postNumberPerPage; $limit = $offset + $postNumberPerPage > $countAll - 1 ? $countAll % $postNumberPerPage : $postNumberPerPage; } return [ 'currentPage' => $pageIndex, 'currentOffset' => $offset, 'currentLimit' => $limit, 'pageCount' => $pageCount, 'loadingMode' => $loadingMode, 'postCount' => $countAll ]; } // Other following methods // ... }
– Voici la partie Twig à intégrer dans un template (les commentaires sont en anglais, c’est une habitude personnelle !) :
Les deux variables importantes à transmettre à la vue depuis une « Action » ou un « Controller » (non détaillé ici) sont le nombre de page total « pageCount » et la page courante « currentPage » pour sa mise en exergue et structurer les comportements autour d’elle.
Dans une deuxième temps, il s’agit d’initialiser des variables en fonction des conditions induites par la personnalisation que l’on souhaite obtenir.
Je décide par exemple d’afficher les 2 pages précédentes et suivantes (libre à vous d’en afficher plus!) autour de la page courante : si cela n’est pas possible, j’évalue la possibilité d’afficher 1 page, ou aucune, avant ou après la page courante, ce qui explique les conditions initialisées au préalable.
Je substitue les autres numéros de page par « … » pour les matérialiser, à l’exception de la première et la dernière pour conserver les bornes extrêmes, et ainsi gérer un nombre conséquent de page à afficher le cas échéant.
<!-- Generate pagination block if there is at least more than 1 page! --> {% if pageCount > 1 %} <!-- Pagination --> {# Page quantity to show around current page is 2 or a calculated minimum value #} {% set defaultPageQuantityAround = 2 %} {# Mininum value #} {% set minimumPageQuantityAround = min(currentPage - 1, pageCount - currentPage) %} {# Condition to show the right page numbers before current page: default or minimum value #} {% set conditionBefore = currentPage != 1 and minimumPageQuantityAround <= currentPage - 1 %} {# Condition to show the right page numbers after current page: default or minimum value #} {% set conditionAfter = currentPage != pageCount and minimumPageQuantityAround <= pageCount - currentPage %} {# Define page numbers before, other pages will be replaced by "..." #} {% set PageQuantityAroundBefore = conditionBefore ? defaultPageQuantityAround : minimumPageQuantityAround %} {# Define page numbers after, other pages will be replaced by "..." #} {% set PageQuantityAroundAfter = conditionAfter ? defaultPageQuantityAround : minimumPageQuantityAround %} <div class="uk-flex uk-flex-center"> <ul class="uk-pagination uk-text-bold uk-text-uppercase"> {# Previous link #} {% if currentPage - 1 != 0 %} <li><a class="st-color-yellow" href="{{ path('posts', { 'page': currentPage - 1 }) }}" title="Previous"><span class="uk-margin-small-right" uk-pagination-previous></span> Previous</a></li> {% endif %} {% for i in 1..pageCount %} {# Current page to show #} {% if currentPage == i %} <li class="st-color-red">{{ i }}</li> {# Show "..." before current page depending on page numbers to show before #} {% elseif (i < currentPage and 1 != i) and (i == currentPage - PageQuantityAroundBefore - 1) %} <li class="uk-disabled">...</li> {# Show "..." after current page depending on page numbers to show after #} {% elseif (i > currentPage and pageCount != i) and (i == currentPage + PageQuantityAroundAfter + 1) %} <li class="uk-disabled">...</li> {# Hide pages under current page and before "..." excepted page 1 #} {% elseif (1 != i) and (i < currentPage - PageQuantityAroundBefore - 1) %} <li class="uk-hidden"><a href="{{ path('posts', { 'page': i }) }}" title="Page {{ i }}">{{ i }}</a></li> {# Hide pages over current page and after "..." excepted page with number "pageCount" (last) #} {% elseif (pageCount != i) and (i > currentPage + PageQuantityAroundAfter + 1) %} <li class="uk-hidden"><a href="{{ path('posts', { 'page': i }) }}" title="Page {{ i }}">{{ i }}</a></li> {# Apply particular style for lowest link corresponding to fisrt page 1, and Highest link corresponding to page total count #} {% elseif i == 1 or i == pageCount %} <li><a class="st-color-blue" href="{{ path('posts', { 'page': i }) }}" title="Page {{ i }}">{{ i }}</a></li> {# Normal links which are not concerned by conditions above #} {% else %} <li><a href="{{ path('posts', { 'page': i }) }}" title="Page {{ i }}">{{ i }}</a></li> {% endif %} {% endfor %} {# Next link #} {% if currentPage + 1 <= pageCount %} <li class="uk-margin-auto-left"><a class="st-color-yellow" href="{{ path('posts', { 'page': currentPage + 1 }) }}" title="Next">Next <span class="uk-margin-small-left" uk-pagination-next></span></a></li> {% endif %} </ul> </div> {% endif %}
Et voici le résultat visuel de la pagination :
Les classes CSS utilisées ici sont personnalisées (pour les couleurs) et issues du framework UIkit pour le comportement (lien désactivé, lien caché …), donc rien de significatif pour ce qui est de l’aspect mise en forme.
Un tel système de pagination peut très facilement être adapté pour de l’AJAX et afficher les posts de la page courante (en renvoyant du contenu HTML avec un block Twig par exemple) en asynchrone afin d’être davantage « user friendly » !
Ces extraits de code sont issus d’un projet opérationnel, ceci-dit des « petites coquilles » peuvent s’être glissées entre les lignes.
N’hésitez pas à me contacter pour signaler des erreurs éventuelles ou pour partager des améliorations, car on peut souvent mieux faire ou faire plus pragmatique !