Diagnosing ProcessWire $pages->find() Selector and Pagination Issues
A diagnostic guide for ProcessWire $pages->find() selector syntax and pagination issues. Covers version differences between PW 3.x and 4.x, cache interference, pager misalignment, and systematic verification steps with concrete code examples.
22 Aug 2026, 15:05 UTC

The Problem: Selectors Return Unexpected Results or Pagination Breaks
You call $pages->find("template=blog, limit=10") but get 50 results, or pagination links skip pages, or the same query returns different counts on consecutive requests. These symptoms usually trace to selector syntax mismatches between ProcessWire versions, implicit limit handling, or query caching interacting with session state. The fix is rarely a single setting—it's a systematic check of how the selector string parses, how the Query object applies limits, and whether caching layers are interfering.
Recognizable Conditions
| Symptom | Typical Trigger |
|---|---|
limit=10 ignored; all matching pages returned | Selector string parsed as field comparison instead of pagination directive |
| Offset shifts results inconsistently across requests | Session-based query cache or $wire->save() context altering query state |
| Pagination links generate duplicate or missing page numbers | MarkupPagerNav configured with mismatched limit/start values |
| Selector works in PW 3.x but fails in PW 4.x | Operator syntax changes (e.g., parent= vs parent.id=) |
Debug SQL shows unexpected WHERE clauses | Wildcard or sub-selector precedence not grouped as intended |
Cause and Diagnostic Reference
| Area | What to Check | How to Verify |
|---|---|---|
| Selector parsing | Does limit= appear before field conditions? | Enable $config->debug = true; inspect query property on returned PageArray |
| Query object methods | Are you chaining ->limit() after find()? | $pages->find("template=blog")->limit(10) vs $pages->find("template=blog, limit=10") |
| Version syntax | PW 3.x vs 4.x operator differences | Test parent=123 vs parent.id=123 in both versions |
| Cache context | Running inside $pages->save() hook or CLI script? | Call $pages->uncacheAll() before query; compare counts |
| Pager configuration | MarkupPagerNav options match query limit | Log $input->pageNum and $pages->find(...)->getTotal() |
Ordered Diagnostic Checks
- Print the parsed query. Add
$results = $pages->find("template=blog, limit=10"); bd($results->getQuery());in a template with Tracy Debugger. Confirm the SQLLIMITclause appears and matches your expectation. - Isolate selector string vs method chaining. Run both forms:
Compare// Form A: selector string $r1 = $pages->find("template=blog, limit=5, sort=-created"); // Form B: Query object methods $r2 = $pages->find("template=blog, sort=-created")->limit(5);count($r1)andcount($r2). They should match; if not, the selector string isn't parsinglimitcorrectly. - Test version-specific operators. In a fresh PW 4.x install, create two pages with parent ID 1042. Run:
PW 4.x requires explicit field notation for relation fields.$v3style = $pages->find("parent=1042"); $v4style = $pages->find("parent.id=1042"); echo "v3 count: " . count($v3style) . ", v4 count: " . count($v4style); - Clear cache and re-test. If counts fluctuate, add
$pages->uncacheAll();before the find. Also check$config->pageCacheand$config->db->queryCachesettings insite/config.php. - Verify pager alignment. When using
MarkupPagerNav, ensure the limit passed to the pager matches the query:
Mismatched limits cause duplicate or missing page links.$selector = "template=blog, limit=10"; $items = $pages->find($selector); $pager = $modules->get('MarkupPagerNav'); echo $pager->render($items, $selector);
Fixes Tied to Findings
Selector string ignores limit
Finding: Debug output shows no LIMIT in SQL.
Fix: Move pagination directives to the end of the selector string or use method chaining. ProcessWire parses selectors left-to-right; a malformed field condition earlier can consume the limit= token. Use $pages->find("template=blog, sort=-created")->limit(10) for guaranteed application.
Offset drifts between requests
Finding: uncacheAll() stabilizes the count.
Fix: Disable query cache for this selector: $pages->find("template=blog, limit=10, cache=0"). Or, if running in a saveReady hook, clone the query: $q = $pages->find("template=blog"); $q->limit(10); $q->uncache();.
PW 4.x parent selector returns zero results
Finding: parent.id= works; parent= does not.
Fix: Update all relation selectors to explicit field syntax. Create a helper:
function findChildren($parentId, $template = '') {
$sel = "parent.id=$parentId";
if ($template) $sel .= ", template=$template";
return wire('pages')->find($sel);
}
This isolates version differences in one place.
Pager shows phantom pages
Finding: getTotal() returns 103 but pager calculates 11 pages for limit=10.
Fix: Pass explicit pageNum and limit to the pager:
$pager->render($items, [
'limit' => 10,
'pageNum' => (int)$input->pageNum,
'getTotal' => true
]);
Ensure $input->pageNum is cast to int; non-numeric values default to 1 and skew calculations.
Escalation Criteria
- After clearing cache and using method-chained limits, results still differ from SQL
EXPLAINoutput → inspectwire/core/PageFinder.phpfor custom hook interference. - Pagination works in template context but fails in CLI/bootstrap script → verify
$config->useFunctionsAPIand$pagesinitialization order. - Selector works in admin but not front-end → check
$userpermissions andaccessfield on templates; admin bypasses access checks.
Practical Verification Steps
- Create a test template
test-selector.phpwith:
Run via<?php namespace ProcessWire; $pages->uncacheAll(); $results = $pages->find("template=basic-page, limit=3, sort=title"); echo "Count: " . count($results) . "\n"; echo "Total: " . $results->getTotal() . "\n"; echo "Query: " . $results->getQuery() . "\n"; foreach ($results as $p) echo "- " . $p->title . "\n";php index.php --template=test-selectorfrom site root. - Compare output with direct SQL:
SELECT * FROM pages WHERE templates_id=... ORDER BY title LIMIT 3(adjust table/column names per your schema). - Repeat on PW 3.0.229 and 4.0.x if both available; note any selector syntax that behaves differently.
Limitations
- This guide covers core
$pages->find()behavior. Modules likeProCache,RockFinder, or customPageFinderhooks can override pagination logic. - Selector caching behavior depends on
$config->db->queryCacheand$config->pageCache; defaults change between minor versions. - PW 4.x is under active development; operator syntax may shift before stable release. Always test against your target version.
Quick Reference: Selector Syntax by Version
| Operation | PW 3.x | PW 4.x (current) |
|---|---|---|
| Parent by ID | parent=123 | parent.id=123 |
| Parent by path | parent=/about/ | parent.path=/about/ |
| Template name | template=blog | template=blog (unchanged) |
| Field equals | title=About | title=About (unchanged) |
| Limit/offset in string | limit=10, offset=20 | limit=10, offset=20 (unchanged) |
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.