Pipelines

The pattern behind middleware — pass an object through a series of stages, each free to transform it.

The Pipeline is the mechanism that powers HTTP middleware, and you can use it directly for any "pass this through a sequence of steps" problem. Each stage receives the object and a $next closure; it does its work, then calls $next($object) to hand off to the following stage — exactly like middleware.

Why it beats a pile of if-statements

Filtering a search query, sanitising input, building a report through composable steps — a pipeline lets each concern live in its own tiny, testable class, and you compose them by listing them in order.

use Illuminate\Support\Facades\Pipeline;

$result = Pipeline::send($query)
    ->through([FilterByStatus::class, SortByDate::class])
    ->thenReturn();

The stage shape

Each stage is a class with a handle($passable, Closure $next) method. Return $next($passable) to continue, or short-circuit by returning early.

Example

Example · php
<?php

// --- A single, focused pipe ---
namespace App\QueryFilters;

use Closure;
use Illuminate\Database\Eloquent\Builder;

class FilterByStatus
{
    public function handle(Builder $query, Closure $next): Builder
    {
        if ($status = request('status')) {
            $query->where('status', $status);
        }
        return $next($query);   // hand off to the next pipe
    }
}

// --- Composing them ---
namespace App\Http\Controllers;

use App\Models\Order;
use App\QueryFilters\{FilterByStatus, FilterByDateRange, SortResults};
use Illuminate\Support\Facades\Pipeline;

class OrderSearchController
{
    public function __invoke()
    {
        $orders = Pipeline::send(Order::query())
            ->through([
                FilterByStatus::class,
                FilterByDateRange::class,
                SortResults::class,
            ])
            ->thenReturn()      // returns the transformed Builder
            ->paginate(20);

        return $orders;
    }
}

When to use it

  • A content moderation system passes a user's post through a Pipeline of stages — spam check, profanity filter, link validation — before saving it, with each stage being a separate testable class.
  • An image upload handler runs an uploaded file through a Pipeline of processors (resize, watermark, optimize, store) keeping each transformation independent and reorderable.
  • A data import service sends each CSV row through a Pipeline that trims whitespace, validates fields, maps to a DTO, and upserts the record, making it easy to add or remove stages.

More examples

Basic Pipeline with send/through/thenReturn

Pipeline::send() sets the payload, through() defines the ordered stages, and thenReturn() runs the pipeline and returns the final transformed value.

Example · php
use Illuminate\Pipeline\Pipeline;

$post = app(Pipeline::class)
    ->send($request->all())
    ->through([
        \App\Pipes\TrimInput::class,
        \App\Pipes\FilterProfanity::class,
        \App\Pipes\CheckSpamScore::class,
    ])
    ->thenReturn();

// $post is the value returned by the last pipe

A pipe class with handle()

Each pipe class receives the payload and a $next closure; calling $next() continues the pipeline, while returning early halts it — just like HTTP middleware.

Example · php
namespace App\Pipes;

use Closure;

class FilterProfanity
{
    public function handle(array $payload, Closure $next): array
    {
        $blocked = config('moderation.blocked_words', []);

        foreach ($blocked as $word) {
            $payload['body'] = str_ireplace($word, '***', $payload['body']);
        }

        return $next($payload); // pass to the next pipe
    }
}

Pipeline with a then() callback

then() receives the transformed value after all pipes have run and performs a final action — here an upsert — returning its own result.

Example · php
use Illuminate\Pipeline\Pipeline;

$result = app(Pipeline::class)
    ->send(new ImportRow($csvRow))
    ->through([
        TrimWhitespace::class,
        ValidateRequiredFields::class,
        MapToProduct::class,
    ])
    ->then(function (ImportRow $row) {
        Product::upsert($row->data, ['sku'], ['name', 'price', 'stock']);
        return 'imported';
    });

Discussion

  • Be the first to comment on this lesson.