Paginación de resultados de la base de datos

Al desarrollar aplicaciones web se encuentra a menudo con el requisito de limitar el número de elementos listados por página, una técnica conocida como paginación.

Partamos de una situación en la que listamos todos los datos sin paginación. Para seleccionar los datos de la base de datos tenemos la clase ArticleRepository. Además del constructor, contiene el método findPublishedArticles, que devuelve todos los artículos publicados ordenados de forma descendente por la 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 después esta clase del modelo. En el método render obtenemos los artículos publicados y se los 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();
	}
}

De listar los artículos se encargará después la plantilla default.latte:

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

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

De esta manera podemos listar todos los artículos, pero eso empieza a ser problemático cuando el número de artículos crece. En ese momento resulta útil implementar un mecanismo de paginación.

Este mecanismo divide todos los artículos en varias páginas y solo mostramos los artículos que pertenecen a la página seleccionada en ese momento. El número total de páginas y el reparto de los artículos los calcula la utilidad Paginator a partir del número total de artículos y del número deseado de artículos por página.

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

	/**
	 * Returns the total number of published articles
	 */
	public function getPublishedArticlesCount(): int
	{
		return $this->database->fetchField('SELECT COUNT(*) FROM articles WHERE created_at < ?', new \DateTime);
	}
}

A continuación modifiquemos el presenter. Le pasaremos al método renderDefault el número de la página actual. Si ese número no forma parte de la URL, estableceremos el valor predeterminado 1 (la primera página).

Ampliaremos también el método render para crear y configurar una instancia del Paginator y seleccionar los artículos adecuados para mostrarlos en la plantilla. El HomePresenter modificado tendrá este aspecto:

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

		// Crea y configura la instancia del Paginator
		$paginator = new Nette\Utils\Paginator;
		$paginator->setItemCount($articlesCount); // número total de elementos
		$paginator->setItemsPerPage(10); // elementos por página
		$paginator->setPage($page); // número de la página actual

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

		// se los pasa a la plantilla
		$this->template->articles = $articles;
		// y también el propio Paginator para mostrar los controles de paginación
		$this->template->paginator = $paginator;
	}
}

La plantilla ahora recorre solo los artículos de la página actual. Solo nos queda añadir los enlaces de paginación:

{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">First</a>
		&nbsp;|&nbsp;
		<a n:href="default, $paginator->getPage() - 1">Previous</a>
		&nbsp;|&nbsp;
	{/if}

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

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

Con esto queda terminada la implementación de la paginación con el Paginator. Si usa como capa de base de datos Nette Database Explorer en lugar de Nette Database Core, puede implementar la paginación incluso sin usar directamente la utilidad Paginator. La clase Nette\Database\Table\Selection incluye el método page(), que encapsula la lógica de la paginación.

Con este enfoque, el repositorio tendrá este aspecto:

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 una instancia del Paginator. En su lugar usaremos el método page() que ofrece el objeto Selection devuelto por 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
	{
		// Obtiene los artículos publicados
		$articles = $this->articleRepository->findPublishedArticles();

		// y pasa a la plantilla solo la parte limitada por 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;
	}
}

Como ya no pasamos el objeto Paginator a la plantilla, tenemos que ajustar la parte que muestra los enlaces de paginación:

{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">First</a>
		&nbsp;|&nbsp;
		<a n:href="default, $page - 1">Previous</a>
		&nbsp;|&nbsp;
	{/if}

	Page {$page} of {$lastPage}

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

De esta manera hemos implementado el mecanismo de paginación sin usar explícitamente la utilidad Paginator.