Зарегистрируйтесь, чтобы продолжить обучение

Middlewares Веб-разработка на PHP

При разработке веб-приложений разработчики сталкиваются с задачами, которые нужно решать на каждом этапе обработки запроса. Например, если у нас есть несколько маршрутов, которые требуют аутентификации, или если нам нужно логировать все входящие запросы, то реализация этой логики в каждом маршруте может привести к дублированию кода и усложнению поддержки приложения. Это также увеличивает вероятность ошибок, так как любой измененный маршрут необходимо будет обновить в нескольких местах. Использование мидлвар позволяет решить эти проблемы.

Мидлвара - это функции, которые обрабатывают запросы и ответы в веб-приложениях. Они выполняются между получением запроса от клиента и отправкой ответа. Они могут выполнять различные задачи, такие как проверка прав доступа, логирование или преобразование данных в ответе

В этом уроке мы рассмотрим, как создавать и использовать мидлвары в 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 добавляет мидлвары в виде концентрических слоев, окружающих основное приложение. Каждая новая мидлвара окружает все существующие слои.

pipline

Когда мы запускаем приложение, объект 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, и это тоже паттерн


Дополнительные материалы

  1. Middlewares

Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff
Рекомендуемые программы
профессия
от 25 000 ₸ в месяц
Разработка веб-приложений на Laravel
10 месяцев
с нуля
Старт 26 декабря

Используйте Хекслет по-максимуму!

  • Задавайте вопросы по уроку
  • Проверяйте знания в квизах
  • Проходите практику прямо в браузере
  • Отслеживайте свой прогресс

Зарегистрируйтесь или войдите в свой аккаунт

Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»
Изображение Тото

Задавайте вопросы, если хотите обсудить теорию или упражнения. Команда поддержки Хекслета и опытные участники сообщества помогут найти ответы и решить задачу