Guide
Implementing Paginated Lists in Zend Framework 3 with Zend\Paginator
Step‑by‑step guide to paginate a MySQL table with Zend\Paginator in Zend Framework 3, including controller factory, action, view, verification and recovery.
Published by Tasadduq Burney
18 Apr 2026, 23:26 UTC
3 min26K views0

Desired outcome
Create a controller action that returns a paginated collection of rows from a MySQL table (e.g., users) showing 20 items per page, with navigation controls rendered in the view.
Prerequisites
- PHP 7.4+ installed.
- Zend Framework 3 (laminas/laminas-mvc) installed via Composer.
- A configured database adapter service (
Zend\Db\Adapter\Adapter) available in the service manager. - A database table with a primary key (for this guide we assume a table named
userswith columnsid,name,email). - Basic understanding of Zend MVC controllers, factories, and view models.
Procedure
-
Inject the database adapter into a factory
Create a factory for the controller that receives the adapter from the service manager.
Register the factory in// module/Application/src/Controller/Factory/UserControllerFactory.php namespace Application\Controller\Factory; use Application\Controller\UserController; use Interop\Container\ContainerInterface; use Zend\Db\Adapter\Adapter; class UserControllerFactory { public function __invoke(ContainerInterface $container) { $adapter = $container->get(Adapter::class); return new UserController($adapter); } }module/Application/config/module.config.php:'controllers' => [ 'factories' => [ Controller\UserController::class => \ Factory\UserControllerFactory::class, ], ], -
Build the controller action
In the controller, create aZend\Db\Sql\Selectobject, wrap it in aZend\Paginator\Adapter\DbSelect, instantiateZend\Paginator, and pass it to the view.// module/Application/src/Controller/UserController.php namespace Application\Controller; use Zend\Mvc\Controller\AbstractActionController; use Zend\View\Model\ViewModel; use Zend\Db\Sql\Select; use Zend\Paginator\Paginator; use Zend\Paginator\Adapter\DbSelect; class UserController extends AbstractActionController { private $adapter; public function __construct($adapter) { $this->adapter = $adapter; } public function indexAction() { // 1. Build a SELECT for the users table $select = new Select('users'); $select->columns(['id', 'name', 'email']); $select->order('id ASC'); // 2. DbSelect adapter knows how to limit/offset via paginator $paginatorAdapter = new DbSelect($select, $this->adapter); // 3. Instantiate paginator $paginator = new Paginator($paginatorAdapter); // 4. Set pagination parameters from query string (?page=) $page = $this->params()->fromQuery('page', 1); $paginator->setCurrentPageNumber((int)$page); $paginator->setItemCountPerPage(20); // 5. Pass paginator to view return new ViewModel([ 'paginator' => $paginator, ]); } } -
Create the view script
Iterate over the paginator and render the pagination control helper.
Create the partialUsers
paginationControl( $this->paginator, 'Sliding', // scroll style 'application/pagination' // partial view script ); ?> paginator as $user): ?>ID Name Email escapeHtml($user['id']) ?> escapeHtml($user['name']) ?> escapeHtml($user['email']) ?> module/Application/view/application/pagination.phtml(a simple example):pageCount, $this->current, $this->urlParamName are available if ($this->pageCount): ?>-
previous)): ?>
- « Previous pagesInRange as $page): ?>
- escapeHtml($page) ?> next)): ?>
- Next »
Expected checks
- Verify that the SQL generated for each page includes
LIMIT 20 OFFSET (page‑1)*20. Enable the Db adapter profiler temporarily to inspect the queries. - Confirm that the view displays exactly 20 rows on pages 1 and 2, and the remaining rows on the final page.
- Check that navigation links reflect correct page numbers and that clicking “Next” or “Previous” updates the
pagequery parameter. - Test error handling by supplying a non‑numeric
pagevalue (e.g.,?page=abc); the paginator should reset to page 1 without throwing an exception.
Recovery options
If the paginator returns no rows or an unexpected slice:
- Ensure the table
userscontains data and that the adapter credentials are correct. - Validate that the
Selectobject is not inadvertently modified elsewhere (e.g., by a shared service). - Temporarily remove the
DbSelectwrapper and run the raw select to confirm the base query returns the expected rows. - Check that the
itemCountPerPage value is an integer greater than zero.
Should the adapter cause memory concerns on very large tables, consider adding explicit WHERE clauses to reduce the result set before pagination, or switch to a custom paginator adapter that fetches only the needed slice.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.