A free and open-source book on ZF3 for beginners


12.15. Пагинация (Pagination)

Пока что у нас всего несколько постов на нашей странице Posts. Но представьте, что там будет много (сотни) постов. Страница будет загружаться очень долго. Также будет очень неудобно прокручивать страницу, чтобы прочитать все посты. В такой ситуации нам будет полезна пагинация (pagination).

Пагинация - это когда вы делите ваши результаты на страницы и перемещаетесь по страницам, кликая на ссылки с номерами страниц на виджете пагинации.

Пример виджета пагинации, стилизованного при помощи Twitter Bootstrap представлен ниже:

Рисунок 12.12. Пример виджета пагинации Рисунок 12.12. Пример виджета пагинации

12.15.1. Пагинатор из Doctrine ORM

Компонент Doctrine\ORM предоставляет класс пагинатора под названием Paginator, живущий в пространстве имен Doctrine\ORM\Tools\Pagination. Он берет на вход объект Doctrine Query и предоставляет несколько методов, чтобы разбить результаты на страницы (для простоты, мы не будем обсуждать здесь эти методы). Но модуль Doctrine ORM не предоставляет никаких помощников вида, чтобы визуализировать пагинатор. Для этих целей мы можем воспользваться функциональностью для пагинации, которую предоставляет нам Zend Framework 3.

Хотя мы и планируем использовать компонент для пагинации из ZF3, мы все равно будем внутренне использовать пагинатор Doctrine ORM, чтобы подавать ему данные. Пагинатор ZF3 будет всего лишь оберткой для пагинатора Doctrine ORM.

12.15.2. ZF3 Paginator

В Zend Framework 3, пагинация реализована внутри компонента Zend\Paginator. Если вы еще не установили этот компонент, сделайте это сейчас, набрав следующую команду Composer:

php composer.phar require zendframework/zend-paginator

Компонент Zend\Paginator предоставляет класс Paginator. Его наиболее полезные методы представлены в таблице 12.6:

Таблица 12.6. Методы класса Paginator из ZF3
Метод Описание
setDefaultScrollingStyle($scrollingStyle = 'Sliding') Устанавливает стиль скроллинга.
setDefaultItemCountPerPage($count) Устанавливает максимальное количество результатов на страницу.
setCurrentPageNumber($pageNumber) Устанавливает текущий номер страницы.
count() Возвращает количество страниц.
getTotalItemCount() Возвращает общее количество результатов.
getCurrentItems() Возвращает элементы с текущей страницы.

Класс Paginator является очень "общим" и не знает ничего о нижележащей модели данных, так что вам нужно иметь адаптер, который предоставляет данные пагинатору. Модуль DoctrineORMModule предоставляет такой адаптер (класс DoctrineORMModule\Paginator\Adapter\DoctrinePaginator), который мы можем использовать, чтобы взять данные из пагинатора ORM и передать их в пагинатор ZF3.

Для примера, предположим, что у нас есть объект класса Query с DQL-запросом, который выбирает все опубликованные посты. Чтобы разбить результаты на страницы, мы можем использовать следующий код:

<?php 
use DoctrineORMModule\Paginator\Adapter\DoctrinePaginator as DoctrineAdapter;
use Doctrine\ORM\Tools\Pagination\Paginator as ORMPaginator;
use Zend\Paginator\Paginator;

// Создаем ZF3 пагинатор.
$adapter = new DoctrineAdapter(new ORMPaginator($query, false));
$paginator = new Paginator($adapter);

// Устанавливаем номер страницы и размер страницы.
$paginator->setDefaultItemCountPerPage(10);        
$paginator->setCurrentPageNumber(1);

// Проходим по результатам с текущей страницы.
foreach ($paginator as $post) {
    // Делаем что-нибудь с текущим постом.
}

Теперь давайте применим этот пример к нашему блогу. Мы хотим отображать "пагинированные" посты на главной странице вебсайта Blog.

12.15.2.1. Модифицируем PostRepository

Прежде всего нам потребуется немного изменить подход, с которым мы получаем список постов из репозитория. Раньше мы пользовались методом findBy() класса EntityRepository, который возвращает массив постов. Но теперь нам придется делать это с помощью метода нашего кастомного репозитория PostRepository, потому что пагинатор Doctrine ORM берет на вход объект Query, а не массив постов:

Добавьте следующий метод к классу PostRepository:

public function findPublishedPosts()
{
    $entityManager = $this->getEntityManager();
    
    $queryBuilder = $entityManager->createQueryBuilder();
    
    $queryBuilder->select('p')
        ->from(Post::class, 'p')
        ->where('p.status = ?1')
        ->orderBy('p.dateCreated', 'DESC')
        ->setParameter('1', Post::STATUS_PUBLISHED);
    
    return $queryBuilder->getQuery();
}

И измените метод findPostsByTag() репозитория, так как мы также хотим, чтобы он возвращал объект Query вместо array:

public function findPostsByTag($tagName)
{
    $entityManager = $this->getEntityManager();
    
    $queryBuilder = $entityManager->createQueryBuilder();
    
    $queryBuilder->select('p')
        ->from(Post::class, 'p')
        ->join('p.tags', 't')
        ->where('p.status = ?1')
        ->andWhere('t.name = ?2')
        ->orderBy('p.dateCreated', 'DESC')
        ->setParameter('1', Post::STATUS_PUBLISHED)
        ->setParameter('2', $tagName);
    
    return $queryBuilder->getQuery();
}

