Unlock the full potential of Laravel's Pipeline pattern to create elegant, maintainable code that goes far beyond traditional middleware usage.
Table Of Contents
- Understanding the Pipeline Pattern
- Beyond HTTP Middleware
- Creating Custom Pipeline Stages
- Advanced Pipeline Techniques
- Data Transformation Pipelines
- Image Processing Pipelines
- Testing Pipeline Components
- Error Handling in Pipelines
- Integration with Laravel Events
- Performance Considerations
- Real-World Applications
Understanding the Pipeline Pattern
The Pipeline pattern in Laravel provides a powerful way to pass data through a series of processing stages, where each stage can modify the data before passing it to the next stage. While most developers know pipelines through Laravel's middleware system, the pattern has much broader applications in building maintainable Laravel applications.
Think of a pipeline as an assembly line where each worker performs a specific task on a product before passing it down the line. In Laravel, this translates to processing objects through a series of classes, each handling a specific concern.
Beyond HTTP Middleware
While middleware is the most common use of pipelines in Laravel, the pattern extends far beyond HTTP request processing. You can use pipelines for data transformation, validation chains, image processing, email filtering, and complex business logic workflows.
use Illuminate\Pipeline\Pipeline;
class OrderProcessor
{
protected array $pipes = [
ValidateOrderData::class,
CheckInventory::class,
CalculateTaxes::class,
ApplyDiscounts::class,
ProcessPayment::class,
SendConfirmationEmail::class,
];
public function process(Order $order): Order
{
return app(Pipeline::class)
->send($order)
->through($this->pipes)
->thenReturn();
}
}
This approach creates clean, testable code where each step has a single responsibility, making it easy to add, remove, or reorder processing steps. This aligns perfectly with Laravel's service-oriented architecture.
Creating Custom Pipeline Stages
Each pipeline stage should implement a consistent interface. The standard Laravel convention uses a handle
method that receives the passable object and a closure to pass control to the next stage.
class ValidateOrderData
{
public function handle(Order $order, Closure $next): Order
{
if (!$order->customer_id) {
throw new InvalidOrderException('Customer ID is required');
}
if ($order->items->isEmpty()) {
throw new InvalidOrderException('Order must contain at least one item');
}
return $next($order);
}
}
class CalculateTaxes
{
public function handle(Order $order, Closure $next): Order
{
$taxRate = $this->getTaxRateForRegion($order->shipping_address->region);
$order->tax_amount = $order->subtotal * $taxRate;
$order->total = $order->subtotal + $order->tax_amount;
return $next($order);
}
protected function getTaxRateForRegion(string $region): float
{
// Tax calculation logic
return 0.08; // 8% tax rate
}
}
Each stage can modify the object, add metadata, or even halt the pipeline by not calling the $next
closure. This provides fine-grained control over the processing flow.
Advanced Pipeline Techniques
Conditional Pipeline Stages
You can conditionally include pipeline stages based on the data being processed or external factors:
class DynamicOrderProcessor
{
public function process(Order $order): Order
{
$pipes = [
ValidateOrderData::class,
CheckInventory::class,
];
// Add tax calculation only for taxable regions
if ($this->isTaxableRegion($order->shipping_address->region)) {
$pipes[] = CalculateTaxes::class;
}
// Add premium processing for VIP customers
if ($order->customer->isVip()) {
$pipes[] = PriorityProcessing::class;
}
$pipes = array_merge($pipes, [
ProcessPayment::class,
SendConfirmationEmail::class,
]);
return app(Pipeline::class)
->send($order)
->through($pipes)
->thenReturn();
}
}
Pipeline Branching
Create complex workflows with branching logic:
class SmartOrderProcessor
{
public function process(Order $order): Order
{
// Common processing
$order = app(Pipeline::class)
->send($order)
->through([
ValidateOrderData::class,
CheckInventory::class,
])
->thenReturn();
// Branch based on order type
if ($order->isDigitalOrder()) {
return $this->processDigitalOrder($order);
} else {
return $this->processPhysicalOrder($order);
}
}
protected function processDigitalOrder(Order $order): Order
{
return app(Pipeline::class)
->send($order)
->through([
ProcessPayment::class,
DeliverDigitalGoods::class,
SendDigitalReceipt::class,
])
->thenReturn();
}
}
Data Transformation Pipelines
Pipelines excel at data transformation tasks, especially when building APIs with Laravel:
class DataTransformationPipeline
{
protected array $transformers = [
NormalizeFieldNames::class,
ValidateDataTypes::class,
SanitizeInput::class,
EnrichWithMetadata::class,
FormatOutput::class,
];
public function transform(array $data): array
{
return app(Pipeline::class)
->send($data)
->through($this->transformers)
->then(function ($data) {
return $data;
});
}
}
class NormalizeFieldNames
{
public function handle(array $data, Closure $next): array
{
$normalized = [];
foreach ($data as $key => $value) {
$normalizedKey = Str::snake($key);
$normalized[$normalizedKey] = $value;
}
return $next($normalized);
}
}
This approach is particularly useful when integrating external APIs or processing user uploads, where data consistency is crucial.
Image Processing Pipelines
For applications handling file uploads, pipelines provide an elegant way to process images:
class ImageProcessingPipeline
{
protected array $processors = [
ValidateImageFile::class,
ResizeImage::class,
OptimizeImage::class,
AddWatermark::class,
GenerateThumbnails::class,
StoreImage::class,
];
public function process(UploadedFile $file): ProcessedImage
{
$imageData = new ImageData($file);
return app(Pipeline::class)
->send($imageData)
->through($this->processors)
->then(function ($imageData) {
return $imageData->getProcessedImage();
});
}
}
class ResizeImage
{
public function handle(ImageData $imageData, Closure $next): ImageData
{
$image = Image::make($imageData->getFile());
if ($image->width() > 1920) {
$image->resize(1920, null, function ($constraint) {
$constraint->aspectRatio();
});
}
$imageData->setProcessedImage($image);
return $next($imageData);
}
}
Testing Pipeline Components
One of the biggest advantages of the pipeline pattern is testability. Each stage can be tested in isolation:
class ValidateOrderDataTest extends TestCase
{
public function test_validates_customer_id_presence(): void
{
$order = new Order(['customer_id' => null]);
$validator = new ValidateOrderData();
$this->expectException(InvalidOrderException::class);
$this->expectExceptionMessage('Customer ID is required');
$validator->handle($order, function ($order) {
return $order;
});
}
public function test_passes_valid_order_to_next_stage(): void
{
$order = new Order([
'customer_id' => 1,
'items' => collect([new OrderItem()])
]);
$validator = new ValidateOrderData();
$nextCalled = false;
$result = $validator->handle($order, function ($order) use (&$nextCalled) {
$nextCalled = true;
return $order;
});
$this->assertTrue($nextCalled);
$this->assertEquals($order, $result);
}
}
You can also test entire pipelines using Laravel's testing features:
class OrderProcessorTest extends TestCase
{
public function test_processes_complete_order(): void
{
$order = Order::factory()->create();
$processor = new OrderProcessor();
$processedOrder = $processor->process($order);
$this->assertNotNull($processedOrder->tax_amount);
$this->assertNotNull($processedOrder->total);
$this->assertTrue($processedOrder->payment_processed);
}
}
Error Handling in Pipelines
Implement robust error handling to manage failures gracefully:
class RobustOrderProcessor
{
public function process(Order $order): Order
{
try {
return app(Pipeline::class)
->send($order)
->through($this->pipes)
->thenReturn();
} catch (PaymentException $e) {
$this->handlePaymentFailure($order, $e);
throw $e;
} catch (InventoryException $e) {
$this->handleInventoryFailure($order, $e);
throw $e;
} catch (Exception $e) {
Log::error('Order processing failed', [
'order_id' => $order->id,
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
throw new OrderProcessingException('Failed to process order', 0, $e);
}
}
protected function handlePaymentFailure(Order $order, PaymentException $e): void
{
$order->update(['status' => 'payment_failed']);
// Release reserved inventory
event(new PaymentFailed($order));
}
}
Integration with Laravel Events
Combine pipelines with Laravel's event system for even more flexibility:
class EventAwarePipelineStage
{
public function handle(Order $order, Closure $next): Order
{
event(new OrderProcessingStarted($order));
try {
$result = $next($order);
event(new OrderProcessingCompleted($order));
return $result;
} catch (Exception $e) {
event(new OrderProcessingFailed($order, $e));
throw $e;
}
}
}
Performance Considerations
While pipelines add flexibility, be mindful of performance implications. Each stage adds method call overhead, and complex pipelines can impact performance. Consider caching processed results when appropriate:
class CachedImageProcessor
{
public function process(UploadedFile $file): ProcessedImage
{
$cacheKey = $this->getCacheKey($file);
return Cache::remember($cacheKey, 3600, function () use ($file) {
return app(Pipeline::class)
->send(new ImageData($file))
->through($this->processors)
->then(fn($data) => $data->getProcessedImage());
});
}
}
Real-World Applications
The pipeline pattern shines in various scenarios:
- Content Management: Processing articles through editing, approval, and publishing stages
- E-commerce: Order processing, payment handling, and fulfillment workflows
- Data Import: Validating, transforming, and storing imported data
- API Integration: Processing responses from external services
- File Processing: Document conversion, image manipulation, and file validation
When building SaaS applications with Laravel, pipelines help create maintainable business logic that can evolve with your requirements.
The pipeline pattern transforms complex, monolithic operations into composable, testable stages. This leads to more maintainable code and better separation of concerns, making your Laravel applications more robust and easier to extend.
Add Comment
No comments yet. Be the first to comment!