Phalcon Paginator QueryBuilder adapter transition from v3 to v4
0 reputation · 04 Aug 2022, 12:23 UTC
0 reputation · 04 Aug 2022, 12:23 UTC
Phalcon 4 introduced significant architectural changes to its namespace structure, moving away from the tighter coupling found in version 3. This shift affects how the Phalcon\Paginator and its associated adapters, such as the QueryBuilder adapter, are instantiated and utilized within the ORM.
When implementing pagination for large datasets, the QueryBuilder adapter relies on database-level OFFSET and LIMIT clauses. As the page index increases, the performance of these queries typically degrades regardless of the framework's C-extension efficiency, creating a bottleneck during the transition to higher page numbers.
There is uncertainty regarding the most efficient way to maintain result count accuracy when utilizing complex joins within the updated Phalcon 4 Paginator architecture without triggering full table scans.
35775 reputation · 04 Aug 2022, 17:50 UTC
The adapter itself did not disappear in Phalcon 4 — it still lives at Phalcon\Paginator\Adapter\QueryBuilder and still expects the same three options: builder (a Phalcon\Mvc\Model\Query\Builder instance), limit, and page. What changed is how you obtain it. Instead of instantiating the adapter class directly as you did in v3, the v4 pattern routes construction through a factory pair:
use Phalcon\Paginator\PaginatorFactory;
use Phalcon\Paginator\AdapterFactory;
$factory = new PaginatorFactory(new AdapterFactory());
$paginator = $factory->newInstance('queryBuilder', [
'builder' => $builder,
'limit' => 25,
'page' => $currentPage,
]);
$page = $paginator->paginate();The second migration trap is the result object. In v3, code commonly read properties such as $page->items or $page->total_pages directly. In v4 the result exposes accessors such as getItems(), getCurrent(), getNext(), getPrevious(), getTotalItems() and getTotalPages(). The exact mix of magic properties versus methods has varied between minor v4 releases, so treat this as a verify-on-your-version item rather than a fixed rule:
$page = $paginator->paginate();
var_dump(get_class_methods($page));
var_dump($page);Two behavioral notes carry over unchanged: the builder is not executed until paginate() is called (so mutating the builder afterwards changes the result), and the adapter requires a Builder instance — a raw PHQL string or an already-executed resultset is not accepted in either version.
On your two specific questions, separate what is confirmed from what is likely:
DISTINCT on the primary key or supply a builder structured for counting. Flag this for verification against your installed minor version.LIMIT ... OFFSET ..., and the database must still scan and discard the skipped rows. No paginator configuration in v4 removes that cost. The standard mitigation is keyset (cursor) pagination — filter on id > :last_seen_id with an ORDER BY id instead of a page number — which means bypassing the offset-based adapter for those deep queries. Restricting selected columns and indexing the sort/filter columns is the cheaper partial fix if you must keep numbered pages.Because Phalcon ships as a C extension, confirm the runtime matches the API you are coding against:
php -m | grep -i phalcon
php -r 'echo Phalcon\Version::get(), PHP_EOL;'Then validate behavior with a small script: build the paginator through the factory, call paginate(), and compare getTotalItems() against a hand-written COUNT query over the same join conditions. If those disagree, the problem is in your builder's join/grouping, not in the migration.
One missing detail would sharpen this answer: your exact v4 minor version (e.g. 4.1.x vs 5.x), since result-object accessors and factory signatures shifted between releases.
Use comments to ask for clarification. Post a solution as an answer.
35,775 reputation · 04 Aug 2022, 23:25 UTC
To build on the discussion regarding join-based result counts, it is important to note that the QueryBuilder adapter in Phalcon 4 typically generates a COUNT(*) query by wrapping the original builder's logic. When complex JOIN operations are involved, this can lead to performance degradation because the database may still process the joins just to return a single integer.
If you encounter latency during the count phase of paginate(), consider these verification steps for your v4 implementation:
LEFT JOINs that don't restrict the result set), removing them from the builder before passing it to the paginator can significantly speed up the count query.OFFSET latency is prohibitive, moving away from the QueryBuilder adapter toward a custom implementation that uses a keyset pagination (seek method) is recommended, as the standard adapter is hard-coded to use LIMIT/OFFSET.