Selectors are simple strings of text that specify fields and values. These selectors are used throughout ProcessWire to find pages (and other types of data).
name=karena is a simple selector that says: “find items that have the name karena.” Selectors in ProcessWire are loosely based around the idea and syntax of attribute selectors in jQuery.
- The components of a selector
- Where do you use selectors?
- Selector fields
- Selector operators
- Selector values
- Specifying multiple selectors
- OR selectors: matching one value or another
- OR selectors, matching one field or another
- AND selectors: matching more than one value in the same field
- OR-groups: matching one group of selectors or another
- Sub-selectors: selectors within selectors
- Sorting the results of a selector
- Limiting the number of results returned by a selector
- Count selectors – finding matches by quantity
- Subfield selectors
- Finding pages that use specific templates
- Finding pages that have specific parents or ancestors
- Access control in selectors
- API variables in stored selectors
- Sanitizing user input in selectors
- Examples of selectors as used in page templates
An individual selector consists of three parts: the field you are looking for, an operator (like an equals '=' sign), and the value you want to match. For example, a basic selector might be:
In this example, "title" is the field, the equals sign "=" is the operator, and "products" is the value you want to match. This basic selector would match all pages having a title field of "products", regardless of where they exist in the site. You may also specify multiple selectors by separating each with a comma.
$pages->find("selector"); $pages->get("selector, path or ID"); $page->children("selector"); $page->siblings("selector"); $page->find("selector");
Any function that accepts a selector will accept multiple selectors split by a comma.
While you are less likely to use them, most of the other utility objects in ProcessWire, all of the array types, and several field types also accept selectors:
$matches = $templates->find("selector"); $matches = $users->find("selector"); $matches = $fields->find("selector"); $matches = $page->images->find("selector")->not("selector"); // ...and so on...
Below we examine the 3 parts of a selector in more detail: fields, operators and values.
The field portion of a selector may refer to any field of a page. If you want to know what fields you can use for matching, see "Setup > Fields" in your ProcessWire admin. All custom fields are typically optimized for fast matches.
If you want to match a value in one field or another, you may specify multiple fields separated by a pipe "|" symbol, i.e.
Using the above syntax, the selector will match any pages that have a title, name, or headline field of "products" or "Products". Selector values are not case sensitive unless you configure your MySQL with a case sensitive collation.
The operator portion of a selector may be one of the following:
= Equal to != Not equal to < Less than > Greater than <= Less than or equal to >= Greater than or equal to *= Contains the exact word or phrase ~= Contains all the words %= Contains the exact word or phrase (using slower SQL LIKE) ^= Contains the exact word or phrase at the beginning of the field $= Contains the exact word or phrase at the end of the field
These are fairly self explanatory, but the "contains" operators need some explanation. Lets start with an example:
The above example would match all pages that have the words "sushi" and "tobiko" somewhere in either their title or body fields. The words don't need to be next to one another in order to match. Whereas this selector…
...would match only those pages that had the exact phrase "sushi tobiko" present in either their title or body fields. As it's a phrase match, the words must be next to each other in that order. Here's an alternate method to do the same thing, but with some compromises and benefits:
This works exactly the same way as the
*= except that it uses a different method with the database engine. This operator has an advantage over the
*= operator when you may need to match very short words or stopwords. As a result, it can match some things that
~= rely upon MySQL fulltext indexes, which only index words of at least a certain length (configurable, but typically 4 characters). They also don't index common English words called stopwords. So while it's potentially preferable to use
~= for their index, if you aren't getting the results you need, you should consider using
%= instead. Interestingly, there are cases where
%= can be faster, and there are cases where it can be slower. Though in our experience the difference is often not measurable. Nevertheless, when doing optimization, you may want to test both to find what performs best for your case.
$= operators use a method similar to the
%= operator, and can have potentially more overhead, but can also do something unique. They can match text at the beginning (^=) or end ($=) of a field.
You'll notice we have a
!= "not equal to" operator. That's great for numbers and fields where we know the entire value, but not particularly handy for large text fields. For example, this would not be useful:
That would likely match all the pages in the site, unless you had a page that contained only the phrase "sushi tobiko" as the entire body field with nothing else in it. So that selector isn't useful, whereas, we can take a different approach to negate the result of any selector by simply preceding the field with an exclamation point "!", i.e.
This would match all pages that didn't contain the phrase "sushi tobiko" somewhere in their body field. Now that is useful.
After the operator comes the selector value that we want to match. At the basic level, little explanation is needed and the examples in the above section (operators) make that clear. But because selector values can contain nearly anything (including text submitted from user input, like a search for example), we need to take some special care with string-based selector values. If your selector value needs to contain a comma, you should surround your selector value in quotes, i.e.
If you don't surround such a selector in quotes, then ProcessWire will assume the comma is starting another selector (see specifying multiple selectors, below). Selector values may not contain double quotes as part of the value to match, unless you escape them with a backslash character "\". Because the need to match double quotes is rare, a simpler approach is just to disallow double quotes from appearing in your selector values by filtering them out of user input. See the $sanitizer->selectorValue() method for a simple way to sanitize strings going into a selector.
Selector values may contain single quotes or apostrophes. For that reason, single quotes aren't interchangeable with double quotes for surrounding selector values.
You may also specify an either/or value, by separating each of the values that may match with a pipe character "|". More details and examples can be found in the "OR selectors" section below.
A selector string can match more than one field. You may specify multiple selectors together by separating each with a comma. For example, the following selector would match all pages with a year of 2010 and the word "Hanna" appearing somewhere in the body:
You may specify as many selectors as you want as long as each is separated by a comma.
In instances where you need to match values in a single field with an either-or expression, the values should be split with the "or" operator, which is the pipe character "|". The following examples demonstrates its usage:
firstname=Mike|Steve id=123|124|125 title*=Red|Blue|Green
Each of the above selectors matches pages that have either of the values specified between the pipe "|".
This was already described in the selector fields section above, but is repeated here for reference. Field names may be separated by a pipe "|" to indicate an OR condition:
The above selector matches pages that have the word "carbonated" in either the body or sidebar fields.
There will be instances where you need to say that a specific field matches more than one selector. This is simple, just specify all the conditions you need to match as separate selectors in the same string. For example:
This AND selector matches all pages that have a "height" field greater than 500, and less than or equal to 1000.
OR-group selectors let you specify multiple expressions and only one of them has to match in order for the selector to match. It's a way of saying "either this has to match OR that has to match". This is useful because selectors by default assume AND – meaning everything has to match. While you can use the pipe "|" to specify ORs for fields or values or both, the scope of it was just that field=value statement only. ProcessWire 2.5 added OR-groups where you can create multiple selector groups and only one of them has to match. You can specify OR-groups by surrounding selectors in parenthesis.
An example demonstrates it best. Lets say that we wanted to find all "product" pages that were in stock, and either in a featured date range, or had a highlighted checkbox checked. Previously we would do like this with two separate find operations:
$items = $pages->find("template=product, stock>0, featured_from<=today, featured_to>=today"); $items->add($pages->find("template=product, stock>0, highlighted=1"));
With OR-groups, we can do it in one find operation:
$items = $pages->find("template=product, stock>0, (featured_from<=today, featured_to>=today), (highlighted=1)");
Above are two selectors surrounded in parenthesis. Only one of them has to match. You can specify as many of them as you want. This type of OR expression is supported by ProcessWire 2.5 and newer. Think of the parenthesis as a way of saying "this is optional". But of course, at least one of your parenthesized selectors has to match in order for the full selector to match.
The above usage probably covers 99% of the situations where you might need it. But lets say that you want to have different combinations of OR expressions. You can create named groups that OR with each-other by specifying:
foo=(selector1), bar=(selector2), foo=(selector3), bar=(selector4)
In the above you'd replace "foo" and "bar" with names of your choice. And you'd replace the "selector" with any selector strings. Those foo/bar names aren't referring to fields, instead they are just named groups that you can name however you want. In that selector, at least one of the "foo" named selectors would have to match, and at least one of the "bar" named selectors would have to match. If you didn't use the foo/bar named groups here (but still used the parenthesis), then only one of the 4 selectors would be required to match.
Sub-selectors let you put a selector within a selector, enabling you to perform more complex matches that you might have assumed would require separate API calls. These can be used on the 'id' property of any field that maps to a page. The 'id' property is assumed when referring to a page reference or a parent, so it's not necessary to specify it unless you want to, i.e. "field" and "field.id" mean the same thing in this case.
Sub-selectors are specified between [square brackets]. For example, lets say we are matching products and our product template has a "company" page field. Each company also has its own page field where all the company locations are identified. Lets say we want to find all products that are made by a company that has more than 5 locations and at least one of those locations has "Finland" in the title. Without sub-selectors we would have had to do it like this:
$companies = $pages->find("template=company, locations>5, locations.title%=Finland"); $items = $pages->find("template=product, company=$companies");
That's easy enough. But with sub-selectors it's even simpler, requiring only one operation:
$items = $pages->find("template=product, company=[locations>5, locations.title%=Finland]");
When you've got a
field=[value] selector, any properties you refer to in "[value]" assume the "field"; so "locations" above is referring to a property of the "company" field.
Please note: Sub-selectors work with database-driven find operations, but not with in-memory filtering (where they are less useful).
In ProcessWire 3.x, you can use nested subselectors:
template=member, invoice=[status=paid, invoice_row!=[product.color=Red]]
There is a reserved-word for selectors called "sort" and it may be used to specify what order should be returned by the matches. Here is an example of it's usage:
That essentially says to sort the results by their "title" field, A-Z. If we want to reverse the sort, we just precede "title" with a minus sign, like this:
That would return results sorted by the "title" field, Z-A.
You may specify multiple fields to sort by. For example, lets say that you wanted to sort first by date descending (newest to oldest) and then by title ascending, you would do this:
That way if two results had the same date, the secondary sort would be alphabetical.
How results are sorted if you don't specify a "sort" in your selector
$page->siblings() the results are automatically sorted by the page's default sort field that you specify in the admin. If not specified in the admin, the pages will be sorted by the order they are placed in the admin. This behavior can be overridden by specifying your own
property is any property or field name). With
$page->find(), if you don't specify your own
sort=property, the results are sorted according to MySQL's text searching relevance. If no text searches are performed in your find(), the results are unsorted. As a result, it is generally a good idea to include a
sort=property when using
$pages->find(), especially if you care about the order and your find() operation is not text/relevance related.
How to force pages to sort by their admin order with $pages->find()
$pages->find() method does not automatically sort by the order they appear in the site tree. This is because
$pages->find() is not limited to finding pages specific to one parent, so it may be pulling pages from multiple places (according to your selector). If your parent page(s) are not already sorting by a specific field, you may still tell the find() to sort by the parent's order by literally specifying
sort=sort in your selector. This is the same as saying "sort by whatever order I dragged the pages to in the admin." But note that if the results have multiple parents, the resulting order isn't likely to be that useful.
If you are dealing with a lot of potential results, you may want to limit the number of returned results for pagination or other reasons. In order to specify a limit, use the reserved word "limit" as a selector, i.e.
That selector tells ProcessWire to return 50 (or fewer) results. The starting result is always the first, unless you use the "start" reserved word, i.e.
This tells ProcessWire to return 50 results starting at the 50th (results 50–100).
If you are using a limit selector with pages, and your template has page-numbers (pagination) turned on, ProcessWire will automatically set the "start" selector according to the current page/pagination number. So when it comes to pagination, you don't usually have to think about anything other than the "limit" selector.
You can match pages with multiple-value fields based on the number of values they contain. For instance, you can find all pages with a given number of children, images, page references, files, comments, etc. You do this by appending
.count to the field name. This is best demonstrated by an example. The following example would find pages that have at least 1 child and between 3 and 5 images:
children.count>0, images.count>=3, images.count<=5
In this next example, we'll assume you have a page reference field called 'categories', and you want to match all pages that have zero (0) categories:
Note that a selector like the above will literally match all pages that have a 'categories' field without any selected. It will not match pages that don't have a 'categories' field.
The 'children' field is automatically built-in to all ProcessWire pages. It is also the only countable field where you can only specify one count operation per query. To clarify, the first example above demonstrates matching 3 to 5 'images'. You can't do this with children.count since it can only be specified once per query. This limitation may be changed at some point in the future.
Some field types support selection by subfield, where a subfield holds a value independent of the field's regular value. This is only used with field types that contain more complex data types. Examples include Page fields, Repeaters, Files, Images, Comments and Map Markers, among others. Usage in a selector is quite simple and very similar to the count selectors, mentioned in the previous section. The format goes like this:
Other than the specification of the ".subfield", the selector is no different from any other. You may use any operators or selector features that you would use elsewhere. For our first example, lets say we have a field called "images" and that you want to find all pages that have at least one image containing the word "atrium" in the image description:
Here is an example of using a subfield selector with a repeater field (or a Page or PageTable field) called buildings that contains various subfields describing the building. Lets say we want to locate all pages that have at least one building greater than 1000 feet, and at least one building built before 1980:
If you instead want to find all pages that have at least one building greater than 1000 feet that also happens to be built before 1980 (matching the same exact building, even if the page has multiple buildings) then you can specify that in the selector by preceding the field name with a "@", i.e.
You can specify that your matches should be using a specific template like this (replacing 'name' with the name of your template):
…or specify that it can have any one of these templates:
To specify that matches should have a specific parent, specify the parent's path, object or ID. First is an example of using the parent's path:
An example of using the parent's object (where $parent is an instance of a Page object):
An example of using the parent's ID (where 123 is the ID of the parent):
Of course, you can also do this with any of the above syntaxes if it can be any one of multiple parents:
Now lets say that you want to find pages that include a particular parent as an ancestor. Meaning, the found pages must include the given parent at some point in the hierarchy between them at the homepage, whether as a parent, grandparent, great-great-grandparent, etc. To do this, use has_parent.
has_parent=$parent has_parent=123 has_parent=/path/to/parent/
You can specify a Page object, Page ID, or path, like shown above.
Lastly, you can always replace the equals sign "=" in any of the above examples with "!=" to reverse the behavior. You might find it useful to find pages that don't have a given parent or ancestor:
Pages with hidden or unpublished status will not appear in the results from database-querying selectors that can return multiple pages (i.e. functions that return the PageArray type). For instance
$page->children(), etc. In addition, pages that the user does not have access to view (via access control in template settings) will also not appear in the results. All of this is usually expected and desirable behavior. However, there may be instances where you actually want to include hidden, unpublished or non-viewable pages in results. To do this, you can use the "include" or "check_access" properties in your selectors.
include=hiddenindicates that you will allow hidden pages to be included in the results.
include=unpublishedindicates that you will allow both hidden and unpublished pages to be included in the results.
include=allindicates that you don't want any exclusions and results may include hidden pages, unpublished pages, or pages that aren't viewable to the user via access control.
check_access=0indicates that you don't want access control to be a factor in the results returned. Results may include pages that the user can't view as a result of template access control settings. Pages that the user can't view only because they are unpublished will not be included. Hidden pages won't be included unless you've also used the include=hidden selector as well. Note that if you are using include=all then there is no reason to use check_access=0 since it is assumed.
These are best demonstrated by examples:
// results exclude hidden, unpublished or non-viewable pages $pages->find("template=skyscraper"); // results may include hidden pages $pages->find("template=skyscraper, include=hidden"); // results have no exclusions $pages->find("template=skyscraper, include=all"); // results may include pages user can't view $pages->find("template=skyscraper, check_access=0");
Note that $pages->get(…); is not subject to this behavior (or access control) and
include=all is assumed. This is because requesting a single page is a very specific request, and not typically used for generating navigation. To put it another way, if you are asking for a single specific page, we assume you mean it. If you want to retrieve a single page where include=all is not assumed, then use $pages->findOne(…) instead.
The examples above are all focused on including pages in results that wouldn't usually be included, among other pages. But lets say that you want to find only pages that have a hidden, unpublished or locked status. This is a fairly uncommon need, so no need to commit this to memory, but we'll include it for completeness. You can do it by querying the "status" property:
status=hiddenindicates that you only want pages with hidden status to be included in the results.
status=unpublishedindicates that you only want pages with unpublished status to be included in the results.
status=lockedindicates that you only want pages with locked status to be included in the results.
When used in a selector string, you can dynamically refer to a property/field from the current $page, $user or $session API variables like this:
field=[page.id] (including the square brackets) where the
field part is the field you want to match, the
page part is one of "page", "user" or "session", and the
id part can be any field or property available on the $page, $user or $session API variables. For $user and $page, the
id part is assumed, so if you want to use
[user.id] you can also just use
[user]. Below are a few examples of using this feature in a selector string, through please also read the text the follows.
// find pages created by current user $pages->find("created_users_id=[user.id]"); // find pages with current page selected in related_pages field $pages->find("related_pages=[page]"); // find pages matching some field you previously set in $session $pages->find("id=[session.last_viewed]"); // find pages with categories field similar to current page $pages->find("categories=[page.categories]");
From the context of PHP code (like above), there's actually little reason to use this feature since you can already directly refer to PHP and API variables in double-quoted selector strings, for example
$pages->find("related_pages=$page"); Instead, where this feature becomes useful is when the selector needs to be stored for later processing. This would be the case for anything that needs to be configured, such as a module or field, or any other configuration need that might arise. For these cases, the values in
[session] can indeed be different from $page, $user or $session, because they represent the current values at runtime (when they are processed) rather than the values at the time they were stored.
If you are supplying user input in selector values (like from a submitted form), you should sanitize the input before placing it in a selector. If you are expecting an integer for example, then type cast it as an int before using it in your selector, i.e.
$year = (int) $input->post->year; $matches = $pages->find("year=$year");
If your selector value needs to contain more arbitrary strings, like for text matching, you must sanitize the value before sending it to a selector. You may want to use ProcessWire's built in $sanitizer for that purpose, specifically $sanitizer->selectorValue(). The sanitizer will remove any characters from the selector that could be problematic. Here is how to use it from your templates:
$keywords = $sanitizer->selectorValue($input->get->keywords); $matches = $pages->find("keywords~=$keywords");
Though less common, if you are using a field name provided by user input, you should sanitize it like this with $sanitizer->selectorField():
$field = $sanitizer->selectorField($input->get->field); $matches = $pages->find("$field=the value you want to match");
Find all pages using the skyscraper template
Find all skyscrapers with a height greater than 500 ft, and less than or equal to 1000 ft.
$pages->find("template=skyscraper, height>500, height<=1000");
Find all skyscrapers in Chicago with 60+ floors, sorted by floors ascending
Find all skyscrapers built before 1950 with 10+ floors, sorted by year descending, then floors descending
$pages->find("template=skyscraper, year<1950, floors>=10, sort=-year, sort=-floors");
Find all skyscrapers by architects David Childs or Renzo Piano, and sort by height descending
$david = $pages->get("/architects/david-childs/"); $renzo = $pages->get("/architects/renzo-piano/"); $pages->find("architect=$david|$renzo, sort=-height");
Find all skyscrapers that mention the words "limestone" and "granite" somewhere in their body copy.
$pages->get("/cities/")->find("template=skyscraper, body~=limestone granite");
Find all skyscrapers that mention the phrase "empire state building" in their body copy.
$pages->get("/cities/")->find("template=skyscraper, body*=empire state building");