Choosing a laminas-paginator Adapter: DbSelect vs Array, Iterator, and Callback
A decision guide for picking a laminas-paginator adapter — DbSelect, Array, Iterator, or Callback — based on data source, count cost, and paging depth, with a working DbSelect implementation.
18 Jan 2026, 20:32 UTC

The decision: which adapter fits your data source
laminas-paginator splits pagination into two halves: an adapter that knows how to fetch a slice of data and count the total, and a view helper (PaginationControl) that renders page links. The rendering half is the same regardless of source, so the real engineering decision is the adapter. Pick wrong and you either load an entire table into memory per request or fight an adapter that was never meant for your data source.
The constraints that matter are: where the data lives, how expensive counting is, and how deep users will page. The examples below assume laminas-paginator 2.x with laminas-db on PHP 8.x; namespaces and constructor signatures have changed across major releases, so confirm against your installed version with composer show laminas/laminas-paginator.
Adapter comparison
| Adapter | Data source | Memory profile | Count cost | When to use |
|---|---|---|---|---|
| DbSelect | laminas-db Select | One page of rows | One COUNT query per request | Default for database-backed lists |
| ArrayAdapter | PHP array | Entire dataset in memory | count() on the array | Small, already-materialized collections |
| Iterator | Existing Iterator/result set | Depends on the iterator | May consume the iterator | Results from a source Select cannot express |
| Callback | Closures for items + count | You control it | You control it | APIs, search engines, custom SQL, keyset paging |
Trade-offs that bite in production
DbSelect wraps a Laminas\Db\Sql\Select, issues a COUNT query for the total, and applies LIMIT/OFFSET per page. Only the current page is hydrated, which is why it is the default for table-backed lists. Two costs to know: the COUNT query runs on every request and can be slow on large or heavily filtered tables (cache it or accept an approximate count if exact totals are not required), and OFFSET performance degrades at deep pages — scanning to offset 500,000 means reading and discarding 500,000 rows. If users routinely page deep, switch to keyset (cursor) pagination through a Callback adapter instead.
ArrayAdapter is fine for a few hundred items that already exist in memory — a config-driven list, a small lookup table. Pointing it at fetchAll() on an unbounded table defeats pagination entirely: you pay the full query and memory cost on every request.
Iterator avoids re-querying when you already hold a result set, but depending on the iterator, counting may consume it. Verify behavior with your specific result object before relying on it.
Callback is the escape hatch: you supply one closure returning the page slice and one returning the total. Use it for HTTP APIs, Elasticsearch, or raw SQL the Select builder cannot express — and for keyset pagination where DbSelect's OFFSET model breaks down.
Concrete implementation: DbSelect with clamped page numbers
Typical controller code (assumes a laminas-mvc controller with a Laminas\Db\Adapter\Adapter available; adapt to your wiring):
use Laminas\Db\Sql\Select;
use Laminas\Paginator\Paginator;
use Laminas\Paginator\Adapter\DbSelect;
public function listAction()
{
$page = (int) $this->params()->fromQuery('page', 1);
$page = max(1, $page); // clamp low; Paginator handles high out-of-range
$select = (new Select('orders'))
->where(['status' => 'open'])
->order('created_at DESC');
$paginator = new Paginator(new DbSelect($select, $this->dbAdapter));
$paginator->setCurrentPageNumber($page);
$paginator->setItemCountPerPage(25);
return ['paginator' => $paginator];
}
Always validate the page parameter. An unfiltered query string value of 0, a negative number, or a non-numeric string should never reach the paginator. Out-of-range high pages return an empty item set — harmless but confusing; consider redirecting to the last page if that matters for your UI.
In the view script, render with the Sliding scroll style, which bounds the number of page links shown — the sensible default for long result sets:
<?= $this->paginationControl(
$this->paginator,
'Sliding',
'partial/paginator.phtml',
['route' => 'orders']
) ?>
You must supply the partial/paginator.phtml partial yourself; the component ships example markup in its documentation you can adapt.
How to verify the choice works
- Confirm installed versions and PHP constraints:
composer show laminas/laminas-paginator laminas/laminas-db. Run this in the project root; no special permissions needed. - Write an integration test against a test database: seed, say, 60 rows, request page 2 with a page size of 25, and assert you get 25 items and a total count of 60.
- Inspect the SQL actually issued — via laminas-db's profiler or your database query log — and confirm each request produces one COUNT query plus one LIMIT/OFFSET query, not a full table fetch.
- Render the paginator with a large page count and check the Sliding style bounds the visible links.
Limitations
This covers OFFSET-based pagination, which suits most admin screens and shallow user-facing lists. It does not address keyset pagination implementation details, count caching strategies, or adapter behavior differences across laminas-paginator major versions — check the migration notes for your installed release before upgrading.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.