Queries
A page often shows a list of posts: the next five events, the events of a genre, a page of news. In plain WordPress you describe such a list as an array of arguments for WP_Query, with the names of post types, meta keys and taxonomies as strings. A typo finds nothing, and your editor cannot help you.
The framework lets you write the same query with the classes you already have. You start at the post class, add conditions on meta fields and terms, sort, and get post objects back:
Every name in this query is a class or a method, so your editor completes it and PHPStan checks it. The framework turns the query into arguments of WP_Query and runs it there. Plugins that filter queries, for example translation plugins, keep working.
How it works
Event::query() returns a PostQuery for posts of the type. A query never changes: every method returns a changed copy. You can therefore keep a query, pass it to other code and vary it without side effects:
The flip side is that a call whose result you do not keep does nothing. To make that mistake visible, every method that returns a query carries PHP's #[\NoDiscard]. PHP warns when you drop the result, and PHPStan reports the line:
Without further methods, a query finds all published posts of the type, sorted the way WordPress sorts them (newest first). The methods get(), first(), count() and paginate() run the query.
Meta fields in queries
To filter or sort by a meta field, the query needs the field as an object. A field object knows its meta class, its name and its type. The type decides which comparisons it offers: an IntField takes integers, an EnumField<EventKind> takes the cases of EventKind.
The field objects come from a class of their own next to the meta class, marked with #[MetaFields]. This keeps the meta class as it is, and you decide for each meta class whether its fields can be queried at all.
Generating the class
The quickest way is to let the framework write it. Run this in your theme or plugin:
The command writes EventDetailsFields.php into the directory of EventDetails, with one method per field that can be queried:
The file is ordinary source code of your project. Commit it, remove methods you do not need, or add a method later. The command does not replace an existing file unless you pass --force. Together with table-fields for tables, it is the only command of the framework that writes source code, and it does so only when you run it.
Writing it by hand
You can also write the class yourself, for example with just the one field you query. The build checks it either way: it calls every public static method that returns a field and reports a field of another meta class, a field name that does not exist, a field class that does not fit the type, and a field that cannot be queried.
Fields that hold a list or a value class cannot be queried. WordPress stores them serialized in one meta value, and SQL cannot look inside.
Missing values
A post that was never saved with a field has no meta value for it. When you load the meta object, such a field has the default of its constructor parameter. Queries follow the same rule. If the default meets a condition, the condition also finds posts without a stored value: capacity()->lessThan(5) finds events without a stored capacity, because the default is 0.
Sorting follows it too: a post without a value sorts like the default. WordPress' own sorting by meta values cannot do this reliably, so the framework adds the sort order to the SQL of the query itself. It uses a filter of WordPress that it adds only while the query runs.
Conditions
where() takes conditions; all of them must be met. Conditions come from field objects, from Terms for taxonomies, and from the groups Any and All:
Any::of() is met if one of its conditions is met, All::of() if all of them are; groups can contain groups. WordPress keeps conditions on meta fields and on terms in two separate parts of a query and always joins the two with AND. A group therefore contains either meta conditions or term conditions; mixing them throws an exception.
The same field classes and groups work for the columns of tables of your own, in LogEntry::query(). A query of posts takes no conditions on columns, and a query of rows none on meta fields or terms.
The data of the post has methods of its own, described in the next section.
Posts and pages
Queries work the same for the post types of WordPress: Post::query() returns posts, Page::query() pages. The fields every post has – slug, title, dates, parent, template, format, password, comments – have methods of their own, because they are columns of the posts table or data WordPress keeps in a fixed place, not fields of a meta class:
WordPress' categories and tags are named by the framework classes Category and Tag, like its post types by Post and Page. They register nothing and work wherever a taxonomy class is accepted: in Terms, getTerms(), TaxonomyTag and #[TermReference].
A few fields need a word of explanation:
- Template: WordPress stores the template of a post in the protected meta key
_wp_page_template, as the slug of the file intemplates/.template('page-wide')finds posts with that template,defaultTemplate()posts without one. - Format: post formats are terms of the taxonomy
post_format. Since you know them as a fixed list,format()takes the enumPostFormat;PostFormat::Standardis a post without format. - Sticky: WordPress keeps the IDs of sticky posts in the option
sticky_posts.sticky()reads it when the query runs, so a cached query notices a change. Only posts of the typepostcan be sticky. - Search: without further arguments,
search()looks in the title, excerpt and content, like the search of WordPress.columnslimits it,exactrequires the text to be the whole value of a column, andsentencesearches the words as one phrase.
Sorting and pages
orderBy() sorts by post data (PostOrder::Title, PostOrder::Date, …) or a field, ascending unless you pass Direction::Desc. Each further call sorts the posts that are still equal.
limit() and offset() restrict get(). For pages, use paginate(), which also counts all matching posts:
Caching
cache() stores the result in the cache of your theme or plugin. The framework stores the IDs of the posts and the number of matching posts, and loads the post objects from them on each call:
You do not need to name tags for the posts. The framework adds the tag of the post type and of every taxonomy in the conditions, so a changed event, meta value or term assignment invalidates the entry. Further tags and variants work as with remember(). A random order cannot be cached.
WordPress caches the IDs of queries in the object cache as well, but only with a persistent object cache, and every change of any post invalidates all of them. cache() also works with the file backend and is invalidated per post type.
Using results in templates
Queries belong in PHP, not in templates. A block runs its query in render() and passes the post objects to its template, where their getters work as with post():
Rules
AbstractPost::query() returns a PostQuery of the post class; it works once the theme or plugin has booted. Every method that returns a query is marked #[\NoDiscard].
Fields and their conditions:
- Strings are compared without regard to case, like the default collations of the database.
contains()matches%and_literally. - A meta value that is not stored counts as the default in conditions and in the sort order.
Terms::in(),notIn()andall()take the taxonomy class, a non-empty list ofWP_Termobjects or term IDs andincludeChildren(default true);Terms::exists()andnotExists()take the taxonomy class.Any::of()andAll::of()need at least one condition, all on meta fields, all on terms or all on the columns of a table.DateTimeFieldand theNullable…field classes are for tables only.
#[MetaFields(metaClass)] on a class of your own:
metaClassis a meta class (#[PostMeta]) of the theme or plugin; one class per meta class.- Every public static method that returns a field has no parameters and returns a field of
metaClasswith an existing field name and the field class of its type (with the enum or post class of the field). Other methods are ignored. - The class is no service. In development mode the build runs again when signatures change, not when only a method body changes; a query checks its fields again when it runs.
wp-app-framework meta-fields <meta class> [--force] writes {MetaClass}Fields.php next to the meta class. An existing file is replaced only with --force.