Adatbázis eredmények lapozása

Webalkalmazások fejlesztése során nagyon gyakran találkozhat azzal a követelménnyel, hogy korlátozni kell az oldalon megjelenített elemek számát.

Kezdjük azzal az állapottal, amikor minden adatot lapozás nélkül listázunk ki. Az adatok adatbázisból történő kiválasztásához van egy ArticleRepository osztályunk, amely a konstruktoron kívül tartalmaz egy findPublishedArticles metódust, amely visszaadja az összes publikált cikket a publikálás dátuma szerint csökkenő sorrendben.

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

A presenterben ezután injectáljuk a modell osztályt, és a render metódusban lekérjük a publikált cikkeket, amelyeket átadunk a sablonnak:

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

A default.latte sablonban pedig gondoskodunk a cikkek kiírásáról:

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

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

Ezzel a módszerrel ki tudjuk listázni az összes cikket, ami azonban problémákat kezd okozni, amint a cikkek száma megnő. Ebben a pillanatban válik hasznossá egy lapozó mechanizmus implementálása.

Ez biztosítja, hogy az összes cikk több oldalra legyen osztva, és mi csak az aktuális oldal cikkeit jelenítjük meg. Az oldalak teljes számát és a cikkek elosztását a Paginator maga számítja ki attól függően, hogy összesen hány cikkünk van, és hány cikket szeretnénk megjeleníteni egy oldalon.

Az első lépésben módosítjuk a cikkek lekérésére szolgáló metódust a repository osztályban úgy, hogy csak egy oldal cikkeit tudja visszaadni. Hozzáadunk egy metódust is az adatbázisban lévő cikkek teljes számának lekérdezésére, amelyre szükségünk lesz a Paginator beállításához:

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

	/**
	 * Visszaadja a publikált cikkek teljes számát
	 */
	public function getPublishedArticlesCount(): int
	{
		return $this->database->fetchField('SELECT COUNT(*) FROM articles WHERE created_at < ?', new \DateTime);
	}
}

Ezután nekilátunk a presenter módosításának. A render metódusba átadjuk az aktuálisan megjelenített oldal számát. Arra az esetre, ha ez a szám nem lenne része az URL-nek, beállítjuk az első oldal alapértelmezett értékét.

Továbbá kibővítjük a render metódust a Paginator példányának megszerzésével, beállításával és a sablonban megjelenítendő megfelelő cikkek kiválasztásával. A HomePresenter a módosítások után így fog kinézni:

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
	{
		// Lekérdezzük a publikált cikkek teljes számát
		$articlesCount = $this->articleRepository->getPublishedArticlesCount();

		// Létrehozunk egy Paginator példányt és beállítjuk
		$paginator = new Nette\Utils\Paginator;
		$paginator->setItemCount($articlesCount); // cikkek teljes száma
		$paginator->setItemsPerPage(10); // elemek száma oldalanként
		$paginator->setPage($page); // aktuális oldal száma

		// Az adatbázisból lekérünk egy korlátozott cikkhalmazt a Paginator számítása szerint
		$articles = $this->articleRepository->findPublishedArticles($paginator->getLength(), $paginator->getOffset());

		// amelyet átadunk a sablonnak
		$this->template->articles = $articles;
		// és magát a Paginatort is a lapozási lehetőségek megjelenítéséhez
		$this->template->paginator = $paginator;
	}
}

A sablonunk most már csak egy oldal cikkein iterál, elég hozzáadnunk a lapozó linkeket:

{block content}
<h1>Cikkek</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">Első</a>
		&nbsp;|&nbsp;
		<a n:href="default, $paginator->page-1">Előző</a>
		&nbsp;|&nbsp;
	{/if}

	Oldal {$paginator->getPage()} / {$paginator->getPageCount()}

	{if !$paginator->isLast()}
		&nbsp;|&nbsp;
		<a n:href="default, $paginator->getPage() + 1">Következő</a>
		&nbsp;|&nbsp;
		<a n:href="default, $paginator->getPageCount()">Utolsó</a>
	{/if}
</div>

Így egészítettük ki az oldalt a Paginator segítségével történő lapozás lehetőségével. Abban az esetben, ha a Nette Database Core helyett adatbázisrétegként a Nette Database Explorer-t használjuk, képesek vagyunk implementálni a lapozást Paginator használata nélkül is. A Nette\Database\Table\Selection osztály ugyanis tartalmaz egy page metódust a Paginatorból átvett lapozási logikával.

A repository ebben az implementációs módban így fog kinézni:

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

A presenterben nem kell Paginatort létrehoznunk, helyette a Selection osztály metódusát használjuk, amelyet a repository ad vissza:

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
	{
		// Lekérjük a publikált cikkeket
		$articles = $this->articleRepository->findPublishedArticles();

		// és a sablonba csak azok egy részét küldjük el, amelyet a page metódus számítása korlátoz
		$lastPage = 0;
		$this->template->articles = $articles->page($page, 10, $lastPage);

		// és a szükséges adatokat is a lapozási lehetőségek megjelenítéséhez
		$this->template->page = $page;
		$this->template->lastPage = $lastPage;
	}
}

Mivel most nem küldünk Paginatort a sablonba, módosítjuk a lapozó linkeket megjelenítő részt:

{block content}
<h1>Cikkek</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">Első</a>
		&nbsp;|&nbsp;
		<a n:href="default, $page - 1">Előző</a>
		&nbsp;|&nbsp;
	{/if}

	Oldal {$page} / {$lastPage}

	{if $page < $lastPage}
		&nbsp;|&nbsp;
		<a n:href="default, $page + 1">Következő</a>
		&nbsp;|&nbsp;
		<a n:href="default, $lastPage">Utolsó</a>
	{/if}
</div>

Ezzel a módszerrel implementáltuk a lapozó mechanizmust Paginator használata nélkül.