Как работает и может быть легко применен паттерн Middleware в PHP

В этом посте мы рассмотрим Middleware в PHP. Этот паттерн наиболее часто встречается в обработке запросов и ответов. Однако паттерн Middleware может применяться и в других местах. Мы рассмотрим, что такое промежуточное ПО, как оно работает, когда промежуточное ПО может быть полезным и какова может быть альтернатива промежуточному ПО.

Примечание: Шаблон промежуточного ПО не является частью шаблонов, представленных
Gang of Four, но лично я все равно рассматриваю его как паттерн, поскольку он может применяться в различных ситуациях.

Что такое промежуточное ПО?

Middleware в PHP — это слой действий/вызовов, которые обернуты вокруг части основной логики приложения. Эти промежуточные модули предоставляют возможность изменять либо вход, либо выход этой логики. Таким образом, они «живут» между входом и выходом, прямо посередине.

Хотя эти краткие объяснения, очевидно, очень просты для понимания и мгновенного восприятия, позвольте мне проиллюстрировать это на небольшом примере:

$input = [];
$output = $this->action($input);
Вход в полноэкранный режим Выход из полноэкранного режима

В этом примере слой Middleware способен:

  1. изменить $input до того, как он будет введен в $this->action()
  2. изменить $output после того, как он был возвращен $this->action().

Давайте посмотрим, как это делается.

Как работает промежуточное ПО

Уровень Middleware состоит из стека вызываемых элементов middleware. Это может быть простой Closure, вызываемый класс или метод класса.

Каждая из вызываемых частей промежуточного ПО обернута вокруг основного действия. Хорошо, я скажу это; это похоже на луковицу или подарок с несколькими слоями оберточной бумаги. Входные данные вводятся в первое промежуточное ПО (или самый внешний слой). Промежуточное ПО может что-то сделать с вводом, а затем передать этот (измененный) ввод следующему промежуточному ПО, и следующему, и следующему, пока он не достигнет конечного действия (ядра). К этому времени входные данные могут измениться незначительно или значительно.

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

Прежде чем мы рассмотрим некоторые примеры, давайте сначала изучим различные типы промежуточного ПО.

Типы промежуточного ПО

Хотя концепция промежуточного ПО практически одинакова, на самом деле существует два общих типа промежуточного ПО: Single Pass Middleware & Double Pass Middleware.

Однопроходное промежуточное ПО

Наиболее распространенным типом промежуточного ПО является Single Pass Middleware. В этом типе каждый вызываемый модуль промежуточного ПО получает два параметра:

  1. вход (например, запрос, сообщение, DTO или скалярное значение).
  2. callable для вызова следующего промежуточного ПО в цепочке, которое также получает один параметр: вход.

Примечание: В некоторых конвенциях вызываемый параметр называется $next для указания вызова следующего промежуточного ПО.

Этот тип промежуточного ПО может изменять входные данные и передавать измененную версию следующему промежуточному ПО. Чтобы повлиять на вывод, сначала вызывается следующий промежуточный модуль и получает его вывод. Затем он может изменить этот вывод и вернуть его вместо этого.

Вот небольшой пример промежуточного ПО для иллюстрации полного поведения.

function ($input, $next) {
  // Receive the input and change it.
  $input = $this->changeInput($input);

  // Call the next middleware or final action with the changed input, and receive the output.
  $output = $next($input); 

  // Change the output, and return it to the previous middleware or final consumer.
  return $this->changeOutput($output);
}
Вход в полноэкранный режим Выход из полноэкранного режима

Промежуточное ПО с двойным проходом

Второй тип промежуточного ПО — это Double Pass Middleware. В этом типе каждое промежуточное ПО также получает в качестве параметра объект вывода по умолчанию. Таким образом, «двойная» часть означает, что промежуточное ПО передает и вход, и выход следующему промежуточному ПО / конечному действию.

Почему это полезно? В типе Single Pass промежуточное ПО должно либо:

  1. Создать требуемый выходной объект при коротком замыкании (не вызывая $next, но возвращая другой результат).
  2. Вызвать промежуточное ПО $next для получения объекта вывода и изменить его.

В зависимости от типа вывода, может быть громоздко или даже нежелательно, чтобы промежуточное ПО зависело от службы или фабрики для создания этого типа вывода. И столь же нежелательным может быть вызов (и, возможно, инстанцирование) каждого другого промежуточного ПО и конечного действия, только для того, чтобы отбросить все, что они делают; и полностью изменить объект вывода.

Поэтому в Double Pass выходной объект по умолчанию создается заранее и передается по кругу. Таким образом, у промежуточного ПО уже есть объект вывода нужного типа, который оно может использовать, когда хочет выполнить замыкание.

Вот еще один небольшой пример, иллюстрирующий полное поведение.

