RRelayer
Home/Routing

Middleware & CORS#

src/Pages/middleware.php (optional) wraps the dispatch of every route in a single closure.

<?php
use Polidog\Relayer\Http\Request;

return function (Request $request, Closure $next): void {
    if (null === $request->header('x-api-key')) {
        \http_response_code(401);
        echo '{"error":"missing api key"}';

        return; // short-circuits if $next() is not called
    }

    $next($request);
};
  • Just one closure. There is no chain runner; compose by hand when you

want several (Composing multiple middleware).

  • Like route.php, declaration-free (return only, evaluated on every

request).

  • If $next($request) is not called, the downstream is not run and it

ends (401 / 429, etc.).

  • The framework's defer / profiler endpoints deliberately run outside

this middleware.

CORS#

Do not hand-write CORS; use the built-in middleware.

<?php
use Polidog\Relayer\Http\Cors;

return Cors::middleware([
    'origins' => ['https://app.example.com'],
    // 'methods', 'headers', 'credentials', 'maxAge' are optional
]);

To allow all origins, use ['origins' => ['*']]. Cors::middleware() also handles responding to preflight (OPTIONS).

Composing multiple middleware#

middleware.php can return only one closure, and there is no chain runner (by design). To stack several behaviors, compose by hand: pass the inner middleware as the $next of the outer one. Every middleware has the same fn(Request $request, Closure $next) signature, so the inner one is simply handed in as "the thing to continue to".

Minimal form:

<?php
// a is the outer layer, b the inner one, then the real route
return fn (Request $r, Closure $next) => $a($r, fn (Request $r) => $b($r, $next));

Realistic example — tag the response with a request id, then delegate to CORS, then the route:

<?php
declare(strict_types=1);

use Polidog\Relayer\Http\Cors;
use Polidog\Relayer\Http\Request;

$cors = Cors::middleware([
    'origins' => ['*'],
    'methods' => ['GET', 'POST', 'OPTIONS'],
]);

return function (Request $request, Closure $next) use ($cors): void {
    if (!\headers_sent()) {
        \header('X-Request-Id: ' . \bin2hex(\random_bytes(8)));
    }

    // $cors answers OPTIONS preflights itself and, for real requests,
    // adds the headers and continues to $next (the real route).
    $cors($request, $next);
};

Key points:

  • Order is outer to inner. Code before $next($request) is

pre-processing; code after it runs on the way back out.

  • Any layer can short-circuit. If a layer does not call $next,

neither the inner middleware nor the route runs (auth 401, rate limit 429, maintenance, CORS preflight, etc.).

  • You can pass a different Request to $next. A layer that

substitutes it (e.g. $next($request->withPath(...))) makes the inner layers and the route see the substituted request.

  • Three or more compose with the same nesting:

fn ($r, $n) => $a($r, fn ($r) => $b($r, fn ($r) => $c($r, $n))), where each of $a/$b/$c is an fn(Request, Closure) middleware.

Last updated: 2026-05-20
Change history (2)
  • Add the composing-multiple-middleware section
  • Created