Selectors are simple strings of text that specify fields and values.

For example, name=karena is a simple selector that says: "find items that have the name karena." These selectors are used throughout ProcessWire to get and find pages (and other types of data). Selectors in ProcessWire are loosely based around the idea and syntax of attribute selectors in jQuery.

The components of a selector

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:

title=products

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.

Where do you use selectors?

Selectors are used throughout ProcessWire, typically for getting and finding pages with the $page and $pages variables. Below is a list of the most common functions where selectors are used:

$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.

Selector fields

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.

title|name|headline=products

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.

Selector operators

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) [v2.1]
^=  Contains the exact word or phrase at the beginning of the field [v2.1]
$=  Contains the exact word or phrase at the end of the field [v2.1]

Operators above with [v2.1] are only implemented in ProcessWire 2.1 or newer.

These are fairly self explanatory, but the "contains" operators need some explanation. Lets start with an example:

title|body~=sushi tobiko

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:

title|body*=sushi tobiko

...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 (in ProcessWire 2.1 or newer), but with some compromises and benefits:

title|body%=sushi tobiko

This works exactly the same way as the *= except that it uses a different (slower, non-indexed SQL 'LIKE') method with the database engine. While slower, 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 *= and ~= can't.

The *= and ~= 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 preferable to use *= and ~= for their speed, if you aren't getting the results you need, you should consider using %= instead (if you can handle the speed hit).

The ^= and $= operators use a method similar to the %= operator, so they are also slower, but can also do something unique. They can match text at the beginning (^=) or end ($=) of a field.

Negative operators

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:

body!=sushi tobiko

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.

!body*=sushi tobiko

This would match all pages that didn't contain the phrase "sushi tobiko" somewhere in their body field. Now that is useful.

Selector values

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 a search engine, 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.

body*="sushi, tobiko"

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.

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.

Specifying multiple selectors

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:

year=2010, body*=Hanna

You may specify as many selectors as you want as long as each is separated by a comma.

OR selectors: matching one value or another

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 "|".

OR selectors, matching one field or another

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:

body|sidebar*=carbonated

The above selector matches pages that have the word "carbonated" in either the body or sidebar fields.

AND selectors: matching more than one value in the same field

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:

height>500, height<=1000

This AND selector matches all pages that have a "height" field greater than 500, and less than or equal to 1000.

OR-groups: matching one group of selectors or another

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.

Please note: OR-groups work with database-driven find operations, but not with in-memory filtering (where they are less useful). OR-groups also require ProcessWire 2.5 or newer.

Sub-selectors: selectors within selectors

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). Sub-selectors also require ProcessWire 2.5 or newer.

Nested sub-selectors

In ProcessWire 3.x, you can use nested subselectors:

template=member, invoice=[status=paid, invoice_row!=[product.color=Red]]

Sorting the results of a selector

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:

sort=title

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:

sort=-title

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:

sort=-date, sort=title

That way if two results had the same date, the secondary sort would be alphabetical.

Only fields that have their "autojoin" checkbox checked in Setup > Fields can be used for sorting pages. So if you need to be able to sort pages by particular fields, check to make sure they are "autojoin". If they aren't already, you can set them to be.

How results are sorted if you don't specify a "sort" in your selector
In $page->children() and $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 "sort=[property]". With $pages->find() and $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()
Unlike $page->children(), the $pages->find() function 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 $pages->find() to sort by the parent's order by 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 useful.

Limiting the number of results returned by a selector

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.

limit=50 

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.

start=50, limit=50

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 turned on, ProcessWire will automatically set the "start" selector according to the current page number. So when it comes to pagination, you don't usually have to think about anything other than the "limit" selector.

Count selectors – finding matches by quantity

In ProcessWire 2.1 or later, 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:

categories.count=0

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.

Subfield selectors

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:

field.subfield=value

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:

images.description*=atrium

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:

buildings.feet_high>1000, buildings.year_built<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.

@buildings.feet_high>1000, @buildings.year_built<1980

Finding pages that use specific templates

You can specify that your matches should be using a specific template like this (replacing 'name' with the name of your template):

template=name

…or specify that it can have any one of these templates:

template=name1|name2|name3

Finding pages that have specific parents or ancestors

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:

parent=/path/to/parent/

An example of using the parent's object (where $parent is an instance of a Page object):

parent=$parent

An example of using the parent's ID (where 123 is the ID of the parent):

parent=123

Of course, you can also do this with any of the above syntaxes if it can be any one of multiple parents:

parent=$parent1|$parent2|$parent3

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. Note however that at present, you can only specify one page at a time here (meaning no has_parent=1|2|3).

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:

parent!=123
has_parent!=/path/to/parent/

Access control in selectors

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 $pages->find("..."), $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=hidden indicates that you will allow hidden pages to be included in the results.
  • include=all indicates 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=0 indicates 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.

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=hidden indicates that you only want pages with hidden status to be included in the results.
  • status=unpublished indicates that you only want pages with unpublished status to be included in the results.
  • status=locked indicates that you only want pages with locked status to be included in the results.

Sanitizing user input in selectors

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. 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:

$field = $sanitizer->selectorField($input->get->field); 
$matches = $pages->find("$field=the value you want to match"); 

Examples of selectors as used in page templates

Find all pages using the skyscraper template

$pages->find("template=skyscraper");

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

$pages->get("/cities/chicago/")->find("floors>=60, sort=floors");

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");

See Also

Comments

  • Neil Haskins

    Neil Haskins 3 years ago 66

    For "OR" selectors, you've said it's possible to match any of a set of values in a field (firstname=Mike|Steve), or any of a set of fields that have one value (body|sidebar*=carbonated). Is it possible to match any of a set of field=value sets? (firstname=Mike,|body*=carbonated)

    If so what would be the correct syntax?

    Thanks.

    • ryan

      ryan 2 years ago 95

      See the section on OR-groups above (which was not there when your comment was originally written, but support was added in ProcessWire 2.5).

  • adrian

    adrian 1 year ago 45

    Ryan - just wanted to say how great "Named OR Groups" are! Definitely a lifesaver with a complicated filtering setup I just had to build. Was just getting ready to resort to SQL when I remembered something about this.

  • Alan Bristow

    Alan Bristow 7 months ago 11

    Lovely to see PW 3 woven in here (the docs) already :)

  • SwimToWin

    SwimToWin 3 years ago 75

    date_start>=today
    date_start>=1365436783
    date_start>=2013-04-08
    date_start>=4/8/2013
    date_start>=8.4.2013
    date_start>="April 8, 2013"

    • ryan

      ryan 2 years ago 75

      To clarify for anyone else reading, all of the above are equivalent selectors since date fields accept dates in any standard date format recognized by PHP (strtotime).

  • Raul Garcia

    Raul Garcia 2 years ago 65

    $page->children("sort=random")

    Returns the children randomly ordered.

Post a Comment

Your e-mail is kept confidential and not included with your comment. Website is optional.