Код вокруг каждого обработчика
Промежуточный слой (middleware) — это код, который выполняется вокруг ваших обработчиков: до того, как запрос доберётся до них, после того как ответ их покинет, или вместо них. Журналирование, аутентификация, ограничение частоты запросов, сжатие — всё это промежуточные слои.
Одна и та же идея встречается под тремя названиями: веб-фреймворки говорят middleware, gRPC говорит interceptor, в некоторых экосистемах говорят filter. Одно понятие, три словаря.
В PHP промежуточный слой — это функция, которая принимает обработчик — любой callable — и возвращает обёрнутый callable: это в точности паттерн Декоратор из группы паттернов проектирования, а стек таких слоёв ведёт себя как Цепочка обязанностей.
Промежуточный слой журналирования
withLog оборачивает любой обработчик: он записывает в журнал приходящий запрос, даёт обработчику сделать свою работу и записывает ещё раз на выходе. Сам обработчик остаётся совершенно не в курсе.
Сервер работает как отдельный процесс, поэтому его журнал сам по себе не попадает в терминал демонстрации — промежуточный слой дописывает строки в небольшой файл журнала, а демонстрация печатает свежие строки журнала после каждого запроса.
Обернуть один раз — журналировать всё
Обёрнутый обработчик обслуживает два разных пути, и оба попадают в журнал — журналирование написано один раз, в обёртке, а не в каждом обработчике.
Вывод: PHP-скрипт
<?php
// Промежуточный слой: принимает обработчик, возвращает обёрнутый обработчик.
$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("<- готово");
};
}
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);
// Свободный порт, затем встроенный веб-сервер PHP как дочерний процесс.
$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); // что записал промежуточный слой
file_put_contents($logFile, "");
echo "тело: " . trim($body) . "\n";
}
proc_terminate($server);
proc_close($server);
unlink($routerFile);
unlink($logFile);
Промежуточный слой, который останавливает запрос
Промежуточный слой может и отказать: проверка аутентификации, не нашедшая верного ключа, отвечает 401 Unauthorized и никогда не вызывает следующий обработчик.
Сцепленные как withLog(withAuth($handler)), они пропускают запрос через слои по порядку — и ответ всплывает обратно через них же.
Слой аутентификации в цепочке
Запрос без ключа останавливается слоем аутентификации с кодом 401 — обработчик так и не запускается. С ключом запрос проходит всю цепочку и добирается до данных.
Вывод: PHP-скрипт
<?php
// withAuth может остановить запрос ещё до того, как обработчик будет вызван.
$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();
};
}
// Промежуточный слой аутентификации: нет верного ключа — нет следующего обработчика.
function withAuth(callable $next): callable {
return function () use ($next) {
if (($_SERVER["HTTP_X_API_KEY"] ?? "") !== "secret123") {
http_response_code(401);
echo "ключ API отсутствует или неверен\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);
// Свободный порт, затем встроенный веб-сервер PHP как дочерний процесс.
$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); // что записал промежуточный слой
file_put_contents($logFile, "");
echo " статус: $status - " . trim($body) . "\n";
}
proc_terminate($server);
proc_close($server);
unlink($routerFile);
unlink($logFile);