- PVSM.RU - https://www.pvsm.ru -
Laravel — это поистине большая и сложная система, которая старается решить большинство бытовых задач веб разработчика максимально элегантным способом и собрать в себе как можно больше инструментов и, что очень важно — с как можно более человеческим интерфейсом.
И сегодня речь пойдет об одном из таких инструментов, а точнее о его использовании и реализации со стороны программиста. Отсутствие полноценной документации, а также отсутствие русскоязычных статей и очень малое количество статей иностранных — подтолкнуло меня к решению раскрыть некую завесу тайны об этой интересной возможности фреймворка и выбору этой темы в качестве моей первой статьи на Хабре.
В этой статье предполагается, что читатель уже знаком с базовым использованием этого функционала фреймворка, поэтому долго останавливаться на этом пункте не буду.
Из коробки Laravel предоставляет нам достаточно мощный функционал фильтрации входящих HTTP запросов к нашему приложению. Речь идет о всеми любимых (или нет) Middleware [1] — с данными классами разработчик на пути освоения Laravel сталкивается достаточно быстро, еще на этапе чтения «The Basics» (Основы) пункта официальной документации, и это не удивительно — Middleware является одним из основных и важнейших кирпичиков, на основе которых строится вся система.
Примерами стандартных юз-кейсов этого компонента в Laravel являются: EncryptCookies/RedirectIfAuthenticated/VerifyCsrfToken, а в пример пользовательской реализации можно привести middleware локализации приложения (установки требуемой локализации на основе определенных данных запроса), перед передачей запроса дальше.
Ну что же, теперь, когда с основными моментами покончено — мы можем углубиться в страшное для многих место — в альфа и омега, начало и конец — в исходники Laravel [2]. Те, кто потянулся сразу закрывать статью — не торопитесь. На самом деле в исходном коде этого фреймворка почти нет чего-то действительно сложного с концептуальной стороны — создатели явно стараются не только создать ясный и удобный интерфейс работы со своим детищем, но и очень стараются делать тоже самое непосредственно на уровне исходного кода, что не может не радовать.
Я постараюсь максимально просто и доступно объяснить концепцию работы Middleware и Pipeline на уровне кода и логики, и постараюсь не углубляться туда — куда не нужно в рамках статьи. Так что, если в комментариях найдутся люди, знающие все строчки исходников наизусть — попрошу воздержаться от критики моего поверхностного повествования. Но любые рекомендации и исправления неточностей — лишь приветствуются.
Я верю, что изучение чего бы то ни было всегда дается проще, когда предоставляются хорошие примеры. Поэтому изучить этого таинственного зверя под именем Pipeline я предлагаю нам с вами вместе. Если действительно найдутся такие храбрецы — то перед дальнейшим чтением нам потребуется установить пустой проект Laravel версии 5.7 — версия обусловлена лишь тем, что она последняя на момент написания статьи, всё перечисленное должно быть идентично как минимум до версии 5.4. Те же, кто хочет просто узнать суть и выводы статьи — можете смело пропускать эту часть.
Что может быть лучше, чем изучение поведения какого-либо компонента, кроме как не изучение поведения уже встроенного в систему? Возможно что-то и может, но мы обойдемся без излишних усложнений и начнем наш разбор со стандартного Middleware — а именно с самого простого и понятного из всей банды — RedirectIfAuthenticated:
class RedirectIfAuthenticated
{
/** Выполнить действия со входящим запросом
* Handle an incoming request.
*
* @param IlluminateHttpRequest $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 наследуется — а наследуется он от класса IlluminateFoundationHttpKernel. На данном этапе мы с вами непосредственно открываем врата в ад исходный код нашего фреймворка, а точнее — в самую важную и основную его часть — в ядро работы с HTTP.
Определение и реализация наших middleware в конструкторе ядра происходит следующим образом:
/** Создать новый объект HTTP Kernel класса.
* Create a new HTTP kernel instance.
*
* @param IlluminateContractsFoundationApplication $app
* @param IlluminateRoutingRouter $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 IlluminateHttpRequest $request
* @return IlluminateHttpResponse
*/
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 [3]. Middleware в этой цепочке действий также участвует. Непосредственно классы middleware реализуют (почти) паттерн проектирования Цепочка обязанностей или Chain of Responsibility [4], таким образом каждый конкретный класс midleware — это лишь звено в этой цепочке.
Выше мы не просто так вернулись в наш изначально рассматриваемый класс RedirectIfAuthenticated. Запрос «циркулирует» по цепи, в том числе он проходит и через все, требуемые для роута middleware. Этот момент поможет нам с работой со своими собственными звеньями своей собственной цепи, об этом дальше.
Один из примеров реализации Pipeline мы видели выше. Но целью статьи было не только объяснение работы этого компонента на уровне интеграции с Laravel, а и объяснение базового принципа работы с этим классом в нашем собственном коде.
Сам класс можно найти по его полному определению с неймспейсом:
IlluminatePipelinePipeline
Применений данному компоненту может быть достаточно много, в зависимости от конкретной задачи, которую вам требуется решить, но одной из самых очевидных мотиваций является требование создания своей собственной цепи обработчиков запроса, которая не вмешивается в процессы всей системы и определяется исключительно на уровне вашей бизнес логики. Также интерфейс класса имеет достаточный уровень абстракции и имеет достаточную для реализации разного вида очередей функциональность.
Реализуем максимально простую и отдаленную от реальности цепочку запросов. В качестве данных мы будем использовать строку " 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. Реализации этого класса в конечном коде могут быть абсолютно разными, а гибкость этого инструмента позволяет избавиться от многих лишних действий при построении определенных алгоритмов.
Как конкретно использовать данную возможность фреймворка — зависит от поставленных перед вами задач.
Автор: Станислав
Источник [5]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/php-2/298543
Ссылки в тексте:
[1] Middleware: https://laravel.com/docs/middleware
[2] исходники Laravel: https://github.com/laravel/framework
[3] Request: https://laravel.com/api/5.7/Illuminate/Http/Request.html
[4] Цепочка обязанностей или Chain of Responsibility: https://ru.wikipedia.org/wiki/%D0%A6%D0%B5%D0%BF%D0%BE%D1%87%D0%BA%D0%B0_%D0%BE%D0%B1%D1%8F%D0%B7%D0%B0%D0%BD%D0%BD%D0%BE%D1%81%D1%82%D0%B5%D0%B9
[5] Источник: https://habr.com/post/429214/?utm_source=habrahabr&utm_medium=rss&utm_campaign=429214
Нажмите здесь для печати.