Routing
Introductionâ
Routes map a request to a controller method. They live in two files under backend/hooks/, one per transport:
| File | Transport | Registered when |
|---|---|---|
ajax.php | admin-ajax.php | The request is an AJAX request |
api.php | WP REST API | The request is a REST request |
Both files are plain PHP, included by HookProvider after it builds a Router for that transport, so a route file is just a list of Route:: calls.
Defining routesâ
use YourPlugin\Deps\BitApps\WPKit\Http\Router\Route;
use YourPlugin\HTTP\Controllers\TagController;
Route::get('tags', [TagController::class, 'index']);
Route::post('tags/save', [TagController::class, 'store']);
The action is a [Controller::class, 'method'] pair. Route::get() and Route::post() cover the common verbs; Route::match() takes a list:
Route::match(['post', 'get', 'head'], 'webhook/callback/{trigger_id}', [WebhookDispatchController::class, 'handleWebhook']);
Route parametersâ
Wrap a segment in braces to capture it. The router compiles the path to a regex and passes the value to the controller method by name:
Route::get('flows/{flow_id}', [FlowController::class, 'show']);
Route::get('node/{flow_id}/{node_id}', [NodeController::class, 'show']);
public function show($flow_id)
{
// $flow_id is the captured segment
}
Append ? to make a parameter optional: {node_id?}.
REST, AJAX and static routes now share one path grammar. Literal segments are regex-quoted, and an optional parameter matches with no trailing separator â entries/{slug?} matches entries. The trade-off: on AJAX routes it no longer matches the empty-value-with-slash form entries/.
Duplicate or invalid parameter names now throw InvalidArgumentException when the route is registered, rather than producing a pattern that silently never matches.
Groupsâ
Route::group() applies shared settings to every route declared inside the closure:
Route::group(
function (): void {
Route::get('tags', [TagController::class, 'index']);
Route::post('tags/save', [TagController::class, 'store']);
}
)->middleware('nonce', 'isAdmin');
Group settings merge with per-route ones rather than replacing them, so a route inside a group can add its own middleware.
Prefixesâ
prefix() prepends a path segment to every route in the group:
Route::prefix('webhook')->group(
function (): void {
Route::post('store', [WebhookController::class, 'store']); // webhook/store
}
);
Middlewareâ
Attach middleware by name. The names come from your plugin's middleware registry â see Middleware:
Route::group(
function (): void {
// ...
}
)->middleware('nonce', 'isAdmin');
Pass parameters after a colon, separated by commas â see middleware parameters:
Route::post('flows/save', [FlowController::class, 'store'])->middleware('nonce', 'isAdmin');
Unauthenticated routesâ
By default an AJAX route is registered for logged-in users only (wp_ajax_*). noAuth() also registers the wp_ajax_nopriv_* action, so logged-out visitors can reach it:
Route::noAuth()->group(
function (): void {
Route::post('webhook/receive', [WebhookController::class, 'receive']);
}
);
ignoreToken() skips the router's token check for routes that authenticate some other way, such as a signed webhook payload.
noAuth() opens the endpoint to the public internet. Validate and authorize inside the controller â an unauthenticated route with no checks of its own is a vulnerability.
Static routesâ
StaticRouter maps a front-end URL to a route through WordPress rewrite rules, for pages that are neither AJAX nor REST:
use YourPlugin\Deps\BitApps\WPKit\Http\Router\StaticRouter;
$static = new StaticRouter('portal');
$static->loadRoutesFromFile(__DIR__ . '/hooks/static.php');
// hooks/static.php
Route::get('portal/{slug?}', [PortalController::class, 'render']);
The action must return string-compatible page content; returning null renders no additional content. Static dispatch enforces the route's declared HTTP methods.
Rewrite rules need flushing when they change â flushOnActivate() and flushOnDeactivate() hook that to plugin activation, and maybeFlushRewriteRules() handles the case where a rule is missing at runtime.
Every static path generates one complete, anchored rewrite rule. Undeclared intermediate prefixes are not registered, so portal/reports/{id} does not make portal/reports routable â declare it if you need it.
Resulting URLsâ
AJAX routes are registered as admin-ajax.php actions, with the router's AJAX prefix prepended to the route path:
POST /wp-admin/admin-ajax.php?action=<var_prefix>tags/save
REST routes are registered with register_rest_route() under the plugin namespace and version:
GET /wp-json/<plugin-slug>/v1/oauthCallback
The namespace and version are set when HookProvider constructs the router, not in the route file.
The REST routes register with 'permission_callback' => '__return_true'. Authorization is the job of middleware and the controller, not the route registration.