Paginação de resultados do banco de dados

Ao criar aplicações web, você frequentemente encontrará a exigência de limitar o número de itens exibidos por página.

Partiremos do estado em que exibimos todos os dados sem paginação. Para selecionar dados do banco de dados, temos a classe ArticleRepository, que, além do construtor, contém o método findPublishedArticles, que retorna todos os artigos publicados ordenados decrescentemente pela data de publicação.

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,
		);
	}
}

No presenter, injetamos a classe do modelo e no método render solicitamos os artigos publicados, que passamos para o 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();
	}
}

No template default.latte, cuidamos da exibição dos artigos:

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

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

Desta forma, podemos exibir todos os artigos, o que, no entanto, começará a causar problemas quando o número de artigos aumentar. Nesse momento, a implementação de um mecanismo de paginação se torna útil.

Ele garantirá que todos os artigos sejam divididos em várias páginas e exibiremos apenas os artigos da página atual. O número total de páginas e a divisão dos artigos serão calculados pelo Paginator com base em quantos artigos temos no total e quantos artigos por página queremos exibir.

No primeiro passo, modificamos o método para obter artigos na classe do repositório para que ele possa retornar apenas artigos para uma página. Também adicionamos um método para descobrir o número total de artigos no banco de dados, que precisaremos para configurar o 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,
		);
	}

	/**
	 * Retorna o número total de artigos publicados
	 */
	public function getPublishedArticlesCount(): int
	{
		return $this->database->fetchField('SELECT COUNT(*) FROM articles WHERE created_at < ?', new \DateTime);
	}
}

Em seguida, começamos a modificar o presenter. Passaremos o número da página atualmente exibida para o método render. Caso este número não faça parte da URL, definiremos o valor padrão da primeira página.

Também estenderemos o método render para obter a instância do Paginator, configurá-lo e selecionar os artigos corretos para exibição no template. O HomePresenter ficará assim após as modificações:

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
	{
		// Descobrimos o número total de artigos publicados
		$articlesCount = $this->articleRepository->getPublishedArticlesCount();

		// Criamos uma instância do Paginator e a configuramos
		$paginator = new Nette\Utils\Paginator;
		$paginator->setItemCount($articlesCount); // número total de artigos
		$paginator->setItemsPerPage(10); // número de itens por página
		$paginator->setPage($page); // número da página atual

		// Do banco de dados, extraímos um conjunto limitado de artigos de acordo com o cálculo do Paginator
		$articles = $this->articleRepository->findPublishedArticles($paginator->getLength(), $paginator->getOffset());

		// que passamos para o template
		$this->template->articles = $articles;
		// e também o próprio Paginator para exibir as opções de paginação
		$this->template->paginator = $paginator;
	}
}

O template agora itera apenas sobre os artigos de uma página, basta adicionar os links de paginação:

{block content}
<h1>Artigos</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">Primeira</a>
		&nbsp;|&nbsp;
		<a n:href="default, $paginator->page-1">Anterior</a>
		&nbsp;|&nbsp;
	{/if}

	Página {$paginator->getPage()} de {$paginator->getPageCount()}

	{if !$paginator->isLast()}
		&nbsp;|&nbsp;
		<a n:href="default, $paginator->getPage() + 1">Próxima</a>
		&nbsp;|&nbsp;
		<a n:href="default, $paginator->getPageCount()">Última</a>
	{/if}
</div>

Assim, adicionamos a opção de paginação à página usando o Paginator. Caso, em vez do Nette Database Core como camada de banco de dados, usemos o Nette Database Explorer, somos capazes de implementar a paginação mesmo sem usar o Paginator. A classe Nette\Database\Table\Selection contém o método page com a lógica de paginação herdada do Paginator.

O repositório ficará assim com este método de implementação:

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');
	}
}

No presenter, não precisamos criar o Paginator, usaremos em seu lugar o método da classe Selection, que o repositório nos retorna:

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
	{
		// Extraímos os artigos publicados
		$articles = $this->articleRepository->findPublishedArticles();

		// e para o template enviamos apenas sua parte limitada de acordo com o cálculo do método page
		$lastPage = 0;
		$this->template->articles = $articles->page($page, 10, $lastPage);

		// e também os dados necessários para exibir as opções de paginação
		$this->template->page = $page;
		$this->template->lastPage = $lastPage;
	}
}

Como agora não enviamos o Paginator para o template, modificamos a parte que exibe os links de paginação:

{block content}
<h1>Artigos</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">Primeira</a>
		&nbsp;|&nbsp;
		<a n:href="default, $page - 1">Anterior</a>
		&nbsp;|&nbsp;
	{/if}

	Página {$page} de {$lastPage}

	{if $page < $lastPage}
		&nbsp;|&nbsp;
		<a n:href="default, $page + 1">Próxima</a>
		&nbsp;|&nbsp;
		<a n:href="default, $lastPage">Última</a>
	{/if}
</div>

Desta forma, implementamos o mecanismo de paginação sem usar o Paginator.