Skip to content

ApiController

Viames Marino edited this page Mar 26, 2026 · 4 revisions

Pair framework: 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

When to use

Use ApiController for endpoints that return JSON only and need centralized request, auth, and middleware behavior.

Lifecycle

When overriding _init(), call parent::_init() first:

protected function _init(): void
{
    parent::_init();
    // custom API setup
}

parent::_init() creates:

  • $this->request as a Pair\Api\Request
  • the internal MiddlewarePipeline
  • the default ThrottleMiddleware when PAIR_API_RATE_LIMIT_ENABLED=true

By default, the throttle reads:

  • PAIR_API_RATE_LIMIT_MAX_ATTEMPTS
  • PAIR_API_RATE_LIMIT_DECAY_SECONDS

Main methods (deep dive)

1) Auth context: getUser(), requireAuth(), setBearerToken(), requireBearer(), setSession()

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]);
    });
}

2) JSON request helpers: getJsonBody() and requireJsonPost()

getJsonBody() is just a convenience proxy to $this->request->json().

requireJsonPost() enforces three things:

  • method must be POST
  • Content-Type must include application/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
}

3) Middleware pipeline: middleware(), registerDefaultMiddleware(), runMiddleware()

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));
}

4) Built-in WhatsApp webhook: whatsappWebhookAction() and handleWhatsAppWebhook()

ApiController now includes a ready-to-use unauthenticated Meta webhook endpoint at:

  • GET /api/whatsappWebhook
  • POST /api/whatsappWebhook

The Pair runtime allows this single action without sid or bearer auth because Meta authenticates it with:

  • the hub.* verification challenge on GET
  • the X-Hub-Signature-256 header on POST

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.

5) __call(mixed $name, mixed $arguments): void

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.

Full minimal controller example

<?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]);
        });
    }
}

Secondary methods (short reference)

  • 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(): ?User returns the loaded current user or null.
  • getJsonBody(): mixed is a small convenience wrapper around $this->request->json().

Common pitfalls

  • Overriding _init() without parent::_init().
  • Forgetting that the default throttle is already mounted when rate limiting is enabled.
  • Using requireJsonPost() for PUT or PATCH; it is intentionally POST-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_TOKEN and WHATSAPP_CLOUD_APP_SECRET.

See also: API, Request, ApiResponse, ThrottleMiddleware, RateLimiter, WhatsAppCloudClient.

Clone this wiki locally