Middleware и возможности Pipeline в Laravel +30




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. Кстати, кому интересно — Laravel базируется на многих компонентах Symfony, конкретно в этой части — на HttpFoundation и HttpKernel.

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

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




К сожалению, не доступен сервер mySQL