Laravel: Что такое Pipeline / Пайплайн

Источник: «Laravel Pipelines»
Pipeline/Пайплайн — одна из малоизвестных возможностей Laravel. Он часто используется в самом фреймворке, например, маршрутизация, но не так много разработчиков его используют. В этой статье я попытаюсь объяснить их и показать несколько примеров.

Что такое Pipeline/Пайплайн в Laravel

Вместо разговоров давайте посмотрим:

app(Pipeline::class)
->send('<p>This is the HTML content of a blog post</p>')
->through([
ModerateContent::class,
RemoveScriptTags::class,
MinifyHtml::class,
])
->then(function (string $content) {
return Post::create([
'content' => $content,
...
]);
});

В этом примере у нас есть содержимое нового сообщение в блоге, и перед сохранением в базе данных мы хотим выполнить следующие действия:

Это три задачи или шага одного большого действия — создания записи. Для сопоставления этих задач можно использовать Pipeline. Давайте разберём этот код.

app(Pipeline::class)

Он получает экземпляр Pipeline из контейнера.

->send('<p>This is the HTML content of a blog post</p>')

Он отправляет это строку через pipe/пайп. Это строка путешественник пайпа. Пайпа, который вы определяете с помощью:

->through([
ModerateContent::class,
RemoveScriptTags::class,
MinifyHtml::class,
])

Три основных класса. Они называются остановки пайплайна. В этом примере каждая остановка получает строку и возвращает строку, что-то вроде этого:

"Post content with bad word and <script> tag"
->
ModerateContent
->
"Post content with <script> tag"
->
RemoveScriptTags
->
"Post content"
->
MinifyHtml
->
"Post content (minified)"

Содержимое контейнера проходит через пайплайн и обрабатывается на каждой остановке. Последняя остановка:

->then(function (string $content) {
return Post::create([
'content' => $content,
...
]);
});

Вы можете задать конечную остановку с помощью then(). После того как вся работа была проделана, мы можем создать новую запись. Это как Promise в JavaScript.

Классы Pipe (ModerateContent, RemoveScriptTags, MinifyHtml) — реально простые классы с одним методом handle():

class ModerateContent
{
public function handle(string $content, Closure $next): string
{
// Content moderation logic
$moderatedContent = 'Do something here';
return $next($moderatedContent);
}
}

Единственная особенность — параметр $next. Это та же концепция, что и в middleware, где одно middleware выполняет свою работы и вызывает следующее middleware. То же самое применимо и здесь. Канал ModerateContent выполняет свою работу и вызывает следующий канал с новым, модерированным контентом.

Ладно, это основы. Я думаю, вы поняли идею. Теперь давайте сделаем несколько более реалистичных примеров.

Laravel Пайплайн/Pipeline в Действии

Теперь представьте, что у вас есть приложение для электронной коммерции или планирования ресурсов предприятия. Когда пользователь создаёт заказ, часто приходится делать что-то вроде этого:

И много других задач. Но мне это кажется хорошей возможностью использовать пайплайны. Поэтому попробуйте смоделировать описанные выше шаги с помощью пайплайна.

Сначала давайте просто воспользуемся простым подходом, а позже сделаем его более элегантным. Это миграция для таблицы заказов:

Schema::create('orders', function (Blueprint $table) {
$table->id();
$table->string('customer_name')->nullable(false);
$table->float('net_amount')->nullable(false);
$table->float('pay_amount')->nullable(true);
$table->timestamps();
});

Это пример проекта, поэтому я сделал его очень простым. У нас нет ни order_items, ни products, ни customers. У нас только одна модель. Это основная идея в том, что конечная точка API получит net_amount, а наш пайплайн рассчитает pay_amount. Это сумма, которую клиент должен заплатить.

Нам нужно три стоп класса для трёх задач. Каждый получит модель Order, и вернёт модель Order. Итак, идея заключается в том, что каждый класс будет изменять свойство pay_amount заказа.

Это Controller, где мы создаём Pipeline:

class OrderController extends Controller
{
public function store(Request $request)
{
$order = Order::create([
'customer_name' => $request->customerName,
'net_amount' => $request->netAmount,
'pay_amount' => $request->netAmount,
]);

$pipes = [
ApplyDiscount::class,
AddVat::class,
AddShipping::class,
];

$order = app(Pipeline::class)
->send($order)
->through($pipes)
->then(function (Order $order) {
$order->save();
return $order;
});

return response($order, Response::HTTP_CREATED);
}
}

Сначала мы создаём заказ и устанавливаем pay_amount на чистую сумму. После этого создаём пайплайн и отправляем заказ через него. Теперь, давайте посмотрим, как реализованы стоп или пайп классы.

class ApplyDiscount
{
public function handle(Order $order, Closure $next): Order
{
$order->pay_amount *= 0.9;
return $next($order);
}
}

Этот просто даёт 10% скидку на каждый заказ.

class AddVat
{
public function handle(Order $order, Closure $next): Order
{
$order->pay_amount *= 1.15;
return $next($order);
}
}

Этот всегда добавляет 15% НДС.

class AddShipping
{
public function handle(Order $order, Closure $next): Order
{
$order->pay_amount += 10;
return $next($order);
}
}

И доставка всегда 10 долларов.

Как видите, каждый стоп — это очень простой класс. Он принимает заказ, изменяет pay_amount и затем возвращает новый заказ.

Соединение Пайплайнов с Action

Не знаю как вам, а мне кажется, что action и пайплайны могут очень хорошо работать вместе, давайте попробуем их смешать! Если вы не знаете, что такое action класс, можете прочитать об этом здесь.

Этот подход будет чрезмерным для этого конкретного примера, но он предназначен для демонстрации.

Сначала нужно создать action из кода контроллера:

class CreateOrder
{
public function __construct(private CalculatePayPrice $calculatePayPrice)
{
}

public function execute(Request $request): Order
{
$order = Order::create([
'customer_name' => $request->customerName,
'net_amount' => $request->netAmount,
'pay_amount' => $request->netAmount,
]);

return $this->calculatePayPrice->execute($order);
}
}

Создаёт заказ, затем передаёт его action CalculatePayPrice. Это класс, в котором мы реализуем пайплайн:

class CalculatePayPrice
{
public function execute(Order $order): Order
{
$pipes = [
ApplyDiscount::class,
AddVat::class,
AddShipping::class,
];

return app(Pipeline::class)
->send($order)
->through($pipes)
->via('execute')
->then(function (Order $order) {
$order->save();
return $order;
});
}
}

Это почти как пользовательская история:

Конечно, вы можете легко добиться такого же поведения без пайплайна, но мне это кажется улучшением. Он инкапсулирует, склеивает разные шаги, и мне это очень нравится.

Честно говоря, раньше я не использовал Laravel Pipeline, потому что не знал о них. И я уверен, что не буде использовать их так часто, но в некоторых ситуациях они могут быть отличным решением. Так что хорошо держать их в своём арсенале.

Дополнительные материалы

Предыдущая Статья

Laravel: Объекты-Значения повсюду

Следующая Статья

Laravel: Валидация данных приложения