Paginer les résultats de base de données

En développant des applications web, vous rencontrez souvent l'exigence de limiter le nombre d'éléments listés par page, une technique appelée pagination.

Partons d'un état où nous listons toutes les données sans pagination. Pour sélectionner les données dans la base, nous avons une classe ArticleRepository. Outre le constructeur, elle contient une méthode findPublishedArticles qui renvoie tous les articles publiés, triés par date de publication décroissante.

namespace App\Model;

use Nette;

class ArticleRepository
{
	public function __construct(
		private Nette\Database\Connection $database,
	) {
	}

	public function findPublishedArticles(): Nette\Database\ResultSet
	{
		return $this->database->query('
			SELECT * FROM articles
			WHERE created_at < ?
			ORDER BY created_at DESC',
			new \DateTime,
		);
	}
}

Dans le presenter, nous injectons ensuite cette classe de modèle. Dans la méthode render, nous récupérons les articles publiés et les passons au template :

namespace App\Presentation\Home;

use Nette;
use App\Model\ArticleRepository;

class HomePresenter extends Nette\Application\UI\Presenter
{
	public function __construct(
		private ArticleRepository $articleRepository,
	) {
	}

	public function renderDefault(): void
	{
		$this->template->articles = $this->articleRepository->findPublishedArticles();
	}
}

Le template default.latte se chargera ensuite de lister les articles :

{block content}
<h1>Articles</h1>

<div class="articles">
	{foreach $articles as $article}
		<h2>{$article->title}</h2>
		<p>{$article->content}</p>
	{/foreach}
</div>

Nous pouvons ainsi lister tous les articles, mais cela devient problématique à mesure que leur nombre augmente. À ce moment-là, mettre en place un mécanisme de pagination devient utile.

Ce mécanisme divise tous les articles en plusieurs pages et nous n'affichons que les articles appartenant à la page actuellement sélectionnée. Le nombre total de pages et la répartition des articles sont calculés par l'utilitaire Paginator à partir du nombre total d'articles et du nombre souhaité d'articles par page.

Dans un premier temps, nous modifierons la méthode de récupération des articles dans la classe repository pour qu'elle puisse ne renvoyer que les articles d'une seule page. Nous ajouterons aussi une méthode donnant le nombre total d'articles dans la base, nécessaire à la configuration du Paginator :

namespace App\Model;

use Nette;


class ArticleRepository
{
	public function __construct(
		private Nette\Database\Connection $database,
	) {
	}

	public function findPublishedArticles(int $limit, int $offset): Nette\Database\ResultSet
	{
		return $this->database->query('
			SELECT * FROM articles
			WHERE created_at < ?
			ORDER BY created_at DESC
			LIMIT ?
			OFFSET ?',
			new \DateTime, $limit, $offset,
		);
	}

	/**
	 * Renvoie le nombre total d'articles publiés
	 */
	public function getPublishedArticlesCount(): int
	{
		return $this->database->fetchField('SELECT COUNT(*) FROM articles WHERE created_at < ?', new \DateTime);
	}
}

Modifions ensuite le presenter. Nous passerons le numéro de la page courante à la méthode renderDefault. Si ce numéro ne fait pas partie de l'URL, nous fixerons la valeur par défaut à 1 (la première page).

Nous étendrons aussi la méthode render pour créer et configurer une instance de Paginator et sélectionner les articles à afficher dans le template. Le HomePresenter modifié ressemblera à ceci :

namespace App\Presentation\Home;

use Nette;
use App\Model\ArticleRepository;

class HomePresenter extends Nette\Application\UI\Presenter
{
	public function __construct(
		private ArticleRepository $articleRepository,
	) {
	}

