Codeskill

Learn to code, step by step

Middleware-style request pipelines

A chain of small pieces that each inspect or tweak the request before your controller runs. You saw a front controller in intermediate; middleware is how that entry point stays readable when auth, CSRF, logging, and rate limits all need a say.

The idea in one sentence

Each middleware receives the request, can short-circuit with a response (403, redirect), or pass control to the next link in the chain. Controllers run last.

A minimal contract

<?php
declare(strict_types=1);

namespace AppHttp;

interface Middleware
{
    /** @param callable(): void $next */
    public function handle(Request $request, callable $next): void;
}

Request can be a thin wrapper around superglobals and route params. Keep it boring – no framework magic required.

The pipeline runner

final class Pipeline
{
    /** @param list<Middleware> $middleware */
    public function __construct(private array $middleware) {}

    public function run(Request $request, callable $destination): void
    {
        $runner = array_reduce(
            array_reverse($this->middleware),
            fn (callable $next, Middleware $mw) => fn () => $mw->handle($request, $next),
            $destination,
        );

        $runner();
    }
}

array_reduce builds the onion from the inside out. The destination (your controller action) sits at the core.

Example: require login

final class RequireAuth implements Middleware
{
    public function handle(Request $request, callable $next): void
    {
        if (empty($_SESSION['user_id'])) {
            header('Location: /login', true, 302);
            return;
        }
        $next();
    }
}

Example: CSRF check on POST

final class VerifyCsrf implements Middleware
{
    public function handle(Request $request, callable $next): void
    {
        if ($request->method() === 'POST') {
            $token = $_POST['_token'] ?? '';
            if (!hash_equals($_SESSION['csrf'] ?? '', $token)) {
                http_response_code(419);
                echo 'Invalid CSRF token.';
                return;
            }
        }
        $next();
    }
}

Per-route stacks

Global middleware runs on every request (security headers, session start). Route groups add more:

$routes = [
    'GET /notes' => [
        'middleware' => [RequireAuth::class],
        'handler'    => [NoteController::class, 'index'],
    ],
    'POST /notes' => [
        'middleware' => [RequireAuth::class, VerifyCsrf::class],
        'handler'    => [NoteController::class, 'store'],
    ],
];

Order matters

  • Start session before auth reads $_SESSION
  • Rate limiting before expensive controller work
  • CSRF after you know the method is POST
  • Logging outermost if you want timing for the whole stack

Keep each middleware focused on one job. When you need to change how login works, you touch one class – not twelve controllers.

PreviousDomain-oriented structure without cargo-cult DDD