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
Requestto$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/$cis anfn(Request, Closure)middleware.
Revisions (3)
- v0.31.0: decodedPath() と guard / middleware.go への案内を追記
- Add the composing-multiple-middleware section
- Created