Yii2: Пагинация

       Yii2      yii2 advanced  •  yii2 extension  •  aJax      205    

Если у вас большое количество записей (статей, товаров ...) на одной странице, то имеет смысл воспользоваться постраничным разделением этих данных. В этой статье я покажу несколько способов создания пагинации в 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' => self::PAGE_SIZE_POSTS,
	  '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">

Тем самым мы говорим роботу о том, что на странице находится постраничное разбиение данных, что неплохо скажется на поисковой оптимизации и сведёт к минимуму появление дубликатов страниц.

Настроим маршрут в 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);
	// Передаем данные в представление
	return $this->render('view', [
	  'category' => $category,
	  'posts' => $data['posts'],
	  'pagination' => $data['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() ?>

Расширение 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>'
 ],
]);

По умолчанию, при выводе страницы находятся в ul>li. Но, что если нам нужно, чтобы ссылки на страницы были в блоке div, вместо ul>li. Для этого нужно переопределить класс LinkPager.

<?php
namespace frontend\components;
 
use Yii;
use yii\base\InvalidConfigException;
use yii\helpers\Html;
use yii\base\Widget;
use yii\data\Pagination;
 
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);
 }
}
Комментарии временно оключены

Поиск

Популярное