Quartermaster

A fluent, args-first query builder for WP_Query and WP_Term_Query. WordPress-native under the hood — no ORM, no magic, no lock-in.

Quartermaster is a fluent, args-first query builder for WP_Query and WP_Term_Query. It helps you build complex query arrays in readable, composable steps while staying 100% WordPress-native under the hood.

Think of it as a reliable quartermaster for your query cargo — you decide what goes aboard, nothing gets smuggled in. 🧭

📦 Install

Terminal
composer require pressgang-wp/quartermaster

Requirements: PHP 8.3+


✨ Why Fluent?

WP_Query arrays are powerful, but as they grow they become harder to scan, review, and refactor.

Benefit
How

📖

Better readability

Query intent is expressed step-by-step

🧩

Better composability

Add or remove clauses without rewriting a large array

🛡️

Better safety

Methods are explicit about which WP args they set

🔍

Better debugging

Inspect exact output with toArgs() and explain()

You still end up with plain WordPress args. No ORM. No hidden query engine. No lock-in. Just well-organised cargo. ⚓

Sometimes raw WP_Query is fine — if your query is short and static, use it. Quartermaster shines when queries evolve, branch, or need to be composed without losing your bearings.


🧠 Design Philosophy

Quartermaster is intentionally light-touch. Steady hands on the wheel, predictable seas ahead. 🚢

Principle
Meaning

🧱 WordPress-native

Every fluent method maps directly to real WP_Query / WP_Term_Query keys

🫙 Zero side effects

Quartermaster::posts()->toArgs() returns [] — nothing gets smuggled in

🎯 Opt-in only

Nothing changes unless you call a method

🔌 Loosely coupled

No mutation of WordPress internals, no global state changes

🌲 Timber-agnostic

Timber support is optional and runtime-guarded

🧭 Explicit over magic

Sharp WP edges are documented, not hidden


🚀 Posts — Quick Start

posts('event') is a convenience seed — it only sets post_type and does not infer any other query args.

Build an args array without executing the query:


🪝 Query Hooks (pre_get_posts)

applyTo() lets you use the full Quartermaster API inside WordPress query hooks like pre_get_posts. Instead of creating a new query, it modifies an existing WP_Query in place.

Relation precedence: when merging clause arrays, the existing query's relation takes precedence over the builder's. If the query already has relation => OR on its meta_query, applyTo() will not overwrite it with AND. This ensures earlier hooks aren't silently overridden.


📌 Post Constraints

Filter by ID, parent, author, and status — all with type-safe integer handling.

All ID methods filter non-integer values automatically and skip silently if the resulting list is empty. No invalid args, no broken queries.


🔎 Meta Queries

Build meta_query clauses fluently — from simple key/value checks to complex nested conditions.


🏷️ Taxonomy Queries


📅 Date Queries


📄 Pagination and Performance


↕️ Ordering

Ordering direction is strict: only ASC and DESC are accepted. Invalid values are normalised to the method default and surfaced as a warning in explain().



🏷️ Terms — Quick Start

Quartermaster also provides a fluent builder for WP_Term_Query via Quartermaster::terms().


🔗 Binding Query Vars

One of Quartermaster's most powerful features — bind URL query parameters directly to query clauses. Perfect for archive filtering, search pages, and faceted navigation.

Nothing reads query vars unless you explicitly call bindQueryVars(). No smuggling, no hidden defaults.

Two styles are supported — both compile to the same binding map.

Available bindings

Binding
Purpose
Empty handling

Bind::paged()

Pagination from query var

Skips if ≤ 0

Bind::tax()

Taxonomy filter

Skips if empty or null

Bind::orderBy()

Sort field + direction with per-field overrides

Falls back to default

Bind::metaNum()

Numeric meta comparison

Skips if null or empty

Bind::search()

Search query

Skips if empty; sanitised

Bind::relevanssi()

Relevanssi-aware search (requires Relevanssi)

Skips if empty; sanitised


🗓️ Common Pattern: Meta Date vs Today

Filtering by a meta date (e.g. upcoming vs past events) is a very common WordPress pattern:

This keeps intent explicit — whereMetaDate(...) adds a meta_query DATE clause while orderByMeta(...) controls ordering separately. No hidden assumptions. No barnacles. ⚓


🔀 Conditional Queries

when(), unless(), and tap() keep fluent chains readable without introducing magic or hidden state. None of them read globals or add defaults.

Runs a closure when the condition is true:

Or with an else clause — cleaner when conditions are binary:

All three are recorded in explain() for debuggability.


🔌 Macros

Macros let you register project-specific fluent methods without bloating the core API. They are opt-in — use them for patterns that repeat across your project.

Both builders (Quartermaster and TermsBuilder) support macros independently. Use flushMacros() in tests to clean up.


🪝 Escape Hatch: tapArgs()

When you need to set an arg that Quartermaster doesn't have a dedicated method for, use tapArgs() to manipulate the raw args array while preserving the fluent chain:

The callback receives the current args array and must return the full replacement array. tapArgs() is recorded in explain() for debuggability.


🔍 Debugging and Introspection

Inspect the generated WordPress args array at any point in the chain:

Smart warnings

Quartermaster automatically detects common gotchas:

Warning
Trigger

Unreliable ordering

orderby=meta_value without meta_key set

