Stronicowanie wyników z bazy danych
Przy tworzeniu aplikacji webowych bardzo często napotkasz wymaganie, żeby ograniczyć liczbę wypisywanych pozycji na stronie. Technika ta nazywa się stronicowaniem.
Wyjdziemy od stanu, w którym wypisujemy wszystkie dane bez stronicowania. Do pobierania danych z bazy mamy klasę
ArticleRepository. Oprócz konstruktora zawiera metodę findPublishedArticles, która zwraca wszystkie
opublikowane artykuły posortowane malejąco 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,
);
}
}
W presenterze wstrzykniemy potem tę klasę modelu. W metodzie render pobierzemy opublikowane artykuły i przekażemy je do szablonu:
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();
}
}
Szablon default.latte zajmie się już wypisaniem artykułów:
{block content}
<h1>Artykuły</h1>
<div class="articles">
{foreach $articles as $article}
<h2>{$article->title}</h2>
<p>{$article->content}</p>
{/foreach}
</div>
W ten sposób potrafimy wypisać wszystkie artykuły, co jednak zaczyna być problematyczne, gdy artykułów przybywa. W tym momencie przydaje się wdrożenie mechanizmu stronicowania.
Mechanizm ten dzieli wszystkie artykuły na kilka stron i wyświetla tylko artykuły należące do aktualnie wybranej strony. Łączną liczbę stron i podział artykułów wylicza narzędzie Paginator na podstawie łącznej liczby artykułów i pożądanej liczby artykułów na stronie.
W pierwszym kroku zmodyfikujemy w klasie repozytorium metodę pobierającą artykuły tak, żeby potrafiła zwrócić artykuły tylko dla jednej strony. Dodamy też metodę zwracającą łączną liczbę artykułów w bazie danych, potrzebną 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,
);
}
/**
* Zwraca łączną liczbę opublikowanych artykułów
*/
public function getPublishedArticlesCount(): int
{
return $this->database->fetchField('SELECT COUNT(*) FROM articles WHERE created_at < ?', new \DateTime);
}
}
Następnie zmodyfikujemy presenter. Do metody renderDefault przekażemy numer aktualnie wyświetlanej strony.
Jeśli numer ten nie będzie częścią URL, ustawimy wartość domyślną 1 (pierwsza strona).
Rozszerzymy też metodę render o utworzenie i skonfigurowanie instancji Paginatora oraz o wybranie odpowiednich artykułów
do wyświetlenia w szablonie. Zmodyfikowany HomePresenter będzie wyglądać tak:
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
{
// pobieramy łączną liczbę opublikowanych artykułów
$articlesCount = $this->articleRepository->getPublishedArticlesCount();
// tworzymy i konfigurujemy instancję Paginatora
$paginator = new Nette\Utils\Paginator;
$paginator->setItemCount($articlesCount); // łączna liczba pozycji
$paginator->setItemsPerPage(10); // liczba pozycji na stronie
$paginator->setPage($page); // numer aktualnej strony
// pobieramy z bazy ograniczony zbiór artykułów według wyliczenia Paginatora
$articles = $this->articleRepository->findPublishedArticles($paginator->getLength(), $paginator->getOffset());
// przekazujemy je do szablonu
$this->template->articles = $articles;
// a także sam Paginator do wyświetlenia opcji stronicowania
$this->template->paginator = $paginator;
}
}
Szablon iteruje teraz tylko po artykułach z aktualnej strony. Musimy jeszcze dodać odnośniki stronicowania:
{block content}
<h1>Artykuły</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">Pierwsza</a>
|
<a n:href="default, $paginator->getPage() - 1">Poprzednia</a>
|
{/if}
Strona {$paginator->getPage()} z {$paginator->getPageCount()}
{if !$paginator->isLast()}
|
<a n:href="default, $paginator->getPage() + 1">Następna</a>
|
<a n:href="default, $paginator->getPageCount()">Ostatnia</a>
{/if}
</div>
W ten sposób uzupełniliśmy stronę o stronicowanie za pomocą Paginatora. Jeśli zamiast Nette Database Core używasz jako warstwy bazodanowej Nette Database Explorer, potrafisz zrealizować stronicowanie nawet bez
bezpośredniego użycia Paginatora. Klasa Nette\Database\Table\Selection zawiera metodę page(), która zamyka w sobie
logikę stronicowania.
Przy takim podejściu repozytorium będzie wyglądać tak:
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');
}
}
W presenterze nie musimy tworzyć instancji Paginatora. Zamiast tego użyjemy metody page() udostępnianej przez
obiekt Selection zwrócony z repozytorium:
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
{
// pobieramy opublikowane artykuły
$articles = $this->articleRepository->findPublishedArticles();
// i przekazujemy do szablonu tylko ich część ograniczoną wyliczeniem metody page
$lastPage = 0;
$this->template->articles = $articles->page($page, 10, $lastPage);
// a także dane potrzebne do wyświetlenia opcji stronicowania
$this->template->page = $page;
$this->template->lastPage = $lastPage;
}
}
Ponieważ do szablonu nie przekazujemy już obiektu Paginatora, musimy dostosować część wyświetlającą odnośniki stronicowania:
{block content}
<h1>Artykuły</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">Pierwsza</a>
|
<a n:href="default, $page - 1">Poprzednia</a>
|
{/if}
Strona {$page} z {$lastPage}
{if $page < $lastPage}
|
<a n:href="default, $page + 1">Następna</a>
|
<a n:href="default, $lastPage">Ostatnia</a>
{/if}
</div>
W ten sposób zrealizowaliśmy mechanizm stronicowania bez jawnego użycia Paginatora.