Skip to main content

Controllers

Introduction​

Controllers live in backend/app/HTTP/Controllers and hold the handling logic for a route. They are plain classes with no base class to extend:

namespace YourPlugin\HTTP\Controllers;

if (!defined('ABSPATH')) {
exit;
}

use YourPlugin\Deps\BitApps\WPKit\Http\Response;
use YourPlugin\Model\Tag;

final class TagController
{
public function index()
{
return Tag::get(['id', 'title', 'status']);
}
}

Mark controllers final and guard the file with the ABSPATH check, as every file in the plugin does.

Receiving the request​

Type-hint Request on the method and the router injects it:

use YourPlugin\Deps\BitApps\WPKit\Http\Request\Request;

public function destroy(Request $request)
{
$tag = Tag::findOne(['id' => $request->tagId ?? $request->id]);

if (!$tag) {
return Response::error('Tag not found');
}

$tag->delete();

return Response::success('Tag deleted successfully');
}

Route parameters are injected by name alongside the request:

// Route::get('node/{flow_id}/{node_id}', [NodeController::class, 'show']);
public function show($flow_id, $node_id)
{
// ...
}

See Request for the full input API.

Validating input​

Call validate() with the rules for the action. It returns only the validated (and sanitized) values:

public function store(Request $request)
{
$validated = $request->validate([
'title' => ['required', 'sanitize:text', new UniqueRule(Tag::class, 'title')],
'filter' => ['nullable', 'string', 'sanitize:text'],
]);

$validated['slug'] = Slug::generate($validated['title']);

return Response::success(Tag::insert($validated));
}

Validation failure never reaches the next line — the request is answered with a 422 JSON body. See Validation.

For rule sets that are reused or long enough to crowd the controller, move them into a form request.

Returning responses​

Response::success() and Response::error() build the JSON envelope:

return Response::success($tag); // 200
return Response::error('Failed to save tag'); // 400
return Response::error('Access denied')->httpStatus(411); // custom status

Both accept a string or any serializable payload. Chainable helpers cover the rest of the envelope:

return Response::success($data)
->message('Flow saved')
->code('FLOW_SAVED')
->httpStatus(201);

Returning a bare value works too — the router serializes it — but the envelope keeps client-side handling uniform:

public function index()
{
return Tag::get(['id', 'title', 'status']); // serialized as-is
}

Method naming​

Controllers follow REST-ish naming, so a reader can guess the route from the method:

MethodPurpose
indexList records
showFetch one record
storeCreate
updateModify
destroyDelete

Anything outside that set gets a descriptive name, such as reExecuteFlow or handleCallback.