Skip to main content

Routing

Introduction​

Routes map a request to a controller method. They live in two files under backend/hooks/, one per transport:

FileTransportRegistered when
ajax.phpadmin-ajax.phpThe request is an AJAX request
api.phpWP REST APIThe 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?}.

Changed in wp-kit 2.4.0

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.

warning

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.

warning

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.

note

The REST routes register with 'permission_callback' => '__return_true'. Authorization is the job of middleware and the controller, not the route registration.