Pagination with Zend\Paginator

Pagination with Zend\Paginator

Time to apply pagination for limiting items per page.

pagination.png
Parent post: Online Book Catalog Application Tutorial
Book records in the Online Database Catalog database will increase by the nature of the application. Therefore I need to implement Pagination to let visitors navigate between a limited number of items per page.

So far I have a table listing of the books in the catalog index page, and it is the worst design for a book catalog index as you can see. I know this is not a design tutorial but I'd like to at least not hate what I look at.

Before jumping to the pagination, I will first change the catalog index view template so instead of looking at that boring list, books will be displayed in relatively more eye-friendly boxes.

# /module/Application/view/application/catalog/index.phtml

<h1>Book Listing</h1>
<div class="row">
    <?php /* @var $book \Application\Model\Book\BookEntity */?>
    <?php foreach ($this->books as $book): ?>
    <div class="col-sm-6 col-md-4 small">
        <div class="panel panel-default">
            <div class="panel-heading">
                <h5><?= $book->getTitle() ?></h5>
                <h6>by <?= $book->getAuthor() ?></h6>
            </div>
            <div class="panel-body">
                <table class="table table-condensed">
                    <colgroup>
                        <col class="col-xs-5">
                        <col class="col-xs-9">
                    </colgroup>
                    <tr>
                        <th>Author:</th><td><?= $book->getAuthor() ?></td>
                    </tr>
                    <tr>
                        <th>Published on:</th><td><?= $book->getPublishDate() ?></td>
                    </tr>
                    <tr>
                        <th>Language:</th><td><?= $book->getLanguage() ?></td>
                    </tr>
                    <tr>
                        <th>ISBN:</th><td><?= $book->getIsbn() ?></td>
                    </tr>
                </table>
            </div>
        </div>
    </div>
    <?php endforeach; ?>
</div>

In fact, I also want to list the books in publish date order. To do this I will change the fetchAll() method of the BookMapper and use Zend\Db\Sql\Select to build the sql select statement instead of selecting all records with TableGateway object's select() method. Therefore I can simply use order() method of the select object.

# /module/Application/src/Model/Book/BookMapper.php

<?php
namespace Application\Model\Book;
 
use Zend\Db\TableGateway\TableGateway;
use Zend\Db\Sql\Select;

class BookMapper
{
    protected $tableGateway;
    
    /**
     * Constructor
     * @param TableGateway $tableGateway
     */
    public function __construct(TableGateway $tableGateway)
    {
        $this->tableGateway = $tableGateway;
    }
    
    /**
     * FetchAll for Book Model
     * 
     * @return array \Application\Model\Book\BookEntity[]
     */    
    public function fetchAll()
    {
        $select = new Select($this->tableGateway->table);
        $select->order('publish_date DESC');
        
        $rowset = $this->tableGateway->selectWith($select);
        
        return $rowset;
    }
}

The Online Book Catalog index page currently looks like the following after these changes.

New catalog index page look
Current look of the catalog index.

Book record count in the database will increase by the nature of the application. Therefore I am going to include Zend\Paginator component into my application to provide a proper pagination.


[smozgur@local OnlineBookCatalog]$ composer require zendframework/zend-paginator

A necessary module is installed and injected into the modules.config.php as I selected that option during the module installation above. Now I can change the fetchAll() method in the mapper to return a paginator object consisting of book entity objects instead of previously returned book entities array. Method will also take two parameters namely $pageNumber representing requested page number and $count defining the total item count per page.

# /module/Application/src/Model/Book/BookMapper.php

<?php
namespace Application\Model\Book;
 
use Zend\Db\TableGateway\TableGateway;
use Zend\Db\Sql\Select;
use Zend\Paginator\Paginator;
use Zend\Paginator\Adapter\DbSelect;

class BookMapper
{
    protected $tableGateway;
    
    // ... original constructor 
    
    /**
     * FetchAll for Book Model
     * 
     * @param int $pageNumber
     * @param int $count
     * @return \Zend\Paginator\Paginator \Application\Model\Book\BookEntity[]
     */
    public function fetchAll($pageNumber = 1, $count = null)
    {
        $select = new Select($this->tableGateway->table);
        $select->order('publish_date DESC');
        
        /**
         * Creating paginator adapter by using DBSelect method
         * of Zend\Paginator\Adapter
         * Paginator adapter will contain book entity objects
         * since DbSelect is also injected with the TableGateway's
         * ResultSet prototype
         * 
         * @var \Zend\Paginator\Adapter\DbSelect $paginatorAdapter
         */
        $paginatorAdapter = new DbSelect($select, 
            $this->tableGateway->adapter, 
            $this->tableGateway->getResultSetPrototype());
        
        /**
         * Actual paginator consist of hydrated book entity objects
         * 
         * @var \Zend\Paginator\Paginator $paginator
         */
        $paginator = new Paginator($paginatorAdapter);
        
        /**
         * Setting item count per page if it is defined
         * If no count specified then all records will be returned 
         */
        if ($count) {
            $paginator->setDefaultItemCountPerPage($count);
        }
        
        /**
         * Retrieve only items in the requested page
         * by setting the current page number
         */
        $paginator->setCurrentPageNumber($pageNumber);
        
        /**
         * Paginator object consist of book entity objects 
         * is ready to be returned
         */
        return $paginator;
    }
}
I assign $count parameter to be null as default, in case it is not provided. That's because I use a similar constructor for my model mappers and some of them return a finite number of items, so I wouldn't need pagination for those. This is kind of a way of providing myself an option to be able to return all records. As long as I give the $count parameter, returned records will be divided into pages accordingly.