	public function renderDefault(int $page = 1): void
	{
		// Récupère le nombre total d'articles publiés
		$articlesCount = $this->articleRepository->getPublishedArticlesCount();

		// Crée et configure l'instance de Paginator
		$paginator = new Nette\Utils\Paginator;
		$paginator->setItemCount($articlesCount); // nombre total d'éléments
		$paginator->setItemsPerPage(10); // éléments par page
		$paginator->setPage($page); // numéro de la page courante

		// Récupère dans la base un ensemble limité d'articles selon le calcul du Paginator
		$articles = $this->articleRepository->findPublishedArticles($paginator->getLength(), $paginator->getOffset());

		// les passe au template
		$this->template->articles = $articles;
		// ainsi que le Paginator lui-même, pour afficher les commandes de pagination
		$this->template->paginator = $paginator;
	}
}

Le template ne parcourt maintenant que les articles de la page courante. Il ne reste qu'à ajouter les liens de pagination :

{block content}
<h1>Articles</h1>

<div class="articles">
	{foreach $articles as $article}
		<h2>{$article->title}</h2>
		<p>{$article->content}</p>
	{/foreach}
</div>

<div class="pagination">
	{if !$paginator->isFirst()}
		<a n:href="default, 1">Première</a>
		&nbsp;|&nbsp;
		<a n:href="default, $paginator->getPage() - 1">Précédente</a>
		&nbsp;|&nbsp;
	{/if}

	Page {$paginator->getPage()} sur {$paginator->getPageCount()}

	{if !$paginator->isLast()}
		&nbsp;|&nbsp;
		<a n:href="default, $paginator->getPage() + 1">Suivante</a>
		&nbsp;|&nbsp;
		<a n:href="default, $paginator->getPageCount()">Dernière</a>
	{/if}
</div>

Cela achève la mise en place de la pagination à l'aide du Paginator. Si vous utilisez Nette Database Explorer plutôt que Nette Database Core comme couche de base de données, vous pouvez mettre en place la pagination sans utiliser directement l'utilitaire Paginator. La classe Nette\Database\Table\Selection dispose d'une méthode page() qui encapsule la logique de pagination.

Avec cette approche, le repository ressemblera à ceci :

namespace App\Model;

use Nette;

class ArticleRepository
{
	public function __construct(
		private Nette\Database\Explorer $database,
	) {
	}

	public function findPublishedArticles(): Nette\Database\Table\Selection
	{
		return $this->database->table('articles')
			->where('created_at < ', new \DateTime)
			->order('created_at DESC');
	}
}

Dans le presenter, nous n'avons pas besoin de créer d'instance de Paginator. Nous utiliserons à la place la méthode page() proposée par l'objet Selection renvoyé par le repository :

namespace App\Presentation\Home;

use Nette;
use App\Model\ArticleRepository;

class HomePresenter extends Nette\Application\UI\Presenter
{
	public function __construct(
		private ArticleRepository $articleRepository,
	) {
	}

	public function renderDefault(int $page = 1): void
	{
		// Récupère les articles publiés
		$articles = $this->articleRepository->findPublishedArticles();

		// et ne passe au template que la portion limitée par le calcul de la méthode page
		$lastPage = 0;
		$this->template->articles = $articles->page($page, 10, $lastPage);

		// ainsi que les données nécessaires à l'affichage des options de pagination
		$this->template->page = $page;
		$this->template->lastPage = $lastPage;
	}
}

Comme nous ne passons plus l'objet Paginator au template, nous devons adapter la partie qui affiche les liens de pagination :

{block content}
<h1>Articles</h1>

<div class="articles">
	{foreach $articles as $article}
		<h2>{$article->title}</h2>
		<p>{$article->content}</p>
	{/foreach}
</div>

<div class="pagination">
	{if $page > 1}
		<a n:href="default, 1">Première</a>
		&nbsp;|&nbsp;
		<a n:href="default, $page - 1">Précédente</a>
		&nbsp;|&nbsp;
	{/if}

	Page {$page} sur {$lastPage}

	{if $page < $lastPage}
		&nbsp;|&nbsp;
		<a n:href="default, $page + 1">Suivante</a>
		&nbsp;|&nbsp;
		<a n:href="default, $lastPage">Dernière</a>
	{/if}
</div>

Nous avons ainsi mis en place le mécanisme de pagination sans utiliser explicitement l'utilitaire Paginator.