Code Around Every Handler
Middleware is code that runs around your handlers: before the request reaches them, after the response leaves them, or instead of them. Logging, authentication, rate limiting, compression — all middleware.
The same idea travels under three names: web frameworks say middleware, gRPC says interceptor, some ecosystems say filter. One concept, three vocabularies.
In PHP, middleware is a function that takes a handler — any callable — and returns a wrapped callable: exactly the Decorator pattern from the design patterns group, and a stack of them behaves like a Chain of Responsibility.
A Logging Middleware
withLog wraps any handler: it logs the request coming in, lets the handler work, and logs again on the way out. The handler itself stays completely unaware.
The server runs as a separate process, so its log cannot reach the demo's terminal by itself — the middleware appends to a small log file, and the demo prints the fresh log lines after each request.
Wrap once, log everything
The wrapped handler serves two different paths and both get logged — the logging is written once, in the wrapper, not in every handler.
Output: PHP script
<?php
// Middleware: takes a handler, returns a wrapped handler.
$router = <<<'PHP'
<?php
function withLog(callable $next): callable {
return function () use ($next) {
$path = parse_url($_SERVER["REQUEST_URI"], PHP_URL_PATH);
serverLog("-> {$_SERVER['REQUEST_METHOD']} $path");
$next();
serverLog("<- done");
};
}
function serverLog(string $line): void {
file_put_contents(sys_get_temp_dir() . "/pc-logging-middleware.log", $line . "\n", FILE_APPEND);
}
$routes = [
"/users" => function () { echo "Ana, Ben\n"; },
"/notes" => function () { echo "shopping, ideas\n"; },
];
$path = parse_url($_SERVER["REQUEST_URI"], PHP_URL_PATH);
$handler = withLog($routes[$path] ?? function () { http_response_code(404); });
$handler();
PHP;
$logFile = sys_get_temp_dir() . "/pc-logging-middleware.log";
file_put_contents($logFile, "");
$routerFile = sys_get_temp_dir() . "/pc-logging-middleware.php";
file_put_contents($routerFile, $router);
// A free port, then PHP's built-in web server as a child process.
$probe = stream_socket_server("tcp://127.0.0.1:0");
$port = (int) explode(":", stream_socket_get_name($probe, false))[1];
fclose($probe);
$server = proc_open(["php", "-S", "127.0.0.1:$port", $routerFile],
[1 => ["file", "/dev/null", "w"], 2 => ["file", "/dev/null", "w"]], $pipes);
do {
usleep(20000);
$up = @fsockopen("127.0.0.1", $port);
} while ($up === false);
fclose($up);
$base = "http://127.0.0.1:$port";
$context = stream_context_create(["http" => ["ignore_errors" => true]]);
foreach (["/users", "/notes"] as $path) {
$body = file_get_contents("$base$path", false, $context);
echo file_get_contents($logFile); // what the middleware logged
file_put_contents($logFile, "");
echo "body: " . trim($body) . "\n";
}
proc_terminate($server);
proc_close($server);
unlink($routerFile);
unlink($logFile);
Middleware That Stops the Request
Middleware can also refuse: an auth check that finds no valid key answers 401 Unauthorized and never calls the next handler.
Chained as withLog(withAuth($handler)), the request passes through the layers in order — and the response bubbles back out through them.
An auth layer in the chain
The request without a key is stopped by the auth layer with 401 — the handler never runs. With the key, the request passes the whole chain and reaches the data.
Output: PHP script
<?php
// withAuth can stop the request before the handler ever runs.
$router = <<<'PHP'
<?php
function withLog(callable $next): callable {
return function () use ($next) {
$path = parse_url($_SERVER["REQUEST_URI"], PHP_URL_PATH);
serverLog("-> {$_SERVER['REQUEST_METHOD']} $path");
$next();
};
}
// Auth middleware: no valid key, no next handler.
function withAuth(callable $next): callable {
return function () use ($next) {
if (($_SERVER["HTTP_X_API_KEY"] ?? "") !== "secret123") {
http_response_code(401);
echo "missing or wrong API key\n";
return;
}
$next();
};
}
function serverLog(string $line): void {
file_put_contents(sys_get_temp_dir() . "/pc-auth-middleware.log", $line . "\n", FILE_APPEND);
}
$handler = withLog(withAuth(function () { echo "the secret report\n"; }));
$handler();
PHP;
function httpRequest(string $url, string $method = "GET", array $headers = [], ?string $body = null): array {
$options = ["method" => $method, "ignore_errors" => true];
if ($headers !== []) {
$options["header"] = implode("\r\n", $headers);
}
if ($body !== null) {
$options["content"] = $body;
}
$responseBody = file_get_contents($url, false, stream_context_create(["http" => $options]));
return [(int) explode(" ", $http_response_header[0])[1], $responseBody, $http_response_header];
}
$logFile = sys_get_temp_dir() . "/pc-auth-middleware.log";
file_put_contents($logFile, "");
$routerFile = sys_get_temp_dir() . "/pc-auth-middleware.php";
file_put_contents($routerFile, $router);
// A free port, then PHP's built-in web server as a child process.
$probe = stream_socket_server("tcp://127.0.0.1:0");
$port = (int) explode(":", stream_socket_get_name($probe, false))[1];
fclose($probe);
$server = proc_open(["php", "-S", "127.0.0.1:$port", $routerFile],
[1 => ["file", "/dev/null", "w"], 2 => ["file", "/dev/null", "w"]], $pipes);
do {
usleep(20000);
$up = @fsockopen("127.0.0.1", $port);
} while ($up === false);
fclose($up);
$base = "http://127.0.0.1:$port";
foreach ([[], ["X-API-Key: secret123"]] as $requestHeaders) {
[$status, $body] = httpRequest("$base/report", "GET", $requestHeaders);
echo file_get_contents($logFile); // what the middleware logged
file_put_contents($logFile, "");
echo " status: $status - " . trim($body) . "\n";
}
proc_terminate($server);
proc_close($server);
unlink($routerFile);
unlink($logFile);