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:

RedirectIfAuthenticated.php

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 в конструкторе ядра происходит следующим образом:

Illuminate\Foundation\Http\Kernel(Application $app, Router $router)

    /** Создать новый объект 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, дословно переводящийся, как отправитьЗапросЧерезРоут:

Illuminate\Foundation\Http\Kernel::sendRequestThroughRouter($request)

    /** Отправить конкретный запрос через 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:

Определение обработчиков

StrToLowerAction.php:

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

Как конкретно использовать данную возможность фреймворка — зависит от поставленных перед вами задач.

FavoriteLoadingДобавить в избранное
Posted in Без рубрики

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *