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

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

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

  [ home ](https://www.msaied.com)    [ articles ](https://www.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%2Fwww.msaied.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%2Fwww.msaied.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://www.msaied.com/articles) 

 [ ![Filament v4 Schema-Based Forms, Infolists, and the Unified Schema API](https://cdn.msaied.com/297/6eb3a7aaf7148fd21116eea870bd004e.png) filament laravel filament-v4 

### Filament v4 Schema-Based Forms, Infolists, and the Unified Schema API

Filament v4 replaces scattered form and infolist definitions with a single Schema API. Learn how unified schem...

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

 26 Jun 2026     3 min read  

  Read    

 ](https://www.msaied.com/articles/filament-v4-schema-based-forms-infolists-and-the-unified-schema-api-1) [ ![Read/Write Splitting, Connection Pooling, and Sticky Reads in Laravel](https://cdn.msaied.com/295/d977bd189583149245c03d6d763d9db5.png) laravel database performance 

### Read/Write Splitting, Connection Pooling, and Sticky Reads in Laravel

Learn how Laravel's database layer handles read/write splitting, when sticky reads matter, and how to layer Pg...

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

 26 Jun 2026     4 min read  

  Read    

 ](https://www.msaied.com/articles/readwrite-splitting-connection-pooling-and-sticky-reads-in-laravel-2) [ ![Laravel Service Container: Contextual Binding, Tagging, and Method Injection](https://cdn.msaied.com/294/e5b9d047bd33c3f8b80069ef6a178884.png) laravel service-container dependency-injection 

### Laravel Service Container: Contextual Binding, Tagging, and Method Injection

Go beyond basic binding. Learn how contextual binding resolves different implementations per consumer, how tag...

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

 26 Jun 2026     3 min read  

  Read    

 ](https://www.msaied.com/articles/laravel-service-container-contextual-binding-tagging-and-method-injection-1) 

   [  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png)   Mohamed Said Laravel Backend Engineer  ](https://www.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://www.msaied.com)
- [Projects](https://www.msaied.com/projects)
- [Articles](https://www.msaied.com/articles)
- [Certificates](https://www.msaied.com/certificates)
- [Contact](https://www.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)