Pagination ignored

posts_per_page=-1 with paged set

Hidden empty terms

hide_empty not explicitly set on term queries


📖 Method Reference

🚢 Bootstrap
Method
Description

Quartermaster::posts($postType?)

Start a new post query builder

Quartermaster::terms($taxonomy?)

Start a new term query builder

Quartermaster::prepare($postType?)

Compatibility alias for posts()

📌 Post Constraints
Method
Sets

postType(string|array)

post_type

status(string)

post_status

whereId(int)

p

whereInIds(array)

post__in

excludeIds(array)

post__not_in

whereParent(int)

post_parent

whereParentIn(array)

post_parent__in

whereAuthor(int)

author

whereAuthorIn(array)

author__in

whereAuthorNotIn(array)

author__not_in

🔎 Meta Queries
Method
Description

whereMeta($key, $value, $compare?, $type?)

AND meta clause

orWhereMeta($key, $value, $compare?, $type?)

OR meta clause

whereMetaNot($key, $value)

Exclude by value (handles NOT EXISTS)

whereMetaDate($key, $operator, $value?, $format?)

Date comparison (defaults to today)

whereMetaExists($key)

Key exists check

whereMetaNotExists($key)

Key does not exist check

whereMetaLikeAny($key, $values)

Match serialized ACF fields

🏷️ Taxonomy Queries
Method
Description

whereTax($taxonomy, $terms, $field?, $operator?)

Taxonomy query clause

Field defaults to slug. Operator defaults to IN. Multiple calls produce AND relation.

📅 Date Queries
Method
Description

whereDate(array)

Raw WordPress date_query clause

whereDateAfter(string|array, $inclusive?)

Published after date

whereDateBefore(string|array, $inclusive?)

Published before date

↕️ Ordering
Method
Description

orderBy($orderby, $order?)

Set order field and direction

orderByAsc($orderby) / orderByDesc($orderby)

Shorthand

orderByMeta($key, $order?, $type?)

Order by meta value

orderByMetaAsc($key) / orderByMetaDesc($key)

Shorthand meta ordering

orderByMetaNumeric($key, $order?)

Order by numeric meta

orderByMetaNumericAsc($key) / orderByMetaNumericDesc($key)

Shorthand

🔍 Search
Method
Description

search(string|null)

Set search term (s); sanitised, null/empty ignored

relevanssi(string|null)

Search with Relevanssi (s + relevanssi = true); requires Relevanssi

📄 Pagination and Performance
Method
Description

paged($perPage?, $page?)

Paginate (reads current page from query vars)

limit(int)

Fixed result count

all()

Fetch all (posts_per_page=-1, nopaging=true)

noFoundRows()

Skip SQL_CALC_FOUND_ROWS

idsOnly()

Return IDs only (fields='ids')

withMetaCache(bool)

Toggle meta cache priming

withTermCache(bool)

Toggle term cache priming

ignoreStickyPosts()

Prevent sticky posts being prepended

🔀 Conditionals and Hooks
Method
Description

when($condition, $then, $else?)

Conditional closure

unless($condition, $then, $else?)

Inverse conditional

tap($callback)

Always-run closure

tapArgs($callback)

Raw args manipulation

🔌 Macros
Method
Description

macro($name, $callable)

Register a named macro

hasMacro($name)

Check if macro is registered

flushMacros()

Remove all macros (for tests)

🔗 Query-Var Binding
Method
Description

bindQueryVars($bindings, $source?)

Bind URL query vars to query clauses

Bind::paged()

Pagination binding

Bind::tax($taxonomy, $field?, $operator?)

Taxonomy binding

Bind::orderBy($default, $dir, $overrides?)

Order binding with per-field overrides

Bind::metaNum($key, $compare)

Numeric meta binding

Bind::search()

Search term binding

Bind::relevanssi()

Relevanssi-aware search binding (requires Relevanssi)

🔍 Introspection
Method
Returns

toArgs()

array<string, mixed> — raw WP_Query args

explain()

array{args, applied, warnings, bindings?}

🏁 Terminals
Method
Returns

get()

WP_Post[] — execute and return posts

toArray()

array — smart Timber/WP detection

wpQuery()

WP_Query — full query object

timber()

Timber\PostQuery — Timber post collection

applyTo($query)

void — modify existing WP_Query in place (for pre_get_posts)

🏷️ Terms Builder
Method
Description

taxonomy(string|array)

Set taxonomy

objectIds(int|array)

Scope to specific post(s)

forPostType(string)

Scope terms to a post type (via object_ids)

hideEmpty(bool)

Hide/show empty terms

slug(string|array)

Filter by slug

name(string|array)

Filter by name

fields(string)

Control return fields (ids, names, slugs, count, etc.)

include(array) / exclude(array)

Include/exclude term IDs

excludeTree(int|array)

Exclude entire term branches

parent(int)

Direct children only

childOf(int)

All descendants

childless(bool)

Leaf terms only

search(string)

Search terms

limit(int) / offset(int)

Limit and offset

page(int, int)

Convenience pagination

orderBy(string, string)

Order terms

whereMeta() / orWhereMeta()

Term meta queries

get()

Execute and return terms

timber()

Execute and return Timber\Term[]


Smooth seas and predictable queries. Happy sailing. ⚓🚢

Last updated