Пагинация результатов базы данных
При создании веб-приложений очень часто возникает требование ограничить количество выводимых элементов на странице.
Начнем с состояния, когда мы выводим все данные без пагинации. Для
выбора данных из базы данных у нас есть класс ArticleRepository, который, помимо
конструктора, содержит метод findPublishedArticles
, возвращающий все
опубликованные статьи, отсортированные по убыванию даты публикации.
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,
);
}
}
В презентере мы затем инжектируем класс модели и в методе рендеринга запрашиваем опубликованные статьи, которые передаем в шаблон:
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();
}
}
В шаблоне default.latte
затем позаботимся о выводе статей:
{block content}
<h1>Статьи</h1>
<div class="articles">
{foreach $articles as $article}
<h2>{$article->title}</h2>
<p>{$article->content}</p>
{/foreach}
</div>
Таким образом, мы умеем выводить все статьи, что, однако, начнет вызывать проблемы, когда количество статей возрастет. В этот момент пригодится реализация механизма пагинации.
Он обеспечит разделение всех статей на несколько страниц, и мы будем отображать только статьи текущей страницы. Общее количество страниц и распределение статей вычислит Paginator сам, исходя из того, сколько всего у нас статей и сколько статей мы хотим отображать на странице.
На первом шаге мы изменим метод получения статей в классе репозитория так, чтобы он мог возвращать только статьи для одной страницы. Также добавим метод для определения общего количества статей в базе данных, который нам понадобится для настройки 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,
);
}
/**
* Возвращает общее количество опубликованных статей
*/
public function getPublishedArticlesCount(): int
{
return $this->database->fetchField('SELECT COUNT(*) FROM articles WHERE created_at < ?', new \DateTime);
}
}
Затем приступим к изменениям в презентере. В метод рендеринга будем передавать номер текущей отображаемой страницы. На случай, если этот номер не будет частью URL, установим значение по умолчанию — первая страница.
Далее также расширим метод рендеринга получением экземпляра Paginator, его настройкой и выбором правильных статей для отображения в шаблоне. HomePresenter после изменений будет выглядеть так:
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
{
// Узнаем общее количество опубликованных статей
$articlesCount = $this->articleRepository->getPublishedArticlesCount();
// Создадим экземпляр Paginator и настроим его
$paginator = new Nette\Utils\Paginator;
$paginator->setItemCount($articlesCount); // общее количество статей
$paginator->setItemsPerPage(10); // количество элементов на странице
$paginator->setPage($page); // номер текущей страницы
// Из базы данных извлечем ограниченное количество статей согласно расчету Paginator
$articles = $this->articleRepository->findPublishedArticles($paginator->getLength(), $paginator->getOffset());
// которую передадим в шаблон
$this->template->articles = $articles;
// а также сам Paginator для отображения опций пагинации
$this->template->paginator = $paginator;
}
}
Шаблон теперь уже итерирует только по статьям одной страницы, нам остается добавить ссылки пагинации:
{block content}
<h1>Статьи</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">Первая</a>
|
<a n:href="default, $paginator->page-1">Предыдущая</a>
|
{/if}
Страница {$paginator->getPage()} из {$paginator->getPageCount()}
{if !$paginator->isLast()}
|
<a n:href="default, $paginator->getPage() + 1">Следующая</a>
|
<a n:href="default, $paginator->getPageCount()">Последняя</a>
{/if}
</div>
Таким образом, мы дополнили страницу возможностью пагинации с
помощью Paginator. В случае, когда вместо Nette
Database Core в качестве слоя базы данных мы используем Nette Database Explorer, мы можем реализовать
пагинацию и без использования Paginator. Класс Nette\Database\Table\Selection
содержит метод page с
логикой пагинации, взятой из Paginator.
Репозиторий при таком способе реализации будет выглядеть так:
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');
}
}
В презентере нам не нужно создавать Paginator, вместо него мы используем
метод класса Selection
, который возвращает репозиторий:
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
{
// Извлечем опубликованные статьи
$articles = $this->articleRepository->findPublishedArticles();
// и в шаблон отправим только их часть, ограниченную согласно расчету метода page
$lastPage = 0;
$this->template->articles = $articles->page($page, 10, $lastPage);
// а также необходимые данные для отображения опций пагинации
$this->template->page = $page;
$this->template->lastPage = $lastPage;
}
}
Поскольку в шаблон мы теперь не передаем Paginator, изменим часть, отображающую ссылки пагинации:
{block content}
<h1>Статьи</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">Первая</a>
|
<a n:href="default, $page - 1">Предыдущая</a>
|
{/if}
Страница {$page} из {$lastPage}
{if $page < $lastPage}
|
<a n:href="default, $page + 1">Следующая</a>
|
<a n:href="default, $lastPage">Последняя</a>
{/if}
</div>
Таким образом, мы реализовали механизм пагинации без использования Paginator.