Yii2: Пагинация
Если у вас большое количество записей (статей, товаров ...) на одной странице, то имеет смысл воспользоваться постраничным разделением этих данных. В этой статье я покажу несколько способов создания пагинации в Yii2.
ДокументацияПагинацию будем реализовывать для постов блога на странице категории. Классический пример: есть категория блога, к ней имеют отношения посты (записи, статьи) по полю category_id
.
Простая пагинация
Получим все посты одной категории
Контроллер */CategoryController.php
:
public function actionView($id)
{
// Получаем категорию по ID
if (!$category = Category::findOne($id)) {
throw new NotFoundHttpException('Категория не найдена!');
}
// Выполняем запрос на получение опубликованных постов у категории, которую получили
$query = Post::find()->where(['category_id' => $category->id])->active();
// Делаем копию выборки
$countQuery = clone $query;
// Подключаем класс Pagination, выводим по столько записей, сколько указано в Post::PAGE_SIZE
$pagination = new Pagination(['totalCount' => $countQuery->count(), 'pageSize' => Post::PAGE_SIZE]);
// Приводим параметры в ссылке к ЧПУ (убираем per-page)
$pagination->pageSizeParam = false;
$pagination->forcePageParam = false;
// Можно так:
/*
$pagination = new Pagination([
'totalCount' => $countQuery->count(),
'pageSize' => Post::PAGE_SIZE,
'forcePageParam' => false,
'pageSizeParam' => false
]);
*/
$posts = $query->offset($pagination->offset)
->limit($pagination->limit)
->all();
// Передаем данные в представление
return $this->render('view', [
'category' => $category,
'posts' => $posts,
'pagination' => $pagination
]);
}
Вид */view.php
:
<ul>
<? foreach ($posts as $post): ?>
<li><a href="<?= Url::to(['/blog/post/view', 'slug' => $post->slug]) ?>"><?= $post->name ?></a></li>
<? endforeach; ?>
</ul>
<? echo LinkPager::widget([
'pagination' => $pagination,
'registerLinkTags' => true
]); ?>
<p>№ текущей страницы: <?= $pagination->getPage() + 1 ?></p>
<p>Количество страниц: <?= $pagination->getPageCount() ?></p>
<p>Количество постов: <?= $pagination->totalCount ?></p>
Здесь я вывел посты просто в ul
для простоты и наглядности.
Параметр registerLinkTags
прописывает в хедер ссылки на текущую, предыдущую и следующую страницы. По-умолчанию он выключен в false
, чтобы не было конфликтов, если у вас несколько пагинаций на одной странице. Если указать данный параметр в true
, то сгенерируются ссылки:
<link href="http://site.com/blog/category/1" rel="canonical">
<link href="/blog/category/1/page/2" rel="self">
<link href="/blog/category/1" rel="first">
<link href="/blog/category/1" rel="prev">
<link href="/blog/category/1/page/3" rel="next">
<link href="/blog/category/1/page/3" rel="last">
Тем самым мы говорим роботу о том, что на странице находится постраничное разбиение данных.
Яндекс вообще не учитывает rel=prev/next. Google до марта 2019 года рекомендовал использовать rel=prev/next. После марта 2019 года и Google отказался от их использования.
Итог: Нет необходимости использовать rel=prev/next в новых проектах. Если данные теги уже установлены на работающем проекте, то можно их не трогать, мешать они никому не будут. Поисковая система Bing до сих пор использует rel=prev/next.
Настроим маршрут в urlManager
:
//...
// Правило для пагинации должно быть выше основного маршрута
'blog/category/<id:\d+>/page/<page:\d+>' => 'blog/category/view',
'blog/category/<id:\d+>' => 'blog/category/view',
//...
Лично мне не очень нравятся толстые контроллеры, поэтому я вынесу пагинацию в отдельный метод модели Post
. В конце концов, это прямая её (модели) работа - получить данные, как-то их обработать и отдать в контоллер.
Модель */Post.php
:
//...
const STATUS_DRAFT = 0;
const STATUS_ACTIVE = 1;
const PAGE_SIZE = 10;
public static function getAllByCategory(Category $category): array
{
// Выполняем запрос на получение опубликованных постов у категории, ID которой пришёл из контроллера
$query = self::find()->where(['category_id' => $category->id])->active(); // active() - PostQuery::active()
// Делаем копию выборки
$countQuery = clone $query;
// Подключаем класс Pagination, выводим по столько записей, сколько указано в self::PAGE_SIZE
$pagination = new Pagination(['totalCount' => $countQuery->count(), 'pageSize' => self::PAGE_SIZE]);
// Приводим параметры в ссылке к ЧПУ (убираем per-page)
$pagination->pageSizeParam = false;
$pagination->forcePageParam = false;
$posts = $query->offset($pagination->offset)
->limit($pagination->limit)
->all();
// Возвращаем массив данных с постами и страницами
return $data = [
'posts' => $posts,
'pagination' => $pagination
];
}
Теперь контроллер стал чище и ничего лишнего там нет (и не должно быть)
Контроллер */CategoryController.php
:
public function actionView($id)
{
// Получаем категорию по ID
if (!$category = Category::findOne($id)) {
throw new NotFoundHttpException('Категория не найдена!');
}
// Получаем список постов этой категории с пагинацией
$data = Post::getAllByCategory($category);
$posts = $data['posts'];
$pagination = $data['pagination'];
/**
* Если набрать в адресной строке страницу пагинации, которой не существует
* http://site.com/blog/category/10/page/1000000
* показывается последняя страница листинга и в адресе остаётся этот путь
* 1. Можно оставить как есть
* 2. Можно отдать ошибку 404
* 3. Можно установить временный редирект на последнюю страницу листинга
*/
$page = (int)\Yii::$app->request->getQueryParam('page');
// Если текущая страница больше чем количество страниц в листинге
if ($page > $pagination->pageCount) {
// Ошибка 404
//throw new NotFoundHttpException('Page is not found!');
\Yii::$app->response->redirect(Url::to(['view', 'page' => $pagination->pageCount]), 302);
}
// Передаем данные в представление
return $this->render('view', [
'category' => $category,
'posts' => $posts,
'pagination' => $pagination
]);
}
Пагинация, используя провайдер данных
Реализация постраничной навигации, используя провайдер данных . В данном примере будем использовать ActiveDataProvider
.
Перепишем метод getAllByCategory()
в нашей модели Post
:
public static function getAllByCategory(Category $category): ActiveDataProvider
{
$query = self::find()->active()->where(['category_id' => $category->id]);
$countQuery = clone $query;
return new ActiveDataProvider([
'query' => $query,
'totalCount' => (int)$countQuery->count(),
'pagination' => [
'pageSize' => self::PAGE_SIZE,
'pageSizeParam' => false,
'forcePageParam' => false
],
'sort' => [
'defaultOrder' => [
'name' => SORT_ASC,
'created_at' => SORT_DESC
]
],
]);
}
Контроллер */CategoryController.php
:
public function actionView($id)
{
// Получаем категорию по ID
if (!$category = Category::findOne($id)) {
throw new NotFoundHttpException('Категория не найдена!');
}
$dataProvider = Post::getAllByCategory($category);
// Передаем данные в представление
return $this->render('view', [
'category' => $category,
'dataProvider' => $dataProvider
]);
}
Вид */view.php
:
<?= ListView::widget([
'dataProvider' => $dataProvider,
'itemOptions' => ['class' => 'post'],
'itemView' => '_post',
'layout' => '{items}{pager}',
'pager' => ['registerLinkTags' => true],
]) ?>
// Можно так:
<?= $this->render('_list', [
'dataProvider' => $dataProvider
]) ?>
Файл */_post.php
:
<?
/**
* @var $model \common\models\Post
*/
use yii\helpers\Url;
?>
<a href="<?= Url::to(['/blog/post/view', 'slug' => $model->slug]) ?>"><?= $model->name ?></a>
Пагинация, используя Pjax
В Yii2 есть класс \yii\widgets\Pjax, с помощью которого можно подгружать страницы пагинации асинхронно. Там можно ознакомится со всеми его свойствами и методами.
Модель Post
и контроллер CategoryController
оставим с предыдущего примера. Нам необходимо внести изменения в файл */view.php
, который показывает категорию и посты, относящиеся к этой категории.
use yii\widgets\ListView;
use yii\widgets\Pjax;
<? Pjax::begin([
// ID блока, на который навешиваем обработчик
'id' => 'linkPagerCategory',
'scrollTo' => true,
// Применяем обработчик Pjax к нашей пагинации (класс генерится сам)
'linkSelector'=>'.pagination a',
'timeout' => 3000
]) ?>
<?= ListView::widget([
'dataProvider' => $dataProvider,
'itemOptions' => ['class' => 'post'],
'itemView' => '_post',
'layout' => '{items}{pager}',
'pager' => ['registerLinkTags' => true]
]) ?>
<? Pjax::end() ?>
Можно во время подгрузки очередной страницы загружать ещё и прелоадер.
HTML:
<div id="process">
<img src="/img/loading.gif" alt="Loading" width="100">
</div>
CSS:
#process{
position: absolute;
top: 0;
right: 0;
left: 0;
bottom: 0;
background: rgba(255, 255, 255, .95);
z-index: 3; /* При необходимости выставить свои значения */
display: none;
}
#process img{
position: absolute;
top: 80px;
right: 30px;
z-index: 4;
}
jQuery:
$(function () {
var process = $('#process');
$('#linkPagerCategory')
.on('pjax:start', function() {
process.fadeIn();
})
.on('pjax:end', function() {
process.fadeOut();
});
});
Расширение Yii2 Scroll Pager
С помощью данного расширения можно реализовать бесконечную (в зависимости от количество элементов в БД) прокрутку страницы, подгружая данные с помощью AJAX
. Репозиторий расширения здесь .
Установка
composer require kop/yii2-scroll-pager "dev-master"
// Или добавить в composer.json, а затем выполнить "composer update"
"kop/yii2-scroll-pager": "dev-master"
Использование
Вид */view.php
:
<?php
/* @var $this yii\web\View */
use kop\y2sp\ScrollPager;
/* @var $dataProvider yii\data\DataProviderInterface */
echo \yii\widgets\ListView::widget([
'dataProvider' => $dataProvider,
'summary' => false,
'options' => [
'tag' => 'div',
'class' => 'posts'
],
'itemOptions' => [
'class' => 'post__item',
'tag' => 'div'
],
'itemView' => '_post',
'emptyText' => ' ',
'pager' => [
'class' => ScrollPager::class,
'container' => '.posts',
'item' => '.post__item',
'triggerTemplate' => '<div class="reload">
<a href="javascript:void(0);" role="button" title="Показать еще..." class="btn-reload">{text}</a>
</div>',
'triggerText' => 'Показать еще...',
'spinnerTemplate' => '<div class="ias-spinner" style="text-align: center;"><img src="{src}"/></div>',
'spinnerSrc' => Yii::getAlias('@web/i/preloader_posts.gif'), // Прелоадер
'noneLeftText' => 'Записей больше нет</a>'
],
]);
Переопределяем класс LinkPager
По умолчанию, при выводе страницы находятся в ul>li
. Но, что если нам нужно, чтобы ссылки на страницы были в блоке div
, вместо ul>li
. Для этого нужно переопределить класс LinkPager
.
<?php
namespace frontend\components;
use yii\helpers\Html;
class CustomPager extends \yii\widgets\LinkPager
{
protected function renderPageButton($label, $page, $class, $disabled, $active)
{
$options = ['class' => $class === '' ? null : $class];
if ($active) {
Html::addCssClass($options, $this->activePageCssClass);
}
if ($disabled) {
Html::addCssClass($options, $this->disabledPageCssClass);
return Html::tag('span', $label);
}
$linkOptions = $this->linkOptions;
$linkOptions['data-page'] = $page;
return Html::a($label, $this->pagination->createUrl($page), $linkOptions);
}
protected function renderPageButtons()
{
$pageCount = $this->pagination->getPageCount();
if ($pageCount < 2 && $this->hideOnSinglePage) {
return '';
}
$buttons = [];
$currentPage = $this->pagination->getPage();
// first page
$firstPageLabel = $this->firstPageLabel === true ? '1' : $this->firstPageLabel;
if ($firstPageLabel !== false) {
$buttons[] = $this->renderPageButton($firstPageLabel, 0, $this->firstPageCssClass, $currentPage <= 0, false);
}
// prev page
if ($this->prevPageLabel !== false) {
if (($page = $currentPage - 1) < 0) {
$page = 0;
}
$buttons[] = $this->renderPageButton($this->prevPageLabel, $page, $this->prevPageCssClass, $currentPage <= 0, false);
}
// internal pages
list($beginPage, $endPage) = $this->getPageRange();
for ($i = $beginPage; $i <= $endPage; ++$i) {
$buttons[] = $this->renderPageButton($i + 1, $i, null, false, $i == $currentPage);
}
// next page
if ($this->nextPageLabel !== false) {
if (($page = $currentPage + 1) >= $pageCount - 1) {
$page = $pageCount - 1;
}
$buttons[] = $this->renderPageButton($this->nextPageLabel, $page, $this->nextPageCssClass, $currentPage >= $pageCount - 1, false);
}
// last page
$lastPageLabel = $this->lastPageLabel === true ? $pageCount : $this->lastPageLabel;
if ($lastPageLabel !== false) {
$buttons[] = $this->renderPageButton($lastPageLabel, $pageCount - 1, $this->lastPageCssClass, $currentPage >= $pageCount - 1, false);
}
return Html::tag('div', implode("\n", $buttons), $this->options);
}
}
Ещё одно интересное решение для пагинации в Yii2.
Предыдущая запись
Yii2: Как вывести ошибки при валидации формыСледующая запись
Создание и управление псевдонимами записей в Yii2