Laravel Pipelines – одна из малоизвестных возможностей Laravel. Ее описание не встретишь в документации Laravel. Однако внутри фреймворка данный инструментарий используется довольно часто. В этой статье будет коротко рассказано о том, в каких случаях может быть полезен подход Laravel Pipelines, а также даны некоторые подробности применения и полезные ссылки.
Содержание статьи
- В какого рода задачах Laravel Pipelines даст выгоды
- Как эволюционирует код в описанных случаях
- Сопутствующие проблемы
- Полезные ссылки
В какого рода задачах Laravel Pipelines даст выгоды
Представим, что у нас есть некоторые исходные данные, с которыми надо произвести последовательно ряд действий.
Например, вы добавляете новый комментарий пользователя к статье. В контроллер приходят данные. Текст комментария проходит через ряд шагов перед добавлением в БД. Это может быть удаление запрещенных тегов, удаление лишних пробелов, блокировка постов с не нормативной лексикой.
Или, например, пользователь проходит аутентификацию. Шагами может быть проверка логина и пароля, учет попыток и блокировка по превышению числа неудачных попыток, создание пользователя (если его еще нет, когда такой подход допустим), применение прав пользователя, отправка приветственного email (или email с предупреждением о входе с не известной локации).
Как эволюционирует код в описанных случаях
Давайте рассмотрим как эволюционирует код пока мы не придем к Laravel Pipelines.
Сначала у нас есть контроллер и, вероятно, один самый важный шаг в нем. Затем появляются дополнительные шаги. Обработка эти шагов в контроллере привела бы нас к спагетти-коду.
Поэтому мы переносим обработку каждого шага в отдельный класс. В контроллере вызываются эти классы и в каждый класс передаются данные после работы другого класса. На этом этапе все еще есть проблемы с лишним кодом в контроллере по получению и передаче данных между задачами, а также, вероятно, отсутствие унификации интерфейсов у этих классов.
После этого мы могли бы унифицировать интерфейсы и обозначить просто имена шагов, которые нужно выполнить. А специальный механизм провел бы последовательно через все эти ступени наши данные.
Код с Laravel Pipelines выглядит примерно так:
use Illuminate\Pipeline\Pipeline;
// ...
public function create(Request $request)
{
$pipes = [
RemoveBadWords::class,
ReplaceLinkTags::clas,
RemoveScriptTags::class
];
$post = app(Pipeline::class)
->send($request->content)
->through($pipes)
->then(function ($content) {
return Post::create(['content' => 'content']);
});
}
В массиве $pipes
перечислены последовательно шаги, которые нужно выполнить коду. В метод send
передаются исходные данные. Анонимная функция передаваемая в метод then
выполняется после всех шагов.
При этом каждый шаг (pipe) должен быть классом, который реализует интерфейс подобный этому:
namespace App;
use Closure;
interface Pipe
{
public function handle($content, Closure $next);
}
Например:
namespace App;
use Closure;
class RemoveBadWords implements Pipe
{
public function handle($content, Closure $next)
{
// Здесь что-то делаем с данными (например, удаляем запрещенные слова)
// ...
// Далее происходит передача обновленных данных ($content) в следующий pipe
return $next($content);
}
}
Хотя названием метода handle
у классов шагов может быть другим. Если нужно другое, то для Pipeline нужно вызвать метод via()
и передать в него название используемого метода (если оно отличается от handle
).
Надо признать, что данный подход напоминает паттерн Цепочка обязанностей (Также известен как CoR, Chain of Command, Chain of Responsibility), хорошо описанный в книге “Погружение в Паттерны Проектирования” (автор Александр Швец).
Сопутствующие проблемы
Пока что все просто. Сложности приходят с некоторыми обстоятельствами:
- Иногда на каком-то шаге нужно прервать выполнение
- После прерывания выполнения может понадобиться почистить данные в БД от выполненных шагов
- Некоторые шаги в результате могут не только менять первоначальные данные, но и создавать какие-либо еще структуры
Вариант решениях этих проблем представлен в статье Handling Complex Data Flows (автор Jesse Schutt). Рекомендую ознакомиться со статьей, чтобы увидеть картину получше. Для краткости приведу пример решения здесь:
class FictitiousRegisterController {
public function register(Request $request): JsonResponse
{
$traveler = (new RegisterTraveler())->setRequest($request);
$pipes = [
ValidateInvitationCode::class,
CreateUser::class,
AssignPermissions::class,
HandleMailingList::class,
AssignToGroups::class,
SendWelcomeEmail::class,
];
try {
DB::beginTransaction();
return app(Pipeline::class)
->send($traveler)
->through($pipes)
->then(function ($traveler) {
DB::commit();
return response()->json([
'message' => 'Success',
]);
});
} catch (Exception $e) {
DB::rollback();
return response()->json([
'message' => $e->getMessage(),
]);
}
}
}
Здесь на базе пришедших в контроллер данных создается объект $traveler
, который будет содержать и измененные данные (полученные из исходных) и дополнительные, которые могут появиться после какого-либо шага.
Для очистки БД от результатов предыдущих шагов при неудаче используется отмена транзакции БД (DB::rollback()
).
Также автор статьи является одним из разработчиков пакета Zaengle Pipeline, который добавляет некоторые удобства при использовании Laravel Pipelines.
Полезные ссылки
Далее представлены ссылки, на материалы, которые помогли в написании данной статьи. Уверен, что некоторые из них заслуживают отдельного изучения для более детального представления.
- Laravel Pipelines, автор Martin Joo
- Пайплайны в Laravel. Часть 1, автор Italo Baeza Cabrera, перевод Алексей Широков
- Middleware и возможности Pipeline в Laravel, автор Станислав @Stasgar
- Understanding Laravel Pipelines, автор Jeff Ochoa
- Handling Complex Data Flows, автор Jesse Schutt
- Цепочка обязанностей, автор Александр Швец