Постраничный вывод результатов из базы данных
Разрабатывая веб-приложения, вы часто сталкиваетесь с требованием ограничить количество выводимых элементов на странице – приёмом, известным как пагинация.
Начнём с состояния, когда мы выводим все данные без пагинации. Для
выборки данных из базы у нас есть класс 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,
);
}
}
Затем мы внедряем этот класс модели в презентер. В методе render получаем опубликованные статьи и передаём их в шаблон:
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);
}
}
Далее изменим презентер. Мы передадим в метод renderDefault номер
текущей страницы. Если этого номера нет в URL, зададим значение по
умолчанию 1 (первая страница).
Мы также расширим метод render, чтобы создать и настроить экземпляр
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->getPage() - 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 Explorer вместо Nette Database Core, вы можете реализовать
пагинацию и без прямого использования Paginator. В классе
Nette\Database\Table\Selection есть метод page(), который
скрывает в себе логику пагинации.
При таком подходе репозиторий будет выглядеть так:
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. Вместо этого мы
используем метод page() объекта 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.