Pagination des résultats de la base de données

Lors de la création d'applications web, vous rencontrerez très souvent la nécessité de limiter le nombre d'éléments affichés par page.

Nous partons d'un état où nous affichons toutes les données sans pagination. Pour sélectionner les données de la base de données, nous avons une classe ArticleRepository qui, en plus du constructeur, contient une méthode findPublishedArticles qui renvoie tous les articles publiés triés par ordre décroissant de date de publication.

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 la classe de modèle et dans la méthode render, nous demandons les articles publiés, que nous transmettons 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();
	}
}

Dans le template default.latte, nous nous occupons ensuite de l'affichage des articles :

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

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

De cette manière, nous savons afficher tous les articles, ce qui commencera cependant à poser problème lorsque le nombre d'articles augmentera. À ce moment-là, l'implémentation d'un mécanisme de pagination s'avérera utile.

Celui-ci garantira que tous les articles sont répartis sur plusieurs pages et que nous n'affichons que les articles d'une page actuelle. Le nombre total de pages et la répartition des articles seront calculés par Paginator lui-même en fonction du nombre total d'articles que nous avons et du nombre d'articles que nous voulons afficher par page.

Dans un premier temps, nous modifions la méthode d'obtention des articles dans la classe du repository afin qu'elle puisse nous renvoyer uniquement les articles d'une page. Nous ajoutons également une méthode pour connaître le nombre total d'articles dans la base de données, dont nous aurons besoin pour configurer le 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);
	}
}

Ensuite, nous nous attaquons aux modifications du presenter. Dans la méthode render, nous transmettrons le numéro de la page actuellement affichée. Au cas où ce numéro ne ferait pas partie de l'URL, nous définirons la valeur par défaut de la première page.

Nous étendrons également la méthode render pour obtenir l'instance du Paginator, la configurer et sélectionner les bons articles à afficher dans le template. Le HomePresenter ressemblera à ceci après les modifications :

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
	{
		// Nous obtenons le nombre total d'articles publiés
		$articlesCount = $this->articleRepository->getPublishedArticlesCount();

		// Nous fabriquons une instance de Paginator et la configurons
		$paginator = new Nette\Utils\Paginator;
		$paginator->setItemCount($articlesCount); // nombre total d'articles
		$paginator->setItemsPerPage(10); // nombre d'éléments par page
		$paginator->setPage($page); // numéro de la page actuelle

		// Nous extrayons de la base de données un ensemble limité d'articles selon le calcul du Paginator
		$articles = $this->articleRepository->findPublishedArticles($paginator->getLength(), $paginator->getOffset());

		// que nous transmettons au template
		$this->template->articles = $articles;
		// et aussi le Paginator lui-même pour afficher les options de pagination
		$this->template->paginator = $paginator;
	}
}

Le template itère désormais uniquement sur les articles d'une seule page, il nous suffit d'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->page - 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>

Nous avons ainsi complété la page avec la possibilité de pagination à l'aide du Paginator. Dans le cas où, au lieu de Nette Database Core comme couche de base de données, nous utilisons Nette Database Explorer, nous sommes capables d'implémenter la pagination i sans utiliser le Paginator. La classe Nette\Database\Table\Selection contient en effet une méthode page() avec la logique de pagination intégrée.

Le repository ressemblera à ceci avec cette méthode d'implémentation :

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 de Paginator, nous utilisons directement la méthode page() de la classe Selection que nous renvoie 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
	{
		// Nous extrayons les articles publiés
		$articles = $this->articleRepository->findPublishedArticles();

		// et nous envoyons au template seulement une partie d'entre eux limitée selon le calcul de la méthode page
		$lastPage = 0;
		$this->template->articles = $articles->page($page, 10, $lastPage);

		// et aussi les données nécessaires pour afficher les options de pagination
		$this->template->page = $page;
		$this->template->lastPage = $lastPage;
	}
}

Comme nous n'envoyons plus de Paginator au template, nous modifions la partie affichant 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>

De cette manière, nous avons implémenté le mécanisme de pagination en utilisant la méthode page() de Nette Database Explorer.