githubEdit

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

circle-check

📦 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. ⚓

circle-info

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

circle-exclamation

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

circle-exclamation
circle-info

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.

circle-info

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

circle-info

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.

circle-info

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

circle-check

🗓️ 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:

circle-info

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.

circle-exclamation

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:

circle-info

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:

circle-check

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

chevron-right🚢 Bootstraphashtag
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()

chevron-right📌 Post Constraintshashtag
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

chevron-right🔎 Meta Querieshashtag
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

chevron-right🏷️ Taxonomy Querieshashtag
Method
Description

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

Taxonomy query clause

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

chevron-right📅 Date Querieshashtag
Method
Description

whereDate(array)

Raw WordPress date_query clause

whereDateAfter(string|array, $inclusive?)

Published after date

whereDateBefore(string|array, $inclusive?)

Published before date

chevron-right↕️ Orderinghashtag
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

chevron-right📄 Pagination and Performancehashtag
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

chevron-right🔀 Conditionals and Hookshashtag
Method
Description

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

Conditional closure

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

Inverse conditional

tap($callback)

Always-run closure

tapArgs($callback)

Raw args manipulation

chevron-right🔌 Macroshashtag
Method
Description

macro($name, $callable)

Register a named macro

hasMacro($name)

Check if macro is registered

flushMacros()

Remove all macros (for tests)

chevron-right🔗 Query-Var Bindinghashtag
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

chevron-right🔍 Introspectionhashtag
Method
Returns

toArgs()

array<string, mixed> — raw WP_Query args

explain()

array{args, applied, warnings, bindings?}

chevron-right🏁 Terminalshashtag
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)

chevron-right🏷️ Terms Builderhashtag
Method
Description

taxonomy(string|array)

Set taxonomy

objectIds(int|array)

Scope to specific post(s)

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