При разработке веб-приложений разработчики сталкиваются с задачами, которые нужно решать на каждом этапе обработки запроса. Например, если у нас есть несколько маршрутов, которые требуют аутентификации, или если нам нужно логировать все входящие запросы, то реализация этой логики в каждом маршруте может привести к дублированию кода и усложнению поддержки приложения. Это также увеличивает вероятность ошибок, так как любой измененный маршрут необходимо будет обновить в нескольких местах. Использование мидлвар позволяет решить эти проблемы.
Мидлвара - это функции, которые обрабатывают запросы и ответы в веб-приложениях. Они выполняются между получением запроса от клиента и отправкой ответа. Они могут выполнять различные задачи, такие как проверка прав доступа, логирование или преобразование данных в ответе
В этом уроке мы рассмотрим, как создавать и использовать мидлвары в Javalin, а также изучим примеры их применения
Оборачивание
Прежде чем перейти к изучению непосредственно мидлвар, разберем их принцип действия на примере обычных функций.
Представим, что у нас есть функция, которая прибавляет 5 к аргументу:
function addFive(int $x): int
{
return $x + 5;
}
Наша задача расширить поведение этой функции, при этом не меняя саму функцию. Например, мы хотим прибавлять 5 к удвоенному значению аргумента. Чтобы решить эту задачу, мы можем написать новую функцию, которая будет внутри себя использовать уже существующую:
function doubleAddFive(int $x): int
{
return addFive($x * 2);
}
Точно так же мы можем расширить и вторую функцию:
function calculate(int $x)
{
return doubleAddFive($x - 10) - 3;
}
В общем случае расширение функции выглядит так:
function nextFunction(/* args */)
{
// preprocessing
$result = prevMethod(/* updatedArgs */);
// afterprocessing
return /* newResult */;
}
Чтобы расширить поведение функции, нужно создать новую функцию, которая будет использовать исходную. Главное условие — интерфейсы обоих методов должны совпадать: количество и тип аргументов, а также тип возвращаемого результата должны быть одинаковыми. Тогда вы можете просто заменить в коде вызов одной функции на вызов другой, обернутой, и код продолжит работать без изменений, поскольку интерфейс остался прежним. В этом случае код, использующий вашу новую функцию, не заметит, что она обернута и его не потребуется переписывать. Этот подход также называется декорированием и описывается в справочниках по шаблонам проектирования как "паттерн Декоратор".
По похожей идее работают мидлвары. Они расширяют функционал обработчиков запросов, оборачивая их в подобные методы. При этом оригинальный обработчик остается неизменным
Использование мидвар
Фреймворк Slim имеет встроенную поддержку мидлвар. Для подключения мидлвар в Javalin используются метод add()
Рассмотрим пример кода с подключением мидлвары:
<?php
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
require __DIR__ . '/../vendor/autoload.php';
$app = AppFactory::create();
$logMiddleware = function (Request $request, RequestHandler $handler): ResponseInterface {
// Логируем информацию о запросе
// Тут мы для простоты используем обычную функцию
// В реальности мы бы использовали специальную библиотеку, например Monolog
error_log("Request: {$request->getMethod()} {$request->getUri()}");
// Передаем управление следующей мидлваре или обработчику
return $handler->handle($request);
};
$app->add($logMiddleware);
$app->get('/', function ($request, $response, $args) {
$response->getBody()->write("Welcome to Hexlet!");
return $response;
});
// ...
$app->run();
В приведенном примере подключена мидлвара, которая для каждого входящего запроса выводит в консоль сообщение с информацией о запросе
Каждая мидлвара представляет собой анонимную функцию, которая принимает два аргумента:
- Объект
Request
, который представляет собой запрос - Объект
RequestHandler
позволяет определять, как обрабатывать входящие запросы и генерировать ответы
Функция обязательно должна вернуть объект класса, реализующего Psr\Http\Message\ResponseInterface
Каждый раз, когда мы используем add()
, очередная мидлвара добавляется к приложению. Slim добавляет мидлвары в виде концентрических слоев, окружающих основное приложение. Каждая новая мидлвара окружает все существующие слои.
Когда мы запускаем приложение, объект Request
обходит слои мидвар снаружи внутрь. Первым выполняется последний добавленный слой промежуточного ПО. Каждая мидлвара выполняет свою функцию, а затем передает управление следующей в цепочке
Сначала запрос попадает в самый внешний слой, затем в следующий слой, и так далее, пока не попадает в само приложение. После того как приложение обработает соответствующий запрос, полученный объект Response
выходит из приложения и проходит по слоям мидвар ПО изнутри наружу. В конце концов, конечный объект Response
выходит из внешнего промежуточного ПО, сериализуется в HTTP-ответ и возвращается клиенту
<?php
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
use Slim\Factory\AppFactory;
require __DIR__ . '/../vendor/autoload.php';
$app = AppFactory::create();
$logMiddleware = function (Request $request, RequestHandler $handler): ResponseInterface {
// Логируем информацию о запросе
error_log("Request: {$request->getMethod()} {$request->getUri()}");
// Передаем управление следующей мидлваре или обработчику
return $handler->handle($request);
};
$afterMiddleware = function (Request $request, RequestHandler $handler) {
// Передаем управление следующей мидлваре или обработчику
$response = $handler->handle($request);
// Меняем ответ, после того как приложение обработало запрос
$response = $response->withHeader('X-Custom-Header', 'value');
return $response;
};
// Первой выполнится последняя добавленная мидлвара logMiddleware
$app->add($afterMiddleware);
$app->add($logMiddleware);
$app->get('/', function ($request, $response, $args) {
$response->getBody()->write("Welcome to Hexlet!");
return $response;
});
$app->run();
В примере выше мы подключили мидлвары, одна из которых выводит в консоль сообщение, а другая добавляет кастомный заголовок X-Custom-Header в каждый ответ.
Запустим сервер и сделаем запрос:
HTTP/1.1 200 OK
Connection: close
Content-type: text/html; charset=UTF-8
Date: Fri, 29 Nov 2024 07:56:52 GMT
Host: localhost
# Вот наш заголовок
X-Custom-Header: value
X-Powered-By: PHP/8.3.11
Welcome to Hexlet!
При этом в консоли, где запущено приложение, мы можем увидеть вывод:
Request: GET http://localhost:8080/
Терминальная мидлвара
Далеко не всегда мы хотим двигаться вглубь по всей цепочке добавленных мидлвар. Более того, в какой-то момент одна из мидлвар должна взять обработку запроса на себя. Чтобы завершить обработку, мидлвар может создать и вернуть ответ, не передавая управление следующему мидлвару или обработчику. Это прекращает движение по цепочке, пропуская оставшиеся мидлвары для текущего запроса, и происходит отправка ответа клиенту
Такой подход полезен, когда необходимо остановить выполнение дальнейших мидлвар или маршрутов в случае ошибки аутентификации или других условий, при которых дальнейшая обработка не имеет смысла. Например, можно создать мидлвару для проверки наличия определенного параметра в запросе, и если параметр отсутствует, отправить ответ с ошибкой, пропустив выполнение остальных мидлвар и обработчиков.
<?php
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
use Slim\Factory\AppFactory;
require __DIR__ . '/../vendor/autoload.php';
$app = AppFactory::create();
$beforeMiddleware = function (Request $request, RequestHandler $handler) use ($app) {
$id = $request->getQueryParam('id');
if (!$id) {
// Если параметр не установлен,
// Не передаем управление дальше, а сразу создаем и возвращаем ответ
$response = $app->getResponseFactory()->createResponse();
$response->getBody()->write('Missing id parameter');
return $response->withStatus(400);
}
// В другом случае передаем управление дальше
// В следующую мидлвару или обработчик
return $handler->handle($request);
};
$afterMiddleware = function (Request $request, RequestHandler $handler) {
$response = $handler->handle($request);
$response = $response->withHeader('X-Custom-Header', 'value');
return $response;
};
$app->add($afterMiddleware);
$app->add($beforeMiddleware);
$app->get('/resource', function ($request, $response, $args) {
$id = $request->getQueryParam('id');
$response->getBody()->write("Resourse {$id}");
return $response;
});
$app->run();
Запустим приложение и сделаем запрос:
HTTP/1.1 400 Bad Request
Connection: close
Content-type: text/html; charset=UTF-8
Date: Fri, 29 Nov 2024 07:58:39 GMT
Host: localhost
X-Powered-By: PHP/8.3.11
Missing id parameter
Видим, что выполнение остановилось на первой мидлваре и сразу вернулся ответ
У такого поведения, когда есть цепочка функций и любая из них в процессе обработки может принять решение остановки цепочки и возврата ответа, есть имя. Такие цепочки называют chain responsibility, и это тоже паттерн
Дополнительные материалы
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.