[Из песочницы] Middleware и возможности Pipeline в Laravel
Laravel — это поистине большая и сложная система, которая старается решить большинство бытовых задач веб разработчика максимально элегантным способом и собрать в себе как можно больше инструментов и, что очень важно — с как можно более человеческим интерфейсом.
И сегодня речь пойдет об одном из таких инструментов, а точнее о его использовании и реализации со стороны программиста. Отсутствие полноценной документации, а также отсутствие русскоязычных статей и очень малое количество статей иностранных — подтолкнуло меня к решению раскрыть некую завесу тайны об этой интересной возможности фреймворка и выбору этой темы в качестве моей первой статьи на Хабре.
Middleware
В этой статье предполагается, что читатель уже знаком с базовым использованием этого функционала фреймворка, поэтому долго останавливаться на этом пункте не буду.
Из коробки Laravel предоставляет нам достаточно мощный функционал фильтрации входящих HTTP запросов к нашему приложению. Речь идет о всеми любимых (или нет) Middleware — с данными классами разработчик на пути освоения Laravel сталкивается достаточно быстро, еще на этапе чтения «The Basics» (Основы) пункта официальной документации, и это не удивительно — Middleware является одним из основных и важнейших кирпичиков, на основе которых строится вся система.
Примерами стандартных юз-кейсов этого компонента в Laravel являются: EncryptCookies/RedirectIfAuthenticated/VerifyCsrfToken, а в пример пользовательской реализации можно привести middleware локализации приложения (установки требуемой локализации на основе определенных данных запроса), перед передачей запроса дальше.
Глубже в бездну
Оставь надежду, всяк сюда входящий
Ну что же, теперь, когда с основными моментами покончено — мы можем углубиться в страшное для многих место — в альфа и омега, начало и конец — в исходники Laravel. Те, кто потянулся сразу закрывать статью — не торопитесь. На самом деле в исходном коде этого фреймворка почти нет чего-то действительно сложного с концептуальной стороны — создатели явно стараются не только создать ясный и удобный интерфейс работы со своим детищем, но и очень стараются делать тоже самое непосредственно на уровне исходного кода, что не может не радовать.
Я постараюсь максимально просто и доступно объяснить концепцию работы Middleware и Pipeline на уровне кода и логики, и постараюсь не углубляться туда — куда не нужно в рамках статьи. Так что, если в комментариях найдутся люди, знающие все строчки исходников наизусть — попрошу воздержаться от критики моего поверхностного повествования. Но любые рекомендации и исправления неточностей — лишь приветствуются.
Middleware — по ту сторону баррикад
Я верю, что изучение чего бы то ни было всегда дается проще, когда предоставляются хорошие примеры. Поэтому изучить этого таинственного зверя под именем Pipeline я предлагаю нам с вами вместе. Если действительно найдутся такие храбрецы — то перед дальнейшим чтением нам потребуется установить пустой проект Laravel версии 5.7 — версия обусловлена лишь тем, что она последняя на момент написания статьи, всё перечисленное должно быть идентично как минимум до версии 5.4. Те же, кто хочет просто узнать суть и выводы статьи — можете смело пропускать эту часть.
Что может быть лучше, чем изучение поведения какого-либо компонента, кроме как не изучение поведения уже встроенного в систему? Возможно что-то и может, но мы обойдемся без излишних усложнений и начнем наш разбор со стандартного Middleware —, а именно с самого простого и понятного из всей банды — RedirectIfAuthenticated:
class RedirectIfAuthenticated
{
/** Выполнить действия со входящим запросом
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param string|null $guard
* @return mixed
*/
public function handle($request, Closure $next, $guard = null)
{
if (Auth::guard($guard)->check()) {
return redirect('/');
}
return $next($request);
}
}
В любом классическом middleware классе существует главный метод, который непосредственно и должен обработать запрос, и передать обработку следующему в цепочке — в нашем случае — это метод handle. В этом конкретном классе обработка запроса достаточно проста — «если пользователь авторизован — то перенаправить его на главную страницу и, тем самым, прекратить выполнение цепочки».
Если мы посмотрим на регистрацию этого Middleware в app/Http/Kernel.php, то мы увидим, что он зарегистрирован в 'route middleware'. Чтобы нам узнать как же система работает с этим middleware — перейдем в класс, от которого наш app/Http/Kernel наследуется —, а наследуется он от класса Illuminate\Foundation\Http\Kernel. На данном этапе мы с вами непосредственно открываем врата в ад исходный код нашего фреймворка, а точнее — в самую важную и основную его часть — в ядро работы с HTTP.
Определение и реализация наших middleware в конструкторе ядра происходит следующим образом:
/** Создать новый объект HTTP Kernel класса.
* Create a new HTTP kernel instance.
*
* @param \Illuminate\Contracts\Foundation\Application $app
* @param \Illuminate\Routing\Router $router
* @return void
*/
public function __construct(Application $app, Router $router)
{
$this->app = $app;
$this->router = $router;
$router->middlewarePriority = $this->middlewarePriority;
foreach ($this->middlewareGroups as $key => $middleware) {
$router->middlewareGroup($key, $middleware);
}
foreach ($this->routeMiddleware as $key => $middleware) {
$router->aliasMiddleware($key, $middleware);
}
}
Код достаточно простой и понятный — для каждого middleware в массиве мы регистрируем его с алиасом/индексом в нашем роутере. Сами методды aliasMiddleware и middlewareGroups нашего Route класса — это простое добавление middleware в один из массивов объекта роутера. Но это не входит в контекст статьи, поэтому пропустим данный момент и двинемся дальше.
Что нас действительно интересует, так это метод sendRequestThroughRoute, дословно переводящийся, как отправитьЗапросЧерезРоут:
/** Отправить конкретный запрос через middleware / roter.
* Send the given request through the middleware / router.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
protected function sendRequestThroughRouter($request)
{
// * пропущена часть кода *
return (new Pipeline($this->app))
->send($request)
->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
->then($this->dispatchToRouter());
}
В качестве параметра данный метод получает запрос. На данном моменте нам следует снова заглянуть в код нашего RedirectIfAuthenticated. В методе handle нашего middleware мы тоже получаем запрос, эта заметка нам понадобится немного позже.
Код выше имеет очень понятный и читаемый интерфейс — «Трубопровод», который отправляет запрос через каждый из зарегистрированных middleware, а затем «передает» его в роутер. Прелестно и замечательно. Я думаю на этом этапе мы не будем пытаться декомпозировать данный участок кода дальше, я лишь вкратце опишу роль этого участка во всей системе:
Перед попаданием запроса в ваш контроллер — проходит достаточно много действий, начиная от простого парсинга самой url, и заканчивая инициализацией класса Request. Middleware в этой цепочке действий также участвует. Непосредственно классы middleware реализуют (почти) паттерн проектирования Цепочка обязанностей или Chain of Responsibility, таким образом каждый конкретный класс midleware — это лишь звено в этой цепочке.
Выше мы не просто так вернулись в наш изначально рассматриваемый класс RedirectIfAuthenticated. Запрос «циркулирует» по цепи, в том числе он проходит и через все, требуемые для роута middleware. Этот момент поможет нам с работой со своими собственными звеньями своей собственной цепи, об этом дальше.
Pipeline — канализация нашего приложения
Один из примеров реализации Pipeline мы видели выше. Но целью статьи было не только объяснение работы этого компонента на уровне интеграции с Laravel, а и объяснение базового принципа работы с этим классом в нашем собственном коде.
Сам класс можно найти по его полному определению с неймспейсом:
Illuminate\Pipeline\Pipeline
Применений данному компоненту может быть достаточно много, в зависимости от конкретной задачи, которую вам требуется решить, но одной из самых очевидных мотиваций является требование создания своей собственной цепи обработчиков запроса, которая не вмешивается в процессы всей системы и определяется исключительно на уровне вашей бизнес логики. Также интерфейс класса имеет достаточный уровень абстракции и имеет достаточную для реализации разного вида очередей функциональность.
Пример реализации в Laravel
Реализуем максимально простую и отдаленную от реальности цепочку запросов. В качестве данных мы будем использовать строку » HELLO WORLD», и с помощью двух обработчиков мы сформируем из нее строку «Hello User». Код намеренно упрощен.
Перед непосредственной реализацией нашей собственной «Трубы», нам нужно определить элементы этой трубы. Элементы пишутся по аналогии с middleware:
use Closure;
class StrToLowerAction
{
/**
* Handle an incoming request.
*
* @param string $content
* @param Closure $next
* @return mixed
*/
public function handle(string $content, Closure $next)
{
$content = strtolower($content);
return $next($content);
}
}
SetUserAction.php:
use Closure;
class SetUserAction
{
/**
* Handle an incoming request.
*
* @param string $content
* @param Closure $next
* @return mixed
*/
public function handle(string $content, Closure $next)
{
$content = ucwords(str_replace('world', 'user'));
return $next($content);
}
}
Затем мы создаем «трубопровод», определяем что за данные мы хотим по нему отправить, определяем через какую коллекцию обработчиков мы хотим эти данные отправить, а также определяем callback, который получает в качестве аргумента наши данные, пройденные через всю цепочку. В том случае, когда данные на протяжении цепочки у нас остаются неизменными — часть с callback’ом можно опустить:
$pipes = [
StrToLowerAction::class,
SetUserNameAction::class
];
$data = 'Hello world';
$finalData = app(Pipeline::class)
->send($data) // Данные, которые мы хотим пропустить через обработчики
->through($pipes) // Коллекция обработчиков
->then(function ($changedData) {
return $changedData; // Возвращаются данные, пройденные через цепочку
});
var_dump($finalData); // Возвращенные данные записаны в переменную $finalData
Также, если у вас есть желание или потребность определить свой собственный метод в обработчиках, интерфейс Pipeline предоставляет специальный метод via ('method_name'), тогда обработка цепи может быть написана таким образом:
$finalData = app(Pipeline::class)
->send($data)
->through($pipes)
->via('handle') // Здесь может быть любое название метода, вы должны гарантировать его наличие во всех обработчиках
->then(function ($changedData) {
return $changedData;
});
Непосредственно данные, которые мы проводим через обработчики — могут быть абсолютно любыми, как и взаимодействие с ними. Тайп хинтинг и установка типа возвращаемого в цепочке объекта поможет избежать ошибок с целостностью данных.
Заключение
Laravel предоставляет большое количество встроенных классов, и гибкость многих из них позволяет с достаточной простотой вести разработку чего-то сложного. В этой статье была рассмотрена возможность создания простых очередей для запросов на основе встроенного в Laravel класса Pipeline. Реализации этого класса в конечном коде могут быть абсолютно разными, а гибкость этого инструмента позволяет избавиться от многих лишних действий при построении определенных алгоритмов.
Как конкретно использовать данную возможность фреймворка — зависит от поставленных перед вами задач.