Mastering ProcessWire’s Selector Engine: $pages->find(), Limits, and Pagination
Learn how to use ProcessWire’s $pages->find() with selector strings, limits, and built‑in pagination. The article covers the selector syntax, a live example, performance limits, and common pitfalls to avoid.
29 Sept 2025, 10:23 UTC

Why $pages->find() Is the Go‑to Query Method
In ProcessWire 3.x, every page listing or filter is built on the selector engine. The $pages->find() method accepts a comma‑separated string of field tests and returns a PageArray that you can iterate over or render with built‑in helpers. This eliminates the need for hand‑written SQL in most cases, keeps queries cache‑friendly, and automatically respects user permissions.
The Selector Syntax in a Nutshell
A selector string is parsed by PageFinder into a parameterised SQL query. Each comma separates a test:
template=article– only pages with the specified template.sort=-published– order by thepublishedfield descending.limit=10– return at most ten results.- Operators:
=,!=,<,>,*=(contains phrase),%=(contains words),|for OR groups.
All values are automatically escaped, but if you inject user input you must sanitise it first.
Worked Example: A Paginated News List
In a template file (e.g., templates/news-list.php) you might write:
// Grab the current page number from the URL, defaulting to 1
$pageNum = (int) $input->pageNum ?: 1;
// Build the selector string
$selector = 'template=news, parent=/news/, sort=-date, limit=10';
// Execute the query
$items = $pages->find($selector);
// Render the list
foreach ($items as $item) {
echo '' . $item->title . '
';
echo '' . $item->summary . '
';
}
// Render the pager – this uses the MarkupPagerNav module
if ($items->count() > 10) {
echo $items->renderPager();
}
Key points:
- The
limit=10selector forces a maximum of tenPageobjects to be created, keeping memory usage low. - The
$input->pageNumvariable automatically adjusts the SQLOFFSETwhen$items->renderPager()is called, so you don’t need to calculate offsets yourself. - Because
$pages->find()respects access control, guests will see only published and visible news items; admins will see all.
Performance Considerations
Every $pages->find() call instantiates a Page object for each match. If you query a very large tree without a limit or with a huge limit, you may exhaust PHP memory. The recommended pattern is:
- Always include a
limitthat matches the UI (e.g., 10 or 20). - If you only need field values and not full
Pageobjects, use$pages->findRaw()(available since 3.0.9). This returns an array of field values without instantiating Page objects. - For admin‑only reports that need all data, consider caching the result or using a custom fieldtype that stores the data in a single table.
Common Mistakes to Avoid
- Unsanitised User Input: Directly interpolating
$input->get('tag')into a selector can lead to unintended matches or SQL injection. Always sanitise with$sanitizer->pageName()or$sanitizer->fieldName()first. - Using
$pages->get()Instead offind():get()retrieves a single page or returns aNullPageif not found. It does not support limits or sorting. - Ignoring Hidden Pages for Guests: Even if a selector matches a page,
$pages->find()will exclude hidden pages for non‑admin users. If you need to override this, use$pages->find('template=article, showHidden=1')but be cautious of permission leaks. - N+1 Query Problem: Accessing a field that triggers a lazy load inside a loop (e.g.,
$item->author->name) can cause an extra query per item. Cache the related page or use$item->author->nameonce per loop if possible. - Not Checking
$items->count()Before Rendering the Pager
Limitations of the Selector Engine
| Limitation | Work‑around |
|---|---|
| Cannot reference non‑indexed fields in a selector | Use $pages->findRaw() or custom SQL |
Wildcard * is not full‑text search | Use wire('search')->search() for advanced queries |
| Complex joins across multiple modules are hard | Create a custom fieldtype or database view |
| Pagination offset is calculated automatically; hard to change | Use $pages->find('limit=10, offset=20') for manual control |
Practical Checklist Before Deploying a Selector Query
- Confirm the target ProcessWire version (syntax may differ between 2.x and 3.x).
- Test the selector as an admin and as a guest to verify access filtering.
- Measure memory usage with
memory_get_peak_usage()for large limits. - Validate that
$items->renderPager()outputs correct links; adjust the MarkupPagerNav module if needed. - Sanitise any user‑supplied values before concatenation into the selector string.
Conclusion
ProcessWire’s selector engine, accessed via $pages->find(), remains the most powerful, portable, and cache‑friendly way to query pages. By applying limits, using $input->pageNum for pagination, and being mindful of the engine’s quirks, you can build robust page listings without writing raw SQL. Keep an eye on memory usage and guard against common pitfalls, and you’ll have a clean, maintainable solution that scales with your site’s growth.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.