Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Pipeline Laravel Package

illuminate/pipeline

Illuminate Pipeline, part of the Laravel framework, sends a value through a sequence of customizable stages, each receiving and modifying the result of the previous one. Ideal for building middleware-style processing chains, filters, and validation workflows in PHP applications.

View on GitHub
Deep Wiki
Context7

Getting Started

In a Laravel app, you already have it. illuminate/pipeline ships with the framework, is aliased as pipeline in the container (Illuminate\Pipeline\Pipeline::class / Illuminate\Contracts\Pipeline\Pipeline::class), and needs zero configuration. In a standalone project, composer require illuminate/pipeline plus illuminate/container (needed to resolve string/class pipes) is the practical minimum.

Where to look first: the source is ~200 lines across two files — Illuminate\Pipeline\Pipeline and Illuminate\Contracts\Pipeline\Pipeline. Read through(), via(), then() and carry() once; almost every behaviour you'll ever debug lives in carry(). Then look at Illuminate\Routing\Pipeline, which is the framework's own subclass and shows the intended extension points.

Smallest useful chain:

use Illuminate\Pipeline\Pipeline;

$payload = app(Pipeline::class)
    ->send($request->all())
    ->through([
        App\Pipes\TrimStrings::class,
        App\Pipes\ValidateRequiredFields::class,
        App\Pipes\CastTypes::class,
    ])
    ->thenReturn();

A pipe is any class with a handle($passable, Closure $next) method:

namespace App\Pipes;

use Closure;

class CastTypes
{
    public function handle(array $payload, Closure $next): array
    {
        $payload['age'] = (int) ($payload['age'] ?? 0);

        return $next($payload);   // never omit this
    }
}

First use case: an API request payload normaliser — trim → validate → cast → persist — so the controller stays a two-liner and each step is independently unit-testable. Second use case: an order/onboarding workflow where the steps are business concepts rather than HTTP concerns.

Implementation Patterns

One pipe per class, one responsibility. Give each pipe a typed handle() with Closure $next and a return type. Keep them stateless; anything they need (repositories, mailers, config) comes through the constructor via the container, which resolves class-string pipes for you.

Wrap each workflow in a named pipeline service. Don't scatter send()->through()->then() across controllers. Build a small class:

class FulfilOrder
{
    public function __construct(private Pipeline $pipeline) {}

    public function run(Order $order): Order
    {
        return $this->pipeline
            ->send($order)
            ->through(config('pipelines.fulfilment'))
            ->thenReturn();
    }
}

Now the pipe list is config-driven — which is how you get feature flagging and environment-specific behaviour for free (filter the array before through()).

Short-circuiting is a first-class pattern. A pipe that returns without calling $next ends the chain and its return value becomes the pipeline result. Use it for guards and idempotency checks; throw ValidationException::withMessages() if you want a 422 rather than a silent null.

then() for a final aggregation/transform, thenReturn() when the payload is already the answer. The destination receives only the payload (no $next), and it must be a Closure — ->then(CreateOrder::class) is a TypeError.

->then(fn (array $data) => Order::create($data))

via('process') when you want a different method name. It applies to the entire chain, not per pipe, so a pipeline mixing handle and process isn't possible — wrap the odd one out.

Parameterised string pipes for configurable steps:

->through(['App\Pipes\Throttle:10,1', ...$extraPipes])
public function handle($passable, Closure $next, $max = 10, $decay = 1) { ... }

This is the cheapest way to make a generic pipe (paginate, retry, throttle) reusable without subclassing. Note explode(',') — no commas inside a parameter.

Reuse pipes as route middleware. A tiny middleware that runs the same pipe list over the Request keeps HTTP concerns and domain concerns sharing one implementation. Use Illuminate\Routing\Pipeline if you want exceptions rendered into responses automatically; the base Illuminate\Pipeline\Pipeline just rethrows.

