Paginación de resultados de la base de datos

Al crear aplicaciones web, muy a menudo te encontrarás con el requisito de limitar el número de elementos mostrados por página.

Partiremos del estado en el que mostramos todos los datos sin paginación. Para seleccionar datos de la base de datos, tenemos la clase ArticleRepository, que además del constructor contiene el método findPublishedArticles, que devuelve todos los artículos publicados ordenados descendentemente por fecha de publicación.

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

En el Presenter, inyectamos la clase del modelo y en el método render solicitamos los artículos publicados, que pasamos a la plantilla:

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

En la plantilla default.latte nos encargamos de mostrar los artículos:

{block content}
<h1>Artículos</h1>

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

De esta manera, podemos mostrar todos los artículos, lo que, sin embargo, comenzará a causar problemas cuando el número de artículos aumente. En ese momento, será útil implementar un mecanismo de paginación.

Este asegurará que todos los artículos se dividan en varias páginas y solo mostraremos los artículos de la página actual. El número total de páginas y la división de los artículos los calculará Paginator por sí mismo según cuántos artículos tengamos en total y cuántos artículos por página queramos mostrar.

En el primer paso, modificaremos el método para obtener artículos en la clase del repositorio para que pueda devolver solo los artículos de una página. También añadiremos un método para averiguar el número total de artículos en la base de datos, que necesitaremos para configurar el 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,
		);
	}

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

A continuación, procederemos a modificar el Presenter. Pasaremos el número de la página actualmente mostrada al método render. En caso de que este número no forme parte de la URL, estableceremos el valor predeterminado de la primera página.

También ampliaremos el método render para obtener una instancia de Paginator, configurarlo y seleccionar los artículos correctos para mostrar en la plantilla. HomePresenter se verá así después de las modificaciones:

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
	{
		// Averiguamos el número total de artículos publicados
		$articlesCount = $this->articleRepository->getPublishedArticlesCount();

		// Creamos una instancia de Paginator y la configuramos
		$paginator = new Paginator;
		$paginator->setItemCount($articlesCount); // número total de artículos
		$paginator->setItemsPerPage(10); // número de elementos por página
		$paginator->setPage($page); // número de la página actual

		// Extraemos de la base de datos un conjunto limitado de artículos según el cálculo de Paginator
		$articles = $this->articleRepository->findPublishedArticles($paginator->getLength(), $paginator->getOffset());

		// que pasamos a la plantilla
		$this->template->articles = $articles;
		// y también el propio Paginator para mostrar las opciones de paginación
		$this->template->paginator = $paginator;
	}
}

La plantilla ahora solo itera sobre los artículos de una página, solo necesitamos añadir los enlaces de paginación:

{block content}
<h1>Artículos</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">Primera</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">Siguiente</a>
		&nbsp;|&nbsp;
		<a n:href="default, $paginator->getPageCount()">Última</a>
	{/if}
</div>

Así hemos añadido la opción de paginación a la página usando Paginator. En caso de que en lugar de Nette Database Core usemos Nette Database Explorer como capa de base de datos, podemos implementar la paginación incluso sin usar Paginator. La clase Nette\Database\Table\Selection contiene el método page con la lógica de paginación tomada de Paginator.

El repositorio se verá así con esta forma de implementación:

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

En el Presenter no necesitamos crear un Paginator, usaremos en su lugar el método de la clase Selection, que nos devuelve el repositorio:

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
	{
		// Extraemos los artículos publicados
		$articles = $this->articleRepository->findPublishedArticles();

		// y a la plantilla enviamos solo una parte de ellos limitada según el cálculo del método page
		$lastPage = 0;
		$this->template->articles = $articles->page($page, 10, $lastPage);

		// y también los datos necesarios para mostrar las opciones de paginación
		$this->template->page = $page;
		$this->template->lastPage = $lastPage;
	}
}

Dado que ahora no enviamos Paginator a la plantilla, modificaremos la parte que muestra los enlaces de paginación:

{block content}
<h1>Artículos</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">Primera</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">Siguiente</a>
		&nbsp;|&nbsp;
		<a n:href="default, $lastPage">Última</a>
	{/if}
</div>

De esta manera, hemos implementado el mecanismo de paginación sin usar Paginator.