Relayer
DocsRoutingmiddleware-cors

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).

Comparing paths#

$request->path has one spelling per route: /%61dmin and /admin/ both arrive as /admin. Percent-encoding stays, so compare non-ASCII paths with $request->decodedPath().

\str_starts_with($request->decodedPath(), '/admin-日本語');

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 revised — 2026-11-01
Revisions (3)
  • v0.31.0: decodedPath() と guard / middleware.go への案内を追記
  • Add the composing-multiple-middleware section
  • Created