Subchapter 4.3
references/controller-enumeration.mdMarkdown5 KBView on GitHub
How to produce an exhaustive list of a plugin’s REST controllers — the first
step of every audit. Plugin family classification is handled separately by
wp-project-triage; this reference covers the mechanics of finding controller
classes inside whatever layout the plugin happens to use.
Observed across the plugins audited to date, there are exactly two paths that together cover every layout seen in the wild:
| Path | When it works | How it works |
|---|---|---|
| Glob | Plugins that follow the standard includes/admin/class-*-rest-*-controller.php layout (WooCommerce core extensions, classic WooPayments). | Fast, deterministic, easy to script. Returns a complete list in one shell call. |
| Grep | Any non-standard layout — includes/api/, includes/rest-api/, src/rest/, monorepo package directories, or anything else. | Universal fallback: grep every PHP file under the plugin root for register_rest_route( call sites, then collect the enclosing class for each hit. |
Running both and de-duplicating is legal; it catches monorepos that have some controllers under the standard layout and others under a package directory.
# From the plugin root:
ls includes/admin/class-*-rest-*-controller.php 2>/dev/null
ls includes/reports/class-*-rest-*-controller.php 2>/dev/nullWhat you’ll see in repos that match this convention:
Automattic/woocommerce-payments) — every controller under
includes/admin/class-wc-rest-payments-*-controller.php plus some under
includes/reports/.If glob returns 5+ hits, it’s almost always the complete inventory. If it returns 0-2, fall through to grep.
# From the plugin root:
grep -rn --include='*.php' 'register_rest_route(' .For each hit:
(class, file, route, callback, permission_callback).This path matters because it’s the only one that finds controllers in non-standard locations:
includes/api/ and
includes/api/legacy/. The standard WooPayments glob returns zero; grep is
mandatory.projects/packages/<name>/src/ with no conventional filename. Grep is
again mandatory.src/Rest/, lib/rest/,
api/v1/, etc. Grep catches them all.A controller can extend a base class in a different repo — typically the
parent plugin (for extensions built on top of another plugin) or WordPress
core itself (for plugins extending WP_REST_Posts_Controller or other core
REST bases). The parent::register_routes() dispatch appears in the
extending plugin’s source, but the literal register_rest_route( call lives
in the parent. WooCommerce extensions extending
WC_REST_Orders_Controller, plugins built on Jetpack package REST classes,
and CPT plugins inheriting from WP_REST_Posts_Controller all hit this
pattern.
Handling:
backing.route_registration_line: null and
backing.callback_line: null in the audit schema.backing.inherited_from: "<parent FQCN>" so downstream skills can tell
the inheritance case from a plain missing line number.See audit-schema.md for the exact field shapes.
The “Controller Inventory” table in the audit doc must list every controller the enumeration found — not just ones backing proposed abilities. A reviewer asking “why isn’t controller X in the MVP?” should be able to point at the inventory and see the explicit answer (usually: “excluded from MVP because…” or “surfaced as a gap because…”).
If your inventory has 3 entries and the plugin clearly exposes more, either the enumeration is incomplete (re-run grep with broader patterns) or you’re filtering the inventory instead of the proposal list. Fix the inventory first; filter after.
If neither glob nor grep produces a complete inventory — for example a
plugin that registers routes dynamically from config or via a factory that
does not contain a literal register_rest_route( string — document the
enumeration gap in “Notes and Surprises”, and extend this reference with the
new pattern once understood.