Laravel Custom Pipelines Beyond Middleware | Mohamed Said        [  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png)   Mohamed Said Laravel Backend Engineer  ](https://msaied.com) [ Home ](https://msaied.com) [ Projects ](https://msaied.com/projects) [ Articles  ](https://msaied.com/articles) [ Certificates ](https://msaied.com/certificates) [ Contact ](https://msaied.com#contact-section) 

       [  ](https://github.com/EG-Mohamed)       

 [ Home ](https://msaied.com) [ Projects ](https://msaied.com/projects) [ Articles ](https://msaied.com/articles) [ Certificates ](https://msaied.com/certificates) [ Contact ](https://msaied.com#contact-section) 

  [ home ](https://msaied.com)    [ articles ](https://msaied.com/articles)    Laravel Pipeline Pattern: Building Custom Pipelines Beyond Middleware        On this page       1. [  The Pipeline Pattern in Laravel: Beyond HTTP Middleware ](#the-pipeline-pattern-in-laravel-beyond-http-middleware)
2. [  The Core API ](#the-core-api)
3. [  Typed Payloads: Stop Passing Arrays ](#typed-payloads-stop-passing-arrays)
4. [  Writing a Pipe ](#writing-a-pipe)
5. [  Short-Circuiting When You Need It ](#short-circuiting-when-you-need-it)
6. [  Registering Pipelines as Named Services ](#registering-pipelines-as-named-services)
7. [  Testing Pipelines with Pest ](#testing-pipelines-with-pest)
8. [  Key Takeaways ](#key-takeaways)

  ![Laravel Pipeline Pattern: Building Custom Pipelines Beyond Middleware](https://cdn.msaied.com/296/afbac95c7f4aac1cee83eb2c87541369.png)

  #laravel   #pipeline   #clean-architecture   #design-patterns  

 Laravel Pipeline Pattern: Building Custom Pipelines Beyond Middleware 
=======================================================================

     26 Jun 2026      4 min read    ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said  

       Table of contents

1. [  01   The Pipeline Pattern in Laravel: Beyond HTTP Middleware  ](#the-pipeline-pattern-in-laravel-beyond-http-middleware)
2. [  02   The Core API  ](#the-core-api)
3. [  03   Typed Payloads: Stop Passing Arrays  ](#typed-payloads-stop-passing-arrays)
4. [  04   Writing a Pipe  ](#writing-a-pipe)
5. [  05   Short-Circuiting When You Need It  ](#short-circuiting-when-you-need-it)
6. [  06   Registering Pipelines as Named Services  ](#registering-pipelines-as-named-services)
7. [  07   Testing Pipelines with Pest  ](#testing-pipelines-with-pest)
8. [  08   Key Takeaways  ](#key-takeaways)

 The Pipeline Pattern in Laravel: Beyond HTTP Middleware
-------------------------------------------------------

Most Laravel developers know `Pipeline` only as the engine behind HTTP middleware. But `Illuminate\Pipeline\Pipeline` is a general-purpose, composable tool you can use anywhere in your domain — order processing, import validation, document transformation, multi-stage approval flows.

This article shows how to design typed, testable pipelines that carry real domain meaning.

---

### The Core API

```php
use Illuminate\Pipeline\Pipeline;

$result = app(Pipeline::class)
    ->send($payload)
    ->through([
        StageOne::class,
        StageTwo::class,
    ])
    ->thenReturn();

```

`thenReturn()` returns the final `$payload` after all pipes run. Each pipe receives `($payload, Closure $next)` and must call `$next($payload)` to continue — identical to middleware.

---

### Typed Payloads: Stop Passing Arrays

The single biggest improvement you can make is replacing anonymous arrays with a typed DTO.

```php
final class OrderImportContext
{
    public function __construct(
        public readonly array  $rawRow,
        public ?Product        $product   = null,
        public ?Customer       $customer  = null,
        public array           $errors    = [],
    ) {}

    public function withError(string $message): self
    {
        $clone = clone $this;
        $clone->errors[] = $message;
        return $clone;
    }
}

```

Each pipe receives a fully typed object. IDE completion works. Pipes are self-documenting.

---

### Writing a Pipe

A pipe is any class with a `handle` method (or `__invoke` if you prefer).

```php
final class ResolveProduct
{
    public function handle(OrderImportContext $ctx, Closure $next): OrderImportContext
    {
        $product = Product::query()
            ->where('sku', $ctx->rawRow['sku'] ?? '')
            ->first();

        if ($product === null) {
            return $next($ctx->withError('Unknown SKU: ' . ($ctx->rawRow['sku'] ?? 'n/a')));
        }

        $ctx = clone $ctx;
        $ctx->product = $product;

        return $next($ctx);
    }
}

```

Notice: even on error we call `$next()`. This lets downstream pipes accumulate *all* errors in one pass rather than short-circuiting on the first failure.

---

### Short-Circuiting When You Need It

Sometimes you *do* want to stop early — for example, a hard authentication failure.

```php
final class AssertActiveCustomer
{
    public function handle(OrderImportContext $ctx, Closure $next): OrderImportContext
    {
        if ($ctx->customer?->isSuspended()) {
            // Return without calling $next — pipeline stops here.
            return $ctx->withError('Customer account is suspended.');
        }

        return $next($ctx);
    }
}

```

The pipeline returns whatever value the pipe returns, so the caller always gets an `OrderImportContext`.

---

### Registering Pipelines as Named Services

Bind your pipeline configuration in a service provider so it's injectable and swappable:

```php
$this->app->bind(OrderImportPipeline::class, function ($app) {
    return new class($app) {
        public function __construct(private \Illuminate\Contracts\Container\Container $app) {}

        public function run(OrderImportContext $ctx): OrderImportContext
        {
            return (new Pipeline($this->app))
                ->send($ctx)
                ->through([
                    ResolveProduct::class,
                    ResolveCustomer::class,
                    AssertActiveCustomer::class,
                    ValidateQuantity::class,
                    PersistOrder::class,
                ])
                ->thenReturn();
        }
    };
});

```

Inject `OrderImportPipeline` wherever you need it. Swap the pipe list in tests.

---

### Testing Pipelines with Pest

```php
it('accumulates errors for unknown sku and suspended customer', function () {
    $ctx = new OrderImportContext(
        rawRow: ['sku' => 'GHOST-99', 'qty' => 1],
        customer: Customer::factory()->suspended()->make(),
    );

    $result = app(OrderImportPipeline::class)->run($ctx);

    expect($result->errors)
        ->toContain('Unknown SKU: GHOST-99')
        ->toContain('Customer account is suspended.');
});

```

Because each pipe is a plain class, you can also unit-test pipes in isolation:

```php
it('short-circuits on suspended customer', function () {
    $ctx = new OrderImportContext(
        rawRow: [],
        customer: Customer::factory()->suspended()->make(),
    );

    $pipe   = new AssertActiveCustomer();
    $result = $pipe->handle($ctx, fn ($c) => $c);

    expect($result->errors)->toContain('Customer account is suspended.');
});

```

---

### Key Takeaways

- **Use typed DTOs** as the pipeline payload — never raw arrays.
- **Accumulate vs. short-circuit** is a deliberate design choice per pipeline.
- **Bind named pipelines** in service providers for DI and testability.
- **Each pipe is a plain class** — unit-testable without bootstrapping the full pipeline.
- The `Pipeline` facade is available globally, but prefer `app(Pipeline::class)` for container-resolved pipes.
- Pipelines compose naturally with Laravel's queue system: serialize the DTO, dispatch a job, run the pipeline inside the job.

 Found this useful?

          [  ](https://twitter.com/intent/tweet?url=https%3A%2F%2Fmsaied.com%2Farticles%2Flaravel-pipeline-pattern-building-custom-pipelines-beyond-middleware-1&text=Laravel+Pipeline+Pattern%3A+Building+Custom+Pipelines+Beyond+Middleware) [  ](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fmsaied.com%2Farticles%2Flaravel-pipeline-pattern-building-custom-pipelines-beyond-middleware-1) 

 Frequently Asked Questions 
----------------------------

  3 questions  

     Q01  Can I use closures instead of classes as pipes?        Yes. The Pipeline accepts any callable, including closures. However, named classes are strongly preferred in production code because they are individually testable, injectable via the container, and visible in stack traces. 

      Q02  How does the Laravel Pipeline differ from a simple foreach loop over handlers?        The Pipeline uses a nested closure chain (similar to a Russian-doll middleware stack), which means each pipe controls whether and when to call the next stage. A foreach loop cannot short-circuit or wrap the downstream execution — for example, a pipe cannot wrap $next() in a database transaction and roll back on failure. 

      Q03  Is there a performance cost to using Pipeline for high-throughput code paths?        The overhead is negligible for typical domain workflows. The closure chain adds a small amount of stack depth, but this is rarely measurable against real I/O. For extremely tight loops (e.g., processing millions of rows in memory), a direct method call chain will be marginally faster, but the Pipeline is appropriate for the vast majority of Laravel use cases. 

  Continue reading

 More Articles 
---------------

 [ View all    ](https://msaied.com/articles) 

 [ ![Cursor Pagination and Lazy Collections at Scale in Laravel](https://cdn.msaied.com/536/3aab48ef4a4eaa26a3267637dc2ec8c7.png) laravel eloquent performance 

### Cursor Pagination and Lazy Collections at Scale in Laravel

Offset pagination breaks under large datasets. Learn how Laravel's cursor pagination and lazy collections let...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 11 Aug 2026     3 min read  

  Read    

 ](https://msaied.com/articles/cursor-pagination-and-lazy-collections-at-scale-in-laravel) [ ![Livewire v3.8.4 Released: Octane Memory Leak Fix and Fetch Redirect Handling](https://cdn.msaied.com/534/fdb2d91db2cb26fba0788d205b663031.png) livewire laravel octane 

### Livewire v3.8.4 Released: Octane Memory Leak Fix and Fetch Redirect Handling

Livewire v3.8.4 ships two important backports: a fix for a computed property listener memory leak under Larave...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 10 Aug 2026     3 min read  

  Read    

 ](https://msaied.com/articles/livewire-v384-released-octane-memory-leak-fix-and-fetch-redirect-handling) [ ![Laravel artisan dev: Run Server, Queue, Logs, and Vite in One Command](https://cdn.msaied.com/533/88ab98460b08aed42d6688eaa02a9620.png) Laravel Artisan Laravel 13 

### Laravel artisan dev: Run Server, Queue, Logs, and Vite in One Command

Laravel 13.16 introduced a first-party `php artisan dev` command that replaces the old Composer script, runnin...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 10 Aug 2026     3 min read  

  Read    

 ](https://msaied.com/articles/laravel-artisan-dev-run-server-queue-logs-and-vite-in-one-command) 

   [  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png)   Mohamed Said Laravel Backend Engineer  ](https://msaied.com)Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

Explore

- [Home](https://msaied.com)
- [Projects](https://msaied.com/projects)
- [Articles](https://msaied.com/articles)
- [Certificates](https://msaied.com/certificates)
- [Contact](https://msaied.com#contact-section)

Connect

- [   hello@msaied.com ](mailto:hello@msaied.com)
- [   +20 109 461 9204 ](tel:+201094619204)

© 2026 Mohamed Said. All rights reserved.

 [  ](https://github.com/EG-Mohamed) [  ](https://www.linkedin.com/in/msaiedm/) [  ](https://wa.me/201094619204) [  ](mailto:hello@msaied.com) [  ](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)
