Paginacja wyników bazy danych
Podczas tworzenia aplikacji internetowych często spotkasz się z wymogiem ograniczenia liczby elementów wyszczególnionych na stronie.
Zaczynamy od stanu, w którym wypisujemy wszystkie dane bez paginacji. Do wybierania danych z bazy mamy klasę
ArticleRepository, która oprócz konstruktora zawiera metodę findPublishedArticles
, która zwraca wszystkie
opublikowane artykuły posortowane w porządku malejącym według daty publikacji.
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,
);
}
}
Następnie wstrzykujemy klasę modelu w prezenterze i w metodzie render żądamy opublikowanych artykułów do przekazania do szablonu:
namespace App\UI\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();
}
}
Szablon default.latte
zajmie się następnie listą artykułów:
{block content}
<h1>Články</h1>
<div class="articles">
{foreach $articles as $article}
<h2>{$article->title}</h2>
<p>{$article->content}</p>
{/foreach}
</div>
W ten sposób możemy wymienić wszystkie artykuły, ale spowoduje to problemy, gdy liczba artykułów wzrośnie. Wtedy właśnie przydaje się implementacja mechanizmu paginacji.
Dzięki temu wszystkie artykuły zostaną podzielone na wiele stron, a my wyświetlimy tylko artykuły z jednej bieżącej strony. Paginator sam obliczy całkowitą liczbę stron i podział artykułów, w zależności od tego ile mamy artykułów w sumie i ile artykułów na stronę chcemy wyświetlić.
W pierwszym kroku modyfikujemy metodę pobierania artykułów w klasie repozytorium tak, aby zwracała ona artykuły tylko dla jednej strony. Dodamy też metodę, która pozwoli nam uzyskać całkowitą liczbę artykułów w bazie, co będzie nam potrzebne do skonfigurowania Paginatora:
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,
);
}
/**
* Vrací celkový počet publikovaných článků
*/
public function getPublishedArticlesCount(): int
{
return $this->database->fetchField('SELECT COUNT(*) FROM articles WHERE created_at < ?', new \DateTime);
}
}
Następnie zabierzemy się do pracy nad modyfikacją prezentera. Do metody render przekażemy numer aktualnie wyświetlanej strony. W przypadku, gdy ten numer nie jest częścią adresu URL, ustawimy domyślną wartość pierwszej strony.
Następnie rozszerzymy również metodę render, aby uzyskać instancję Paginatora, skonfigurować ją i wybrać odpowiednie artykuły do wyświetlenia w szablonie. HomePresenter po modyfikacjach będzie wyglądał tak:
namespace App\UI\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
{
// Uzyskaj całkowitą liczbę opublikowanych artykułów
$articlesCount = $this->articleRepository->getPublishedArticlesCount();
// Utwórz instancję Paginatora i skonfiguruj ją
$paginator = new Nette\Utils\Paginator;
$paginator->setItemCount($articlesCount); // całkowita liczba artykułów
$paginator->setItemsPerPage(10); // ilość elementów na stronie
$paginator->setPage($page); // aktualny numer strony
// Wyciągamy z bazy ograniczony zestaw artykułów według obliczeń Paginatora
$articles = $this->articleRepository->findPublishedArticles($paginator->getLength(), $paginator->getOffset());
// które przekażemy do szablonu
$this->template->articles = $articles;
// a także sam Paginator, aby wyświetlić opcje paginacji
$this->template->paginator = $paginator;
}
}
Szablon już iteruje po tylko artykułach jednej strony, musimy tylko dodać linki paginacji:
{block content}
<h1>Články</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">První</a>
|
<a n:href="default, $paginator->page-1">Předchozí</a>
|
{/if}
Stránka {$paginator->getPage()} z {$paginator->getPageCount()}
{if !$paginator->isLast()}
|
<a n:href="default, $paginator->getPage() + 1">Další</a>
|
<a n:href="default, $paginator->getPageCount()">Poslední</a>
{/if}
</div>
W ten sposób dodaliśmy do strony paginację Paginator. W przypadku, gdy jako warstwy bazy danych użyjemy Nette Database Explorer zamiast Nette Database Core, jesteśmy w stanie zaimplementować paginację bez
użycia Paginatora. Klasa Nette\Database\Table\Selection
zawiera metodę page z logiką paginacji zaczerpniętą
z Paginatora.
Repozytorium będzie wyglądać tak z tą implementacją:
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');
}
}
Nie musimy tworzyć Paginatora w prezenterze, zamiast tego używamy metody klasy Selection
zwracanej przez
repozytorium:
namespace App\UI\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
{
// Vytáhneme si publikované články
$articles = $this->articleRepository->findPublishedArticles();
// i przesłać do szablonu tylko tę ich część, która jest ograniczona przez obliczenie metody strony
$lastPage = 0;
$this->template->articles = $articles->page($page, 10, $lastPage);
// a także dane niezbędne do wyświetlenia opcji paginacji
$this->template->page = $page;
$this->template->lastPage = $lastPage;
}
}
Ponieważ nie wysyłamy teraz Paginatora do szablonu, wyedytujemy część pokazującą linki paginacji:
{block content}
<h1>Články</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">První</a>
|
<a n:href="default, $page - 1">Předchozí</a>
|
{/if}
Stránka {$page} z {$lastPage}
{if $page < $lastPage}
|
<a n:href="default, $page + 1">Další</a>
|
<a n:href="default, $lastPage">Poslední</a>
{/if}
</div>
W ten sposób zaimplementowaliśmy mechanizm paginacji bez użycia Paginatora.