function ($input, $output, $next) {
  // Quickly return the output object, instead of passing it along.
  if ($this->hasSomeReason($input)) {
    return $output;
  }

  // Call the next middleware and return that output instead of the default output.
    return $next($input, $output); 
}
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь мы знаем, как выглядит вызываемая промежуточная программа, но как мы можем обернуть все эти промежуточные программы поверх действия?

Простая реализация промежуточного ПО

Давайте погрузимся в код и создадим очень базовую реализацию промежуточного ПО Single Pass. В этом примере мы будем добавлять значение к строке (повторно), используя один класс промежуточного ПО.

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

Как мы уже видели, сначала нам нужно основное действие (или функция), вокруг которого будет обернуто наше промежуточное ПО. Наше действие будет просто получать
и возвращать строку.

$action = fn(string $input): string => $input;
Войти в полноэкранный режим Выйти из полноэкранного режима

Теперь мы создадим вызываемый класс промежуточного ПО, который мы сможем использовать несколько раз с разными значениями для вставки и добавления.

class ValueMiddleware
{
    public function __construct(string $value) {}

    public function __invoke(string $input, callable $next): string
    {
        // Prepend value to the input.
        $output = $next($this->value . $input);

        // Append value to the output.
        return $output . $this->value;
    }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Класс промежуточного ПО инстанцируется с $value. Этот экземпляр будет нашим вызываемым middleware, поскольку это вызываемый класс. Давайте создадим 3 таких экземпляра:

$middlewares = [
    new ValueMiddleware('1'),
    new ValueMiddleware('2'),
    new ValueMiddleware('3'),
];
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь мы добавим все эти промежуточные программы как слой вокруг нашего $action, а затем рассмотрим код.

foreach ($middlewares as $middleware) {
    $action = fn(string $input): string => $middleware($input, $action);
}
Вход в полноэкранный режим Выход из полноэкранного режима

Давайте посмотрим, что происходит в этом цикле.