Now the listing page will contain a limited number of books in every page. I will update the associated controller which is CatalogController to provide the defined limit and also the requested page.

# /module/Application/src/Controller/CatalogController.php

<?php
namespace Application\Controller;

use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;
use Application\Model\Book\BookMapper;

/**
 * CatalogController
 * 
 * Controller for Catalog Model
 */
class CatalogController extends AbstractActionController
{
    /**
     *
     * @var BookMapper $bookMapper
     */
    private $bookMapper;
    
    public function __construct(BookMapper $bookMapper)
    {
        $this->bookMapper = $bookMapper;
    }
    
    /**
     * 
     * Action called for displaying the book listing
     * 
     * {@inheritDoc}
     * @see \Zend\Mvc\Controller\AbstractActionController::indexAction()
     */
    public function indexAction()
    {
        // Getting requested page number from the query 
        $pageNumber = (int) $this->params()->fromQuery('page', 1);
        
        // Set total item count per page for pagination
        $count = 3;
        
        $books = $this->bookMapper->fetchAll($pageNumber, $count);

        return new ViewModel([
            'books' => $books,
        ]);       
    }
}
I used the $count variable to pass the total item count per page.This value could be actually used application wide and you can visit one of my previous posts Controller Plugin for Configuration Array Access to implement it here.

$pageNumber is retrieved as a url query value that is named as page. I prefer to separate page number from the actual route, so I pass it as query in the url. Therefore I use the url to view the first page with ?page=1 suffix as shown in Fig2.

Paginated catalog index page
Catalog index page after pagination.

Everything looks good however, how will I go to the second page other than changing the page parameter in the url?

I need to implement pagination links preferably at the bottom of the listing and I will do this by using the PaginationControl view helper.

PaginationControl view helper requires 4 parameters in the following order:

  • Paginator object contains book entities
    • $this->books variable passed to view in controller
  • One of the following scrolling style of the paginator
    • All
    • Elastic
    • Jumping
    • Sliding
  • Partial view script that defines how paginator looks
    • /partial/paginator.phtml
  • Additional custom parameters

I should obviously start with creating the partial view. I will create the view at /module/Application/view/partial/ folder and it could be used by the other controllers as well as the view helpers in other views as well. Therefore, I will have an application-wide paginator view.

# /module/Application/view/partial/paginator.phtml

<?php if ($this->pageCount): ?>
    <ul class="pagination pagination-sm">
        <?php if (!isset($this->previous)): ?>
            <li class="disabled">
                <a>&laquo;</a>
                <a>&lsaquo;</a>
            </li>
        <?php else: ?>
            <li>
                <a href="<?= $this->url($this->route, [], ['query' => ['page' => $this->first]]); ?>">&laquo;</a>
                <a href="<?= $this->url($this->route, [], ['query' => ['page' => $this->previous]]); ?>">&lsaquo;</a>
            </li>
        <?php endif; ?>
        
        <!-- Numbered page links -->
        <?php foreach ($this->pagesInRange as $page): ?>
            <?php if ($page == $this->current): ?>
                <li class="active">
                    <a><?= $page ?></a>
                </li>
            <?php else: ?>
                <li>
                    <a href="<?= $this->url($this->route, [], ['query' => ['page' => $page]]); ?>">
                        <?= $page; ?>
                    </a>
                </li>
            <?php endif; ?>
        <?php endforeach; ?>
    
        <?php if (!isset($this->next)): ?>
            <li class="disabled">
                <a>&rsaquo;</a>
                <a>&raquo;</a>
            </li>
        <?php else: ?>
            <li>
                <a href="<?= $this->url($this->route, [], ['query' => ['page' => $this->next]]); ?>">&rsaquo;</a>
                <a href="<?= $this->url($this->route, [], ['query' => ['page' => $this->last]]); ?>">&raquo;</a>
            </li>
        <?php endif; ?>
    </ul>
<?php endif; ?>

Note that URL Helper's 3rd parameter contains query element defining the custom named page query value in the generated URL.

And finally, I can add the pagination control into the index view template.

# /module/Application/view/application/catalog/index.phtml

<h1>Book Listing</h1>
<div class="row">
    // ...
</div>
<div>
<?= $this->paginationControl(
            $this->books,     // Paginator
            'Sliding',           // Style
            'partial/paginator', // You can add file extension but helper actually takes care about it  
            []                   // Parameters - none for this time
        );
?>
</div>

Current look of the index page displaying the second page book items.

Pagination partial applied
Partial view template applied paginator.

Current module directory structure:

Application directory structure with partial view template folder
Current application module directory with the partial view template folder

Published on Jan 04, 2017