Вам также придется немного изменить код метода PostManager::getTagCloud() так как он ожидает массив постов, а теперь он будет получать объект Query. Это довольно простая модификация, так что мы не будем ее описывать здесь (обратитесь к примеру Blog для полного кода).

12.15.2.2. Модифицируем IndexController

Затем измените метод действия indexAction() контроллера IndexController следующим образом:

<?php
namespace Application\Controller;

// Add aliases for paginator classes
use DoctrineORMModule\Paginator\Adapter\DoctrinePaginator as DoctrineAdapter;
use Doctrine\ORM\Tools\Pagination\Paginator as ORMPaginator;
use Zend\Paginator\Paginator;
// ...

class IndexController extends AbstractActionController 
{
    // ...
    
    public function indexAction() 
    {
        $page = $this->params()->fromQuery('page', 1);
        $tagFilter = $this->params()->fromQuery('tag', null);
        
        if ($tagFilter) {
         
            // Фильтруем посты по тегу
            $query = $this->entityManager->getRepository(Post::class)
                    ->findPostsByTag($tagFilter);
            
        } else {
            // Получаем недавние посты
            $query = $this->entityManager->getRepository(Post::class)
                    ->findPublishedPosts();
        }
        
        $adapter = new DoctrineAdapter(new ORMPaginator($query, false));
        $paginator = new Paginator($adapter);
        $paginator->setDefaultItemCountPerPage(1);        
        $paginator->setCurrentPageNumber($page);
                       
        // Получаем популярные теги.
        $tagCloud = $this->postManager->getTagCloud();
        
        // Отображаем шаблон представления.
        return new ViewModel([
            'posts' => $paginator,
            'postManager' => $this->postManager,
            'tagCloud' => $tagCloud
        ]);
    }
}

Вы можете заметить, что в строке 16 мы получаем номер текущей страницы (page) из параметра GET. Так что вы можете установить номер страницы результатов путем ввода следующего URL в вашем браузере: "http://localhost/application/index?page=<page>". Номер страницы по умолчанию - 1.

В строках 22 и 27 мы извлекаем объект Query из нашего репозитория PostRepository вместо массива (array) постов. Мы затем передаем этот объект Query пагинатору ZF3 в строке 31.

В строках 33-34 мы устанавливаем текущий номер страницы пагинатора и размер страницы.

В строке 41 мы передаем пагинатор (!) вместо массива постов в шаблон представления.

12.15.2.3. Визуализация виджета пагинатора

Теперь осталась последняя по порядку (но не по значению) часть работы. Нам нужно отобразить виджет пагинации в нашем шаблоне представления.

Чтобы сделать это с привлекательным оформлением Bootstrap, нам нужно будет сначала создать частичный шаблон представления view/application/partial/paginator.phtml и поместить следующий код в этот файл:

<?php if ($this->pageCount): ?>

<nav>
<ul class="pagination">

<!-- Previous page link -->
<?php if (isset($this->previous)): ?>
    <li>
      <a href="<?= $this->url($this->route, [], ['query'=>['page'=>$this->previous]]); ?>" aria-label="Previous">
        <span aria-hidden="true">&laquo;</span>
      </a>
    </li>  
<?php else: ?>
    <li>
        <span aria-hidden="true">&laquo;</span>      
    </li>  
<?php endif; ?>

<!-- Numbered page links -->
<?php foreach ($this->pagesInRange as $page): ?>
  <?php if ($page != $this->current): ?>
    <li>
        <a href="<?= $this->url($this->route, [], ['query'=>['page'=>$page]]); ?>"><?= $this->escapeHtml($page); ?></a>
    </li>    
  <?php else: ?>
    <li>
        <span aria-hidden="true"><b><?= $this->escapeHtml($page); ?></b></span> 
    </li>    
  <?php endif; ?>
<?php endforeach; ?>

<!-- Next page link -->
<?php if (isset($this->next)): ?>
    <li>
      <a href="<?php echo $this->url($this->route, [], ['query'=>['page'=>$this->next]]); ?>" aria-label="Next">
        <span aria-hidden="true">&raquo;</span>
      </a>
    </li>  
<?php else: ?>
    <li>
        <span aria-hidden="true">&raquo;</span>      
    </li> 
<?php endif; ?>
</ul>
</nav>
<?php endif; ?>

Частичный шаблон представления будет использован помощником представления PaginationControl ZF3, чтобы визуализировать виджет пагинатора. Как вы можете увидеть, этот частичный шаблон представления берет на вход несколько переменных ($pageCount, $pagesInRange, $current, $previous, $next, $route), которые используются, чтобы определить как будет выглядеть виджет.

Теперь давайте отобразим виджет в нашем шаблоне представления view/application/index/index.phtml под списком постов следующим образом:

<?= $this->paginationControl($posts,
            'Sliding',
            'application/partial/paginator', 
            ['route' => 'application']); ?>

Помощник представления PaginationControl принимает четыре аргумента:

12.15.2.4. Результаты

Ура! Наш блог готов теперь. Главная страница вебсайта теперь содержит красивый виджет пагинации (смотрите рис. 12.13 ниже):

Рисунок 12.13. Главная страница с пагинатором Рисунок 12.13. Главная страница с пагинатором

В этой главе мы реализовали только простую пагинацию. Для реальных вебсайтов, дополнительно к пагинации, вы можете также захотеть интерактивно сортировать ваши результаты по конкретным полям. Для такого сложного отображения табличных данных, рассмотрите возможность использования jQuery-плагина DataTables или BootGrid.


Top