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
- Цепочка обязанностей, автор Александр Швец