  • Сначала закрытие $action — это наш простой обратный вызов, который возвращает $input.
  • В первом цикле $action перезаписывается новым закрытием, которое также получает $input (как и начальное действие). Когда это закрытие будет выполнено, оно вызовет первое промежуточное ПО и предоставит этот вход, а также наше начальное $action. Таким образом, внутри этого промежуточного ПО $next теперь является исходным действием.
  • Во втором и третьем цикле $action снова перезаписывается, но в этом промежуточном компоненте вызываемый $next — это не исходное действие, а закрытие, которое мы задали на предыдущей итерации.

Примечание: Как мы видим, $next никогда напрямую не ссылается на промежуточное ПО. Этого не может быть, потому что сигнатура метода middleware ожидает 2 параметра, а $next получает только один — вход. $next — это всегда вызываемый метод, который отвечает за вызов следующего промежуточного ПО. Таким образом, это может быть метод обработчика промежуточных программ, который содержит и отслеживает список (применяемых) промежуточных программ.

Вот и все. Наша реализация промежуточного ПО завершена. Теперь нам остается только запустить $action со значением и проверить ответ.

echo $action('value');
Вход в полноэкранный режим Выход из полноэкранного режима

Результатом этого, конечно же, будет: 123value123… подождите, что? Вы ожидали 123value321? Тогда вся эта история с луком имела бы больше смысла, верно? Но на самом деле все правильно.

Промежуточное ПО, которое добавляет и добавляет 1, применяется первым, но затем оборачивается 2, которое, в свою очередь, оборачивается 3. Таким образом, промежуточное ПО 3 первым добавляет 3 на вход, но оно же является последним промежуточным ПО, добавляющим это значение. Это немного заумно, но когда мы проверим $input и return для каждого промежуточного ПО, мы получим следующий список:

Middleware $input возврат
Middleware 3 значение 123value123
Middleware 2 3 значение 123value12
Среднее программное обеспечение 1 23 значение 123value1
Основная деятельность 123значение 123value

Работая вниз по списку $input и обратно по списку return, мы можем увидеть, через какие этапы проходит значение во всем этом потоке промежуточных программ.

Совет: Если вы хотите, чтобы порядок промежуточных программ был снаружи внутрь, где первой вызывается верхняя часть списка, вам следует array_reverse массив, или использовать итератор Last-In-First-Out (LIFO), например Stack.

Промежуточное программное обеспечение для запросов и ответов

Одно из самых распространенных мест, где вы можете встретить использование промежуточного ПО, — это фреймворк, который преобразует объект Request в объект Response. PSR-15: HTTP Server Request Handlers in the PHP Standard Recommendation (PSR), это рекомендация о том, как (PSR-7) объект Request должен быть обработан и превращен в объект Response. Эта рекомендация также содержит PsrHttpServerMiddlewareInterface. Этот интерфейс предпочитает использовать метод process в классе промежуточного ПО, но принцип тот же. Он получает входные данные (объект Request), изменяет их и передает их в RequestHandler, который вызовет следующее промежуточное или конечное действие.

Совет: Mezzio от Laminas — это минимальный PSR-7 middleware Framework, который предоставляет набор PSR-15 middlewares. И хотя большинство фреймворков, работающих с PSR-7 и PSR-15, используют Single Pass Middleware (так как именно это
это то, что рекомендует PSR-15), он также предоставляет Double Pass Middleware Decorator, так что они также будут работать с этими PSR.

Узнайте больше о декораторах и прокси

Примеры промежуточных модулей запроса и ответа

Промежуточные модули могут использоваться для выполнения всевозможных действий и проверок во время запроса, чтобы основное действие (контроллер) мог сосредоточиться на поставленной задаче. Давайте рассмотрим несколько примеров промежуточного ПО, которое может быть полезным.

Промежуточное ПО для проверки подделки межсайтовых запросов (CSRF)

Для предотвращения атаки CSRF фреймворк может добавить определенную проверку к запросу. Эта проверка должна произойти до того, как будет запущено основное действие. В этом случае промежуточное ПО может исследовать объект запроса. Если он считает запрос корректным, то может передать его следующему обработчику. Но если проверка не прошла, промежуточное ПО может немедленно оборвать запрос и вернуть ответ 403 Forbidden.

Многопользовательское промежуточное ПО

Некоторые приложения допускают многопользовательскую эксплуатацию, что означает, что один и тот же исходный код используется для разных клиентов, например, путем подключения отдельной базы данных / источника для каждого клиента. Промежуточное ПО может проанализировать запрос и выяснить (например, проверив URL запроса), какая база данных должна быть выбрана или какой клиент должен быть загружен. Оно даже может присоединить сущность Customer (если таковая существует) к объекту Request, так что основное действие может считать нужную информацию из объекта запроса, вместо того, чтобы самому определять нужного клиента.

Промежуточное ПО для обработки исключений

Еще один замечательный тип промежуточного ПО — это обработка исключений. Если это промежуточное ПО применяется в качестве внешнего промежуточного ПО, оно может обернуть весь поток от запроса к ответу в блок try-catch. Если возникло исключение, это промежуточное ПО может зарегистрировать его, а затем вернуть правильно настроенный объект ответа, содержащий информацию об исключении.

Другие примеры использования промежуточного ПО в реальном мире

Хотя промежуточное ПО чаще всего используется для обработки запросов и ответов, существуют и другие примеры использования. Вот несколько примеров:

Промежуточное ПО Symfony Messenger

Обработчик очереди в Symfony symfony/messenger использует промежуточное ПО для манипулирования Message, когда оно отправляется в очередь, и когда оно считывается из очереди и обрабатывается MessageHandler.

Одним из примеров является промежуточное программное обеспечение router_context. Поскольку сообщения (в основном) обрабатываются асинхронно, исходный Request недоступен. Это промежуточное ПО хранит исходное состояние Request (такие вещи, как хост и порт HTTP), когда он отправляется в очередь, и восстанавливает его, когда обрабатывается Message, так что обработчик может использовать этот контекст для построения таких вещей, как абсолютные URL.

Промежуточное программное обеспечение Guzzle

В то время как запрос и ответ находятся в некоторой степени в сфере запросов и ответов, HTTP-клиент Guzzle также поддерживает использование промежуточного программного обеспечения. Они снова позволяют вам изменять запрос и ответ на HTTP-запрос и обратно. Эта реализация работает немного по-другому, но все еще вращается вокруг обратного вызова, который запускает следующее промежуточное ПО / действие.

Альтернатива промежуточному ПО

Как мы уже видели, промежуточное ПО позволяет нам изменять вход и выход соответственно до и после основного действия в рамках одной функции / вызываемого модуля. Поэтому отличной альтернативой промежуточному ПО может быть предоставление крючков событий до и после основного действия. С помощью этих событий вы можете изменить входные данные до того, как они попадут в основное действие, и, возможно, даже вернуть их раньше. А после этого вы можете изменить выход.

Оба этих варианта работают достаточно хорошо. Например, Laravel использует промежуточное программное обеспечение вокруг потока запросов и ответов, а Symfony использует подход событий.

Примечание: Если вы хотите узнать больше о событиях, вы можете ознакомиться с моей (довольно объемной) статьей в блоге о диспетчеризации событий.

Спасибо за прочтение

При использовании промежуточного ПО вы можете легко создать разделение проблем в вашем коде. Каждое промежуточное ПО выполняет свою крошечную работу. Его можно легко тестировать, а основное действие может быть сосредоточено на том, что оно делает лучше всего, не беспокоясь о возможных побочных случаях или побочных эффектах. И хотя в настоящее время он в основном вращается вокруг (PSR-7) объектов запроса и ответа, паттерн может быть использован широко. Есть ли у вас другие идеи, где этот паттерн может быть полезен? Дайте мне знать!

Оцените статью
Procodings.ru
Добавить комментарий