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.