Querying posts
5 min read
Field values live in real table columns, so WP_Query can filter and sort on them with an indexed JOIN instead of scanning wp_postmeta. You use three extra WP_Query arguments:
Argument | Purpose |
|---|---|
| Conditions on field values |
| Sort by a field |
| Distance search around a point (Map fields) |
$query = new WP_Query( [
'post_type' => 'listing',
'posts_per_page' => 12,
'wpmcf_query' => [
'relation' => 'AND',
[ 'field' => 'listing.price', 'compare' => '<=', 'value' => 500 ],
[ 'field' => 'listing.features', 'compare' => 'CONTAINS', 'value' => 'garden' ],
],
'orderby' => [ 'wpmcf:listing.price' => 'ASC', 'date' => 'DESC' ],
] );Everything here applies to posts (any post type) on WP_Query. Groups attached only to users, terms or comments cannot be queried this way: a group must have location rules that attach it to posts, otherwise the query matches nothing.
wpmcf_query_posts()
wpmcf_query_posts( array $args ): array // WP_Post[] or int[] with 'fields' => 'ids'get_posts() with suppress_filters forced to false, which the field query needs. Returns an empty array when the query fails (and logs it). Use it instead of get_posts():
$posts = wpmcf_query_posts( [
'post_type' => 'listing',
'numberposts' => -1,
'wpmcf_query' => [ [ 'field' => 'listing.price', 'value' => 20 ] ],
] );Field references
A reference is "group.field", optionally followed by keys or list indexes into a JSON value:
listing.price
listing.details.city (Group field)
listing.tags.0 (list item)Every segment must match [a-z0-9_] (1 to 64 characters). Only fields that have a column work; Password fields cannot be queried or sorted (a query on one matches nothing).
wpmcf_query
An array of conditions plus optional relation:
'wpmcf_query' => [
'relation' => 'OR', // 'AND' (default) or 'OR'
[ 'field' => 'listing.price', 'compare' => '>', 'value' => 100, 'type' => 'NUMERIC' ],
[ 'relation' => 'AND', [ ... ], [ ... ] ], // nested group
]Clause keys
Key | Description |
|---|---|
| The reference (required) |
| A scalar, or an array for |
| One of the operators below, case-insensitive. Default |
| Cast of the column before comparing. See below. |
Operators
Operator | Meaning |
|---|---|
| Comparison with a single value |
| Value is (not) one of the list. An empty |
|
|
| Substring match (the value is wrapped in |
| The column is not |
| List fields (Checkbox, Relationship, Taxonomy, User) match an item ( |
Any other operator (REGEXP, ...) is an error (see "Invalid arguments").
Types
type casts the column before comparison: NUMERIC (becomes SIGNED), DECIMAL (means DECIMAL(65,10)) or DECIMAL(M,D) (M and D up to two digits, D <= M, for example DECIMAL(10,2)), SIGNED, UNSIGNED, CHAR, BINARY, DATE, DATETIME, TIME. Number columns are already numeric; use DATE/DATETIME for date strings you compare to a literal. Casting a number to CHAR gives its decimal text (a stored 20 becomes '20.000000'), so = against '20' does not match; avoid it for numbers.
Verified results
Three posts with price 10, 20 and 30, plus posts without a value:
Clause | Matches |
|---|---|
| the 20 |
| 10 and 30 (posts without a value are not matched) |
| 20, 30 |
| 20, 30 |
| 10 |
| 10, 30 |
| 20 |
| the three with a value |
| the posts without a value |
| rows containing b/B (case-insensitive collation) |
| rows whose list holds |
| rows without |
path | by the Group's |
Limits
- Nesting depth: 5 levels. Deeper queries are invalid.
- At most 30 conditions per query.
- Every referenced field group is joined once.
Ordering
Add wpmcf:group.field keys to orderby:
'orderby' => [ 'wpmcf:listing.price' => 'DESC', 'title' => 'ASC' ]
'orderby' => 'wpmcf:listing.price', 'order' => 'DESC' // string form- Field ordering comes first, then WordPress's own ordering.
- Empty values sort last in both directions.
- Up to 5
wpmcf:keys per query (further ones are ignored). - Direction is
ASCorDESC; anything else isDESC(a plainorderapplies when you use the string form:ASCorDESC). - Password fields cannot be used (invalid query).
Distance search
For a Map field:
'wpmcf_distance' => [
'field' => 'listing.place',
'lat' => 40.7,
'lng' => -74.0,
'radius' => 50, // optional; km by default
'unit' => 'km', // 'km' (default) or 'mi'
],
'orderby' => 'wpmcf_distance', 'order' => 'ASC',- Only posts with coordinates are returned.
- With
radius, results are limited to that distance (a bounding box first, so an index over the latitude/longitude columns helps). Without it all posts with coordinates are returned. - Every returned post carries
$post->wpmcf_distance(in the chosen unit; verified1.4km and33.36km for two posts near New York). orderby => 'wpmcf_distance'sorts by it and needs thewpmcf_distanceargument.latmust be between -90 and 90,lngbetween -180 and 180,radiuspositive.
Invalid arguments
An argument that cannot be used (unknown group or field, unknown operator, BETWEEN without two values, an array value for =, a Password field, a non-map field for distance, bad latitude, too many conditions, nesting too deep, a group that is not attached to posts) makes the query match nothing (AND 0 = 1) and raises a _doing_it_wrong() notice (visible with WP_DEBUG). A typo therefore never lists every post. All values are bound with $wpdb->prepare(); identifiers are validated, so a value like x' OR 1=1 -- or a field reference with a backtick matches nothing instead of injecting.
Main-query ordering is protected
orderby is a public query variable (?orderby=wpmcf:listing.price). On the front-end main query, field ordering is honored only when code set it, not when the visitor's URL asked for it. This stops anyone sorting by arbitrary columns (including private ones) from the address bar. Verified: with ?orderby=wpmcf:... the main query ignores the field order. To allow it deliberately:
add_filter( 'wpmcf/query/public_orderby', '__return_true' );Secondary queries (new WP_Query, wpmcf_query_posts()) and admin queries are not affected.
Examples
// Price range, newest first, with pagination.
$q = new WP_Query( [
'post_type' => 'listing',
'paged' => max( 1, get_query_var( 'paged' ) ),
'wpmcf_query' => [
[ 'field' => 'listing.price', 'compare' => 'BETWEEN', 'value' => [ 100000, 500000 ] ],
[ 'field' => 'listing.status', 'value' => 'for_sale' ],
],
] );
// Opening on or after a date (Date picker column).
$q = new WP_Query( [
'post_type' => 'event',
'wpmcf_query' => [ [ 'field' => 'event.starts', 'compare' => '>=', 'value' => gmdate( 'Y-m-d' ), 'type' => 'DATE' ] ],
'orderby' => [ 'wpmcf:event.starts' => 'ASC' ],
] );
// Nearest stores.
$stores = wpmcf_query_posts( [
'post_type' => 'store',
'numberposts' => 10,
'wpmcf_distance' => [ 'field' => 'store.place', 'lat' => $lat, 'lng' => $lng, 'radius' => 25 ],
'orderby' => 'wpmcf_distance',
'order' => 'ASC',
] );
foreach ( $stores as $store ) {
printf( '%s: %.1f km', esc_html( $store->post_title ), $store->wpmcf_distance );
}Notes
- A post without a row in the group's table has
NULLfor every field, soNOT EXISTS,NOT CONTAINSand!=-style negations behave as the table above shows. - Pro's admin-column filters build on the same
wpmcf_queryargument.
Still stuck? Contact support