Paginarea rezultatelor bazei de date

La crearea aplicațiilor web, vă veți întâlni foarte des cu cerința de a limita numărul de elemente afișate pe pagină.

Pornim de la starea în care afișăm toate datele fără paginare. Pentru selectarea datelor din baza de date avem clasa ArticleRepository, care, pe lângă constructor, conține metoda findPublishedArticles, ce returnează toate articolele publicate sortate descrescător după data publicării.

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

În presenter injectăm apoi clasa model și în metoda render solicităm articolele publicate, pe care le transmitem șablonului:

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

În șablonul default.latte ne ocupăm apoi de afișarea articolelor:

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

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

În acest mod putem afișa toate articolele, ceea ce însă începe să cauzeze probleme în momentul în care numărul articolelor crește. În acel moment devine utilă implementarea unui mecanism de paginare.

Acesta asigură că toate articolele sunt împărțite în mai multe pagini și noi afișăm doar articolele unei pagini curente. Numărul total de pagini și împărțirea articolelor sunt calculate de Paginator singur, în funcție de câte articole avem în total și câte articole dorim să afișăm pe pagină.

În primul pas, modificăm metoda pentru obținerea articolelor în clasa repository astfel încât să ne poată returna doar articolele pentru o singură pagină. Adăugăm și o metodă pentru aflarea numărului total de articole din baza de date, de care vom avea nevoie pentru setarea Paginatorului:

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

	/**
	 * Returnează numărul total de articole publicate
	 */
	public function getPublishedArticlesCount(): int
	{
		return $this->database->fetchField('SELECT COUNT(*) FROM articles WHERE created_at < ?', new \DateTime);
	}
}

Ulterior, ne apucăm de modificările presenterului. În metoda render vom transmite numărul paginii afișate curent. Pentru cazul în care acest număr nu va face parte din URL, setăm valoarea implicită a primei pagini.

Extindem, de asemenea, metoda render cu obținerea instanței Paginatorului, setarea sa și selectarea articolelor corecte pentru afișare în șablon. HomePresenter va arăta astfel după modificări:

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
	{
		// Aflăm numărul total de articole publicate
		$articlesCount = $this->articleRepository->getPublishedArticlesCount();

		// Creăm o instanță a Paginatorului și o setăm
		$paginator = new Nette\Utils\Paginator;
		$paginator->setItemCount($articlesCount); // numărul total de articole
		$paginator->setItemsPerPage(10); // numărul de elemente pe pagină
		$paginator->setPage($page); // numărul paginii curente

		// Extragem din baza de date un set limitat de articole conform calculului Paginatorului
		$articles = $this->articleRepository->findPublishedArticles($paginator->getLength(), $paginator->getOffset());

		// pe care îl transmitem șablonului
		$this->template->articles = $articles;
		// și, de asemenea, Paginatorul însuși pentru afișarea opțiunilor de paginare
		$this->template->paginator = $paginator;
	}
}

Șablonul nostru iterează acum doar peste articolele unei singure pagini, este suficient să adăugăm linkurile de paginare:

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

	Pagina {$paginator->getPage()} din {$paginator->getPageCount()}

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

Astfel am completat pagina cu posibilitatea de paginare folosind Paginator. În cazul în care, în loc de Nette Database Core ca strat de bază de date, folosim Nette Database Explorer, suntem capabili să implementăm paginarea și fără utilizarea Paginatorului. Clasa Nette\Database\Table\Selection conține metoda page cu logica de paginare preluată din Paginator.

Repository-ul va arăta astfel la acest mod de implementare:

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

În presenter nu trebuie să creăm Paginator, folosim în locul său metoda clasei Selection, pe care ne-o returnează repository-ul:

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
	{
		// Extragem articolele publicate
		$articles = $this->articleRepository->findPublishedArticles();

		// și trimitem către șablon doar o parte din ele, limitată conform calculului metodei page
		$lastPage = 0;
		$this->template->articles = $articles->page($page, 10, $lastPage);

		// și, de asemenea, datele necesare pentru afișarea opțiunilor de paginare
		$this->template->page = $page;
		$this->template->lastPage = $lastPage;
	}
}

Deoarece acum nu trimitem Paginator către șablon, modificăm partea care afișează linkurile de paginare:

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

	Pagina {$page} din {$lastPage}

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

În acest mod am implementat mecanismul de paginare fără utilizarea Paginatorului.