Laravel Pipelines

Laravel Pipelines – одна из малоизвестных возможностей Laravel. Ее описание не встретишь в документации Laravel. Однако внутри фреймворка данный инструментарий используется довольно часто. В этой статье будет коротко рассказано о том, в каких случаях может быть полезен подход 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.

Полезные ссылки

Далее представлены ссылки, на материалы, которые помогли в написании данной статьи. Уверен, что некоторые из них заслуживают отдельного изучения для более детального представления.

Оставить ответ

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