« back published by @mmartin_joo on November 13, 2022
Laravel Pipelines
Pipeline is one of Laravel's less-known features. It's often used in the framework itself, for example routing, but not so many developers use it. In this article, I try to explain them, and show some examples.
What Is a Pipeline in Laravel?
Instead of talking let's take a look:
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,
...
]);
});
In this example we have the content of a new blog post, and before saving into the database we want to do the following tasks:
- Removing bad words, moderating the content.
- Remove every script tag from the HTML content.
- Minify the HTML.
These are 3 tasks or steps of one bigger action which is create a post. Pipelines can be used to "compose" these tasks. Let's break it down.
app(Pipeline::class)
It gets an instance of Pipeline from the container.
->send('<p>This is the HTML content of a blog post</p>')
It sends this string through the pipe. This string is the 'traveler' of the pipe. The pipe that you define with:
->through([
ModerateContent::class,
RemoveScriptTags::class,
MinifyHtml::class,
])
3 very basic classes. They called the "stops" of a pipeline. In this example each stops get a string and returns a string., something like this:
"Post content with bad word and <script> tag"
->
ModerateContent
->
"Post content with <script> tag"
->
RemoveScriptTags
->
"Post content"
->
MinifyHtml
->
"Post content (minified)"
The content of the post travels through the pipeline and it gets processed at each stop. The last stop is:
->then(function (string $content) {
return Post::create([
'content' => $content,
...
]);
});
You can define a final stop with then(). After all work has been done we can create the new post. It's like a Promise in Javascript.
The Pipe classes (ModerateContent, RemoveScriptTags, MinifyHtml) are really simple classes with a single handle() method:
class ModerateContent
{
public function handle(string $content, Closure $next): string
{
// Content moderation logic
$moderatedContent = 'Do something here';
return $next($moderatedContent);
}
}
The only special thing is the $next parameter. This is the same concept as in middlewares, where one middleware will do its job and call another middleware. The same applies here. The ModerateContent pipe does its job and calls the next pipe with the new, moderated content.
Okay, that's the basics. I think you get the idea. Now let's make some more realistic examples.
Laravel Pipelines in Action
Now imagine you have an e-commerce or an enterprise resource planning application. When a user creates an order we often need to do stuff like that:
- Calculate VAT.
- Apply some discount.
- Add shipping fee.
And a lot of other tasks. But this sounds like to me as a good opportunity to use pipelines. So try to model the steps above with a pipeline.
First, let's just go with the simple approach, and later on we make it more elegant. This is the migration for the orders table:
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();
});
This is an example project, so I made it really simple. We have no order_items, or products or customers. We only have one model. The main idea here that the API endpoint will get the net_amount, and our pipeline will calculate the pay_amount. That's the amount the customer needs to pay.
We need 3 "stop" class for the 3 tasks. Each will get an Order model and will return an Order model. So the idea here that each class will modify the pay_amount property of the order.
This is the Controller, where we create the 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);
}
}
First we create the order and set the pay_amount to the net amount. After that we create the pipeline and send the order through it. Now let's see how the "stop" or pipe classes implemented.
class ApplyDiscount
{
public function handle(Order $order, Closure $next): Order
{
$order->pay_amount *= 0.9;
return $next($order);
}
}
It just gives 10% discount for every order.
class AddVat
{
public function handle(Order $order, Closure $next): Order
{
$order->pay_amount *= 1.15;
return $next($order);
}
}
It always adds 15% VAT.
class AddShipping
{
public function handle(Order $order, Closure $next): Order
{
$order->pay_amount += 10;
return $next($order);
}
}
And the shipping is always 10 bucks.
As you can see each "stop" is a really simple class. It takes an order, modify the pay_amount and then returns the new order.
Composing Pipelines With Actions
I don't know about you but it seems to me that actions and pipelines can work together very well, so let's try and mix them! If you don't know what an action class is, you can read about here.
This approach will be overengineering for this particular example, but it's just for presentation purpose so bare with me.
First we need to create an action from the controller code:
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);
}
}
It creates the order and then passes it to the CalculatePayPrice action. This is the class where we implement the pipeline:
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;
});
}
}
This is almost like a user story:
- Create a new order
- Calculate the price
- Add VAT
- Apply discount
- Add shipping fee
- Calculate the price
Of course you can easily achieve the same behavior without a Pipeline, but it seems like an improvement for me. It encapsulates, glues together different steps and I quite like it.
To be honest I haven't used Laravel Pipelines in the past, because I didn't know about them. And I'm sure I won't use them that much, but I think they can be a great solution in some situations. So it's a good thing to have them in my arsenal.