Pipelines modernize your code
Pipelines are an incredible way to manage logic that can be broken down into smaller pieces. We recently rolled out an example of pipelines to better handle order processing.
What is a pipeline?
Pipelines are a powerful design pattern that allows you to process data through a series of stages or actions. Each stage performs a specific task on the input data and passes it to the next stage in the pipeline. These stages can be as simple as calling a function or as complex as a base class that is extended by multiple steps classes. By breaking down complex tasks into smaller, manageable steps, pipelines promote a clean and modular codebase that can be easily maintained and tested.
Why Should You Use Pipelines?
Pipelines may not be the best solution for every problem, but here are a couple of reasons why you should consider using them:
- Code Re-usability: Pipelines enable you to reuse individual stages across multiple workflows, reducing code duplication and promoting a cleaner codebase.
- Readability and Maintainability: By breaking down complex logic into smaller stages, pipelines enhance code readability, making it easier to understand, modify, and maintain.
- Flexibility and Modularity: Pipelines allow you to add, remove, or reorder stages effortlessly, providing the flexibility to adapt to changing requirements without major code refactoring.
- Testability: Pipelines make it easier to test individual stages of your workflow, allowing you to write more focused tests that are easier to maintain.
Code Example: Processing Orders Using Pipelines
Let's take a look at an example of how Pipelines can be used to process orders in an e-commerce application.
Suppose we have an order object that needs to go through multiple stages of processing. We want to validate and sanitize the order data, apply discounts, calculate the total amount, process the payment, and send an order confirmation email. We can use Pipelines to break down this logic into smaller stages, making it easier to read and maintain.
Here is a basic Pipeline class that allows us to add stages and process the order through each stage:
use Closure;
class OrderPipeline
{
protected $stages = [];
/**
* Add a stage to the pipeline.
*
* @param Closure $stage
*
* @return $this
*/
public function addStage(Closure $stage)
{
$this->stages[] = $stage;
return $this;
}
/**
* Process the order through the pipeline.
*
* @param $order
*
* @return object
*/
public function process($order)
{
// Loop through each stage in the pipeline, passing the order through each stage
$pipeline = array_reduce(
$this->stages,
function ($carry, $stage) {
return function ($order) use ($carry, $stage) {
return $stage($carry($order));
};
},
function ($order) {
return $order;
}
);
return $pipeline($order);
}
}
And here is an example of how we can use this class to process orders:
// Usage example
// Create an order object
$order = new stdClass();
$order->line_items = [
[
'id' => 1,
'name' => 'Item 1',
'price' => 100,
'quantity' => 2,
],
[
'id' => 2,
'name' => 'Item 2',
'price' => 200,
'quantity' => 1,
],
];
// Build the pipeline
$pipeline = new OrderPipeline();
// Add stages to the pipeline
$pipeline->addStage(function ($order) {
// Stage 1: Split line items into quantities of 1
$order->line_items = array_reduce(
$order->line_items,
function ($carry, $item) {
for ($i = 0; $i < $item['quantity']; $i++) {
$carry[] = [
'id' => $item['id'],
'name' => $item['name'],
'price' => $item['price'],
'quantity' => 1,
];
}
return $carry;
},
[]
);
return $order;
});
$pipeline->addStage(function ($order) {
// Stage 2: Calculate order total
$order->total = array_reduce(
$order->line_items,
function ($carry, $item) {
return $carry + ($item['price'] * $item['quantity']);
},
0
);
return $order;
});
// Process the order through the pipeline
$processedOrder = $pipeline->process($order);
// Output of the processed order
// {
// "line_items": [
// {
// "id": 1,
// "name": "Item 1",
// "price": 100,
// "quantity": 1
// },
// {
// "id": 1,
// "name": "Item 1",
// "price": 100,
// "quantity": 1
// },
// {
// "id": 2,
// "name": "Item 2",
// "price": 200,
// "quantity": 1
// }
// ],
// "total": 400
// }
In this example, we create an OrderPipeline
class that allows us to add stages of processing for orders.
Each stage is a closure function that performs a specific task on the order object and passes it to the next stage.
Finally, we can process the order by calling the process()
method on the pipeline object.
This is a very simple example of how pipelines can be used to process orders. You could make this example more complex by moving these stages into separate classes and extending a base class that contains the pipeline logic. This would allow us to not only reuse the stages across multiple pipelines but also make it easier to test each stage individually.
For example, we could create a SplitLineItems
class that extends a BaseStage
class that contains the pipeline
logic. The SplitLineItems
class would contain the logic for splitting the line items. Then we
can create a specific test to validate that the SplitLineItems
class works as expected.
Some cool packages
Laravel Workflow
Laravel Workflow is a package that provides a simple and elegant way to manage 'workflows' in your Laravel application. You can check it out here: https://github.com/laravel-workflow/laravel-workflow
League\Pipeline
The League\Pipeline package provides a simple and elegant way to manage 'pipelines' in your PHP application. You can check it out here: https://github.com/thephpleague/pipeline