PHPackages                             pressgang-wp/quartermaster - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. pressgang-wp/quartermaster

ActiveLibrary[Utility &amp; Helpers](/categories/utility)

pressgang-wp/quartermaster
==========================

WordPress-first fluent WP\_Query args builder

v0.1.0(3mo ago)120MITPHPPHP ^8.3CI passing

Since Feb 8Pushed 1mo ago1 watchersCompare

[ Source](https://github.com/pressgang-wp/pressgang-quartermaster)[ Packagist](https://packagist.org/packages/pressgang-wp/quartermaster)[ RSS](/packages/pressgang-wp-quartermaster/feed)WikiDiscussions main Synced 1mo ago

READMEChangelog (1)Dependencies (1)Versions (2)Used By (0)

cl

⚓ Quartermaster
===============

[](#-quartermaster)

**Quartermaster** is a fluent, args-first builder for `WP_Query`.

It helps you build complex query arrays in readable, composable steps, while staying **100% WordPress-native under the hood**. It ships as a standalone package in the `pressgang-wp` ecosystem, but it does **not** depend on the PressGang theme framework.

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

---

📦 Install
---------

[](#-install)

```
composer require pressgang-wp/quartermaster
```

Requirements: PHP 8.3+.

---

🗺️ Quick Reference Method Index
-------------------------------

[](#️-quick-reference-method-index)

AreaMethodsBootstrap`posts()`, `terms()`, `prepare()` (compatibility alias)Core post constraints`postType()`, `status()`, `whereId()`, `whereInIds()`, `excludeIds()`, `whereParent()`, `whereParentIn()`Author constraints`whereAuthor()`, `whereAuthorIn()`, `whereAuthorNotIn()`Pagination / search`paged()`, `limit()`, `all()` (fetch all: `posts_per_page=-1`, `nopaging=true`), `search()`Query-var binding`bindQueryVars()`, `Bind::paged()`, `Bind::tax()`, `Bind::orderBy()`, `Bind::metaNum()`, `Bind::search()`Ordering`orderBy()`, `orderByAsc()`, `orderByDesc()`, `orderByMeta()`, `orderByMetaAsc()`, `orderByMetaDesc()`, `orderByMetaNumeric()`, `orderByMetaNumericAsc()`, `orderByMetaNumericDesc()`Meta query`whereMeta()`, `orWhereMeta()`, `whereMetaNot()`, `whereMetaLikeAny()`, `whereMetaDate()`, `whereMetaExists()`, `whereMetaNotExists()`Tax query`whereTax()`Date query`whereDate()`, `whereDateAfter()`, `whereDateBefore()`Query-shaping flags`idsOnly()`, `noFoundRows()`, `withMetaCache()`, `withTermCache()`Conditional &amp; hooks`when()`, `unless()`, `tap()`Macros`macro()`, `hasMacro()`, `flushMacros()`Escape hatch`tapArgs()`Introspection`toArgs()`, `explain()`Terminals`get()`, `toArray()`, `wpQuery()`, `timber()`, `applyTo()`Terms core`taxonomy()`, `objectIds()`, `hideEmpty()`, `slug()`, `name()`, `fields()`, `include()`, `exclude()`, `excludeTree()`, `parent()`, `childOf()`, `childless()`, `search()`Terms pagination / ordering`limit()`, `offset()`, `page()`, `orderBy()`Terms meta query`whereMeta()`, `orWhereMeta()`Terms terminal`get()`, `timber()`---

🤔 Why Fluent?
-------------

[](#-why-fluent)

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

Quartermaster gives you:

- ✨ 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
-------------------

[](#-design-philosophy)

Quartermaster is intentionally light-touch:

- 🧱 WordPress-native — every fluent method maps directly to real `WP_Query` keys
- 🫙 Zero side effects by default — `Quartermaster::posts()->toArgs()` is empty
- 🎯 Opt-in only — nothing changes unless you call a method
- 🔌 Loosely coupled — no mutation of WordPress internals, no global state changes
- 🌲 Timber-agnostic core — Timber support is optional and runtime-guarded
- 🧭 Explicit over magic — sharp WP edges are documented, not hidden

Steady hands on the wheel, predictable seas ahead. 🚢

```
Quartermaster::posts()->toArgs(); // []
```

---

🚫 Non-Goals (Read Before Boarding)
----------------------------------

[](#-non-goals-read-before-boarding)

Quartermaster deliberately does **not** aim to:

- Replace `WP_Query` or abstract it away
- Act as an ORM or ActiveRecord layer
- Hide WordPress limitations (e.g. tax/meta OR logic)
- Automatically infer defaults or “best practices”
- Replace WordPress term query APIs

If WordPress requires a specific argument shape, **Quartermaster expects you to be explicit**.
No fog, no illusions, no siren songs. 🧜‍♀️

---

🚀 Quick Start
-------------

[](#-quick-start)

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

```
Quartermaster::posts('event');

// is equivalent to

Quartermaster::posts()->postType('event');
```

`prepare()` remains available as a low-level backwards-compatible alias.

```
use PressGang\Quartermaster\Quartermaster;

$args = Quartermaster::posts()
    ->postType('event')
    ->status('publish')
    ->paged(10)
    ->orderByMeta('start', 'ASC')
    ->search(get_query_var('s'))
    ->toArgs();
```

Run the query and get posts:

```
$posts = Quartermaster::posts()
    ->postType('event')
    ->status('publish')
    ->get();
```

When you need the full `WP_Query` object (pagination metadata, found rows, loop state):

```
$query = Quartermaster::posts()
    ->postType('event')
    ->status('publish')
    ->wpQuery();

$posts = $query->posts;
$total = $query->found_posts;
```

🌿 Terms Quick Start
-------------------

[](#-terms-quick-start)

```
use PressGang\Quartermaster\Quartermaster;

$terms = Quartermaster::terms('category')
    ->hideEmpty()
    ->orderBy('name')
    ->limit(20)
    ->get();
```

Filter by slug, get just IDs, or scope to a specific post:

```
// Terms attached to a specific post
$tags = Quartermaster::terms('post_tag')
    ->objectIds($post->ID)
    ->get();

// Leaf categories only (no children), return IDs
$leafIds = Quartermaster::terms('category')
    ->childless()
    ->fields('ids')
    ->get();

// Find terms by slug
$terms = Quartermaster::terms('genre')
    ->slug(['rock', 'jazz'])
    ->hideEmpty(false)
    ->get();

// All descendants of a parent term
$children = Quartermaster::terms('category')
    ->childOf(5)
    ->excludeTree(12)
    ->get();

// Get Timber term objects (runtime-guarded)
$timberTerms = Quartermaster::terms('category')
    ->hideEmpty()
    ->orderBy('name')
    ->timber();
```

Inspect generated args:

```
$args = Quartermaster::terms('category')
    ->hideEmpty(false)
    ->whereMeta('featured', 1)
    ->toArgs();
```

🔗 Binding Query Vars (Two Styles)
---------------------------------

[](#-binding-query-vars-two-styles)

Nothing reads query vars unless you explicitly call `bindQueryVars()`.

Map style with `Bind::*`:

```
use PressGang\Quartermaster\Bindings\Bind;
use PressGang\Quartermaster\Quartermaster;

$q = Quartermaster::posts('route')->bindQueryVars([
    'paged' => Bind::paged(),
    'orderby' => Bind::orderBy('date', 'DESC', ['title' => 'ASC']),
    'shape' => Bind::tax('route_shape'),
    'difficulty' => Bind::tax('route_difficulty'),
    'min_distance' => Bind::metaNum('distance_miles', '>='),
    'max_distance' => Bind::metaNum('distance_miles', '=');
    $b->metaNum('max_distance')->to('distance_miles', '=')
    ->orderByMeta('start', $isArchive ? 'DESC' : 'ASC');
```

This keeps intent explicit:

- `whereMetaDate(...)` adds a `meta_query` DATE clause
- `orderByMeta(...)` controls ordering separately

No hidden assumptions. No barnacles. ⚓

---

🔌 Macros (Project-Level Sugar)
------------------------------

[](#-macros-project-level-sugar)

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

```
Quartermaster::macro('orderByMenuOrder', function (string $dir = 'ASC') {
    return $this->orderBy('menu_order', $dir);
});

$posts = Quartermaster::posts('page')
    ->orderByMenuOrder()
    ->status('publish')
    ->get();
```

Macros should call existing Quartermaster methods — avoid mutating internal args directly. Macro invocations are recorded in `explain()` as `macro:` for debuggability.

Register macros in your theme's `functions.php` or a service provider. Both builders (`Quartermaster` and `TermsBuilder`) support macros independently.

---

🔀 Conditional Queries &amp; Hooks
---------------------------------

[](#-conditional-queries--hooks)

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

**`when()`** — runs a closure when the condition is true:

```
$q = Quartermaster::posts('event')
    ->when($isArchive, fn ($q) =>
        $q->whereMetaDate('start', '=')->orderByMeta('start', 'ASC')
    );
```

Or with an else clause:

```
$q = Quartermaster::posts('event')
    ->when(
        $isArchive,
        fn ($q) => $q->orderBy('date', 'DESC'),
        fn ($q) => $q->orderBy('date', 'ASC'),
    );
```

**`unless()`** — inverse of `when()` (`unless($x)` is `when(!$x)`):

```
$q = Quartermaster::posts('event')
    ->unless($isArchive, fn ($q) =>
        $q->whereMetaDate('start', '>=')->orderByMeta('start', 'ASC')
    );
```

**`tap()`** — always runs a closure, for builder-level logic without breaking the chain:

```
$q = Quartermaster::posts('event')
    ->tap(function ($q) use ($debug) {
        if ($debug) {
            $q->noFoundRows();
        }
    })
    ->status('publish');
```

All three are recorded in `explain()` for debuggability. No magic, no hidden state. ⚓

---

🪝 Query Hooks (`pre_get_posts`)
-------------------------------

[](#-query-hooks-pre_get_posts)

`applyTo()` modifies an existing `WP_Query` in place instead of creating a new one — designed for WordPress `pre_get_posts` hooks:

```
add_action('pre_get_posts', function (WP_Query $query): void {
    if (! $query->is_main_query() || is_admin()) {
        return;
    }

    Quartermaster::posts('product')
        ->whereTax('product_visibility', ['exclude-from-catalog'], 'name', 'NOT IN')
        ->whereMetaExists('_price')
        ->applyTo($query);
});
```

When multiple hooks call `applyTo()`, clause arrays (`tax_query`, `meta_query`, `date_query`) are **merged** with existing clauses — not overwritten — so hooks compose safely.

`applyTo()` is a **void terminal**: it does not return the builder. If you need to inspect the applied args, hold a reference to the builder and call `explain()` separately.

---

🌲 Optional Timber Terminal
--------------------------

[](#-optional-timber-terminal)

```
$posts = Quartermaster::posts()
    ->postType('event')
    ->status('publish')
    ->timber();
```

If Timber is unavailable, Quartermaster throws a **clear runtime exception** rather than hard-coupling Timber into core.

---

🔍 Debugging &amp; Introspection
-------------------------------

[](#-debugging--introspection)

Ordering direction is explicit: Quartermaster accepts only `ASC`/`DESC`; invalid values are normalized to method defaults and surfaced in `explain()` warnings.

Inspect generated args:

```
$args = Quartermaster::posts()
    ->postType('event')
    ->toArgs();
```

Inspect args plus applied calls and warnings:

```
$explain = Quartermaster::posts()
    ->orderBy('meta_value')
    ->explain();
```

Perfect for reviews, debugging, and keeping junior crew out of trouble. 🧭

Smooth seas and predictable queries.
Happy sailing. ⚓🚢

###  Health Score

37

—

LowBetter than 83% of packages

Maintenance85

Actively maintained with recent releases

Popularity10

Limited adoption so far

Community7

Small or concentrated contributor base

Maturity39

Early-stage or recently created project

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

99d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/1303610?v=4)[Benedict](/maintainers/benedict-w)[@benedict-w](https://github.com/benedict-w)

---

Top Contributors

[![benedict-w](https://avatars.githubusercontent.com/u/1303610?v=4)](https://github.com/benedict-w "benedict-w (62 commits)")

---

Tags

fluentpressgangquery-builderwordpress-development

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/pressgang-wp-quartermaster/health.svg)

```
[![Health](https://phpackages.com/badges/pressgang-wp-quartermaster/health.svg)](https://phpackages.com/packages/pressgang-wp-quartermaster)
```

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