Reach for Bus::chain() or job middleware for async work. The pipeline is synchronous and not resumable; if step 3 fails, steps 1–2 already happened. Keep pipelines for in-request orchestration and wrap the call site in DB::transaction() when the steps have side effects.

Test at two levels. Unit-test each pipe with a spy $next (fn ($p) => $p) and assert both the transformed payload and whether $next was called (that's how you prove a short-circuit). Then one feature test per named pipeline class with fake collaborators, not per pipe permutation.

Gotchas and Tips

  • A pipe that forgets return $next($passable) yields null downstream. This is the number one bug. Some pipes then "pass", validation silently skips, and you get a mystery 500 three steps later. Return type declarations (: array) catch it at the pipe boundary.
  • through() replaces the pipe array, it doesn't append. Calling it twice while building a chain dynamically silently drops the first list. Collect into one array, then call once.
  • via() is chain-wide, and if the pipe class lacks that method, carry() falls back to invoking the object. So a class with neither handle nor __invoke fails with "not callable" — a confusing error that has nothing to do with the actual mistake.
  • then() accepts Closure only. Conversely, a closure pipe is fine in through() but cannot be serialised — never put closures in a pipeline that gets queued or cached.
  • No built-in exception handling. handleException() rethrows (base class). Wrap the whole then()/thenReturn() call, add an error-boundary pipe that logs and returns a fallback, or use Illuminate\Routing\Pipeline for HTTP. Also note exceptions lose step context: catch inside a try/catch in the pipe and rethrow with the payload snapshot attached.
  • Payloads are mutated in place, not passed by value. Arrays are fine; a shared object or Eloquent model is the same instance across pipes. clone before retries/dry-runs — and remember clone on a model doesn't deep-copy loaded relations.
  • Don't reuse a Pipeline instance across executions. send() and through() mutate internal state and then() does not reset it. Always start a fresh chain (resolve Pipeline fresh from the container) rather than stashing one in a singleton property.
  • Don't rebind the pipeline container alias to a decorated subclass; inject your own subclass type-hint instead. The framework constructs its own instances for routing, and overriding the alias produces surprising, hard-to-trace differences between route middleware and your code.
  • Ordering is a performance decision. Put cheap guards (isAuthenticated, null checks, feature flags) first and DB/external calls last; you want the chain to end before the expensive part, not after it.
  • $next can be called more than once. Inside a pipe you can loop, retry, or fan out by invoking $next per item — powerful for batch pipes (foreach ($items as $item) { $results[] = $next($item); }), but it means downstream pipes must be safe to re-run.
  • Debugging: add a throwaway Log::withContext pipe that logs the payload class/shape at each index, or a single DebugPayload pipe inserted conditionally via app()->isLocal(). dump() in a closure pipe is fine but it's easy to leave in and it's invisible in code review. For timing, put hrtime() marks in a Telemetry pipe pair (start/end) and log the delta in the destination closure.
  • Empty pipe list is legal — the destination still runs with the untouched payload. That's a clean way to A/B a workflow's steps without branching code.
  • Static analysis is blind here. Nothing enforces that pipe 2 returns the type pipe 3 expects. Add @template/PHPDoc on the pipeline wrapper class and keep payloads as arrays or dedicated DTOs rather than mixed; it catches the missing-return class of bug at analysis time instead of in production.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
alesima/laravel-azure-service-bus
tomshaw/shopcart
likyly/laravel
darthsoup/shoppingcart
tdt/core
labyrinthe/connect-php
encore2/laravel-admin2
cboxdk/laravel-console-kit
shapecode/doctrine-dbal-datetimeutc
gilads-otiannoh254/inertia-protocol
dakiquang/laravel-oauth-refresh-lock
woda/laravel-worktrees
dalefranks/ramasite
faghy/laracashbook
roundly-consulting/google-places-for-laravel
roundly-consulting/kubernetes-api-for-laravel
roundly-consulting/plausible-for-laravel
gvp/gvp-laravel-template
metasyncsite/laravel-metasync-client
roundly-consulting/onboarding-for-laravel