-
Notifications
You must be signed in to change notification settings - Fork 2
ApiController
Pair\Api\ApiController is the base class for JSON API modules. It extends Controller and adds:
- a parsed Request object
- auth helpers for session and bearer-token flows
- a middleware pipeline
- default rate limiting when enabled by env
- a ready-to-use
whatsappWebhookAction()endpoint for Meta webhooks
Use ApiController for endpoints that return JSON only and need centralized request, auth, and middleware behavior.
When overriding _init(), call parent::_init() first:
protected function _init(): void
{
parent::_init();
// custom API setup
}parent::_init() creates:
-
$this->requestas aPair\Api\Request - the internal
MiddlewarePipeline - the default
ThrottleMiddlewarewhenPAIR_API_RATE_LIMIT_ENABLED=true
By default, the throttle reads:
PAIR_API_RATE_LIMIT_MAX_ATTEMPTSPAIR_API_RATE_LIMIT_DECAY_SECONDS
getUser() reads the current authenticated user from Application::getInstance()->currentUser and returns null if there is no loaded user.
requireAuth() is the usual guard for session-authenticated endpoints:
public function profileAction(): void
{
$this->runMiddleware(function () {
$user = $this->requireAuth();
\Pair\Api\ApiResponse::respond($user->toArray());
});
}setBearerToken() and setSession() are normally called by the Pair runtime during API bootstrap. requireBearer() is useful when an endpoint must reject calls that are not authenticated via bearer token:
public function tokenInfoAction(): void
{
$this->runMiddleware(function () {
$token = $this->requireBearer();
\Pair\Api\ApiResponse::respond(['token' => $token]);
});
}getJsonBody() is just a convenience proxy to $this->request->json().
requireJsonPost() enforces three things:
- method must be
POST -
Content-Typemust includeapplication/json - the parsed JSON body must be valid and non-empty
public function loginAction(): void
{
$payload = $this->requireJsonPost();
$data = $this->request->validate([
'email' => 'required|email',
'password' => 'required|string|min:8',
]);
\Pair\Api\ApiResponse::respond([
'email' => $data['email'],
'received' => $payload,
]);
}Important: requireJsonPost() is intentionally POST-only. For JSON PUT or PATCH endpoints, validate the content type and body explicitly:
public function updateOrderAction(): void
{
if (!$this->request->isJson()) {
\Pair\Api\ApiResponse::error('UNSUPPORTED_MEDIA_TYPE', [
'expected' => 'application/json',
]);
}
$data = $this->request->json();
if (is_null($data)) {
\Pair\Api\ApiResponse::error('BAD_REQUEST', [
'detail' => 'Invalid or empty JSON body',
]);
}
// handle PUT/PATCH payload
}middleware() appends a middleware instance to the pipeline. runMiddleware() executes the chain and then the destination callable.
protected function _init(): void
{
parent::_init();
// the default throttle is already attached here
$this->middleware(new \Pair\Api\CorsMiddleware());
}If you need a custom order, override registerDefaultMiddleware():
protected function registerDefaultMiddleware(): void
{
$this->middleware(new \Pair\Api\CorsMiddleware());
$this->middleware(new \Pair\Api\ThrottleMiddleware(20, 60));
}Sensitive-endpoint example with an additional stricter limiter:
protected function _init(): void
{
parent::_init();
$this->middleware(new \Pair\Api\ThrottleMiddleware(10, 60));
}ApiController now includes a ready-to-use unauthenticated Meta webhook endpoint at:
GET /api/whatsappWebhookPOST /api/whatsappWebhook
The Pair runtime allows this single action without sid or bearer auth because Meta authenticates it with:
- the
hub.*verification challenge onGET - the
X-Hub-Signature-256header onPOST
The endpoint uses WHATSAPP_CLOUD_WEBHOOK_VERIFY_TOKEN and WHATSAPP_CLOUD_APP_SECRET from .env.
Override handleWhatsAppWebhook() in the application controller to process normalized events:
class ApiController extends \Pair\Api\CrudController {
protected function handleWhatsAppWebhook(array $events, array $payload): ?array
{
foreach ($events as $event) {
if ($event['event'] === 'message') {
// handle inbound message
}
if ($event['event'] === 'status') {
// handle sent/delivered/read/failed updates
}
}
return [
'received' => true,
'events' => count($events),
];
}
}If you do not override the hook, Pair still acknowledges the webhook with 200 OK.
If an API action is missing, ApiController responds with NOT_FOUND instead of falling back to MVC behavior.
That makes it safe to expose JSON-only modules without HTML redirects or view rendering.
<?php
namespace App\Modules\Api;
use Pair\Api\ApiController as BaseApiController;
use Pair\Api\ApiResponse;
class ApiController extends BaseApiController {
protected function _init(): void
{
parent::_init();
$this->middleware(new \Pair\Api\CorsMiddleware());
}
public function healthAction(): void
{
$this->runMiddleware(function () {
ApiResponse::respond(['ok' => true]);
});
}
}-
setBearerToken(string $bearerToken)stores the bearer token resolved by the Pair API bootstrap. -
setSession(Session $session)stores the current session object for SID-based API auth flows. -
getUser(): ?Userreturns the loaded current user ornull. -
getJsonBody(): mixedis a small convenience wrapper around$this->request->json().
- Overriding
_init()withoutparent::_init(). - Forgetting that the default throttle is already mounted when rate limiting is enabled.
- Using
requireJsonPost()forPUTorPATCH; it is intentionallyPOST-only. - Mixing HTML redirects or views inside API actions.
- Calling
requireBearer()before the runtime has populated the bearer token. - Enabling the WhatsApp endpoint without setting
WHATSAPP_CLOUD_WEBHOOK_VERIFY_TOKENandWHATSAPP_CLOUD_APP_SECRET.
See also: API, Request, ApiResponse, ThrottleMiddleware, RateLimiter, WhatsAppCloudClient.