← Back to Blog
|7 min read|Vipula Saman Anandapiya

Laravel Pipelines: Installation, Setup, and Step-by-Step Usage

Laravel Pipelines: Installation, Setup, and Step-by-Step Usage

Laravel Pipelines: Installation, Setup, and Step-by-Step Usage

Laravel’s Pipeline is a clean way to pass a value (a request, a DTO, a query builder, an array, etc.) through a series of “pipes” (small classes or callables). Each pipe can modify the value, validate it, stop the flow, or perform side effects—then pass control to the next pipe.

This pattern is great for:

  • Breaking large request/command handling into small, testable steps
  • Building reusable processing flows (filters, transformations, validations)
  • Keeping controllers/services thin and readable

What You’ll Build

We’ll implement a user registration pipeline that:

  1. Normalizes input (e.g., trims and lowercases email)
  2. Validates data
  3. Creates the user
  4. Optionally sends a welcome email

1) Installation / Requirements

Good news: Laravel Pipeline is included with Laravel via the illuminate/pipeline component. If you’re using a standard Laravel installation, you already have it.

Confirm you have it

In a typical Laravel app, you can use it immediately:

use Illuminate\Pipeline\Pipeline;

If you are in a non-Laravel context (or a very custom setup), you can install it via Composer:

composer require illuminate/pipeline

2) Understanding How a Pipe Works

A pipe is usually a class with a handle method:

public function handle($passable, \Closure $next)
{
    // Do something with $passable

    return $next($passable);
}
  • $passable is the thing moving through the pipeline (array, DTO, request, builder, etc.)
  • $next passes control to the next pipe
  • If you don’t call $next, the pipeline stops early

3) Create a Folder for Pipes

A common convention is:

app/Pipelines/Registration/

Create it manually, or just create files and let your editor handle the directories.


4) Step-by-Step: Build the Registration Pipeline

Step 4.1: Define the data you’ll pass

For simplicity, we’ll pass an array through the pipeline:

$data = [
    'name' => '  Jane Doe  ',
    'email' => '  [email protected] ',
    'password' => 'secret1234',
];

In real projects, you might prefer a DTO, but arrays work well for learning and quick setups.

Step 4.2: Create the first pipe (Normalize Input)

Create app/Pipelines/Registration/NormalizeInput.php:

<?php

namespace App\Pipelines\Registration;

use Closure;

class NormalizeInput
{
    public function handle(array $data, Closure $next)
    {
        $data['name'] = trim($data['name'] ?? '');
        $data['email'] = strtolower(trim($data['email'] ?? ''));

        return $next($data);
    }
}

Step 4.3: Create the second pipe (Validate Data)

Create app/Pipelines/Registration/ValidateRegistration.php:

<?php

namespace App\Pipelines\Registration;

use Closure;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;

class ValidateRegistration
{
    public function handle(array $data, Closure $next)
    {
        $validator = Validator::make($data, [
            'name' => ['required', 'string', 'min:2'],
            'email' => ['required', 'email', 'max:255', 'unique:users,email'],
            'password' => ['required', 'string', 'min:8'],
        ]);

        if ($validator->fails()) {
            throw new ValidationException($validator);
        }

        return $next($data);
    }
}

Step 4.4: Create the third pipe (Create the User)

Create app/Pipelines/Registration/CreateUser.php:

<?php

namespace App\Pipelines\Registration;

use App\Models\User;
use Closure;
use Illuminate\Support\Facades\Hash;

class CreateUser
{
    public function handle(array $data, Closure $next)
    {
        $user = User::create([
            'name' => $data['name'],
            'email' => $data['email'],
            'password' => Hash::make($data['password']),
        ]);

        // Add the created user to the passable so downstream pipes can use it
        $data['user'] = $user;

        return $next($data);
    }
}

Step 4.5: Create the fourth pipe (Send Welcome Email)

This is optional, but it shows how pipes can perform side effects.

Create app/Pipelines/Registration/SendWelcomeEmail.php:

<?php

namespace App\Pipelines\Registration;

use App\Mail\WelcomeMail;
use Closure;
use Illuminate\Support\Facades\Mail;

class SendWelcomeEmail
{
    public function handle(array $data, Closure $next)
    {
        if (!empty($data['user'])) {
            Mail::to($data['user']->email)->send(new WelcomeMail($data['user']));
        }

        return $next($data);
    }
}

If you don’t have a mail class yet, you can generate one:

php artisan make:mail WelcomeMail

5) Run the Pipeline (Controller or Service)

Now you’ll wire everything together using Laravel’s Pipeline class.

Example: Use the pipeline in a controller

Create or update a controller method like:

<?php

namespace App\Http\Controllers;

use App\Pipelines\Registration\CreateUser;
use App\Pipelines\Registration\NormalizeInput;
use App\Pipelines\Registration\SendWelcomeEmail;
use App\Pipelines\Registration\ValidateRegistration;
use Illuminate\Http\Request;
use Illuminate\Pipeline\Pipeline;

class RegisterController extends Controller
{
    public function store(Request $request)
    {
        $data = $request->only(['name', 'email', 'password']);

        $result = app(Pipeline::class)
            ->send($data)
            ->through([
                NormalizeInput::class,
                ValidateRegistration::class,
                CreateUser::class,
                SendWelcomeEmail::class,
            ])
            ->thenReturn();

        // $result now includes the created user
        return response()->json([
            'message' => 'Registration successful',
            'user' => $result['user'],
        ], 201);
    }
}

What thenReturn() does: it returns the final value after it passes through all pipes.


6) Passing Parameters to Pipes

Laravel pipelines support passing parameters by using a string like PipeClass:param. This is handy for reusable pipes.

Example: a pipe that checks a feature flag or environment setting:

<?php

namespace App\Pipelines\Common;

use Closure;

class AbortIf
{
    public function handle($passable, Closure $next, string $condition)
    {
        if ($condition === 'maintenance') {
            abort(503, 'Service temporarily unavailable');
        }

        return $next($passable);
    }
}

Use it like:

->through([
    'App\\Pipelines\\Common\\AbortIf:maintenance',
    // other pipes...
])

7) Early Exit: Stopping the Pipeline

If a pipe returns something without calling $next, the pipeline stops. For example, you can block registration if an email domain is not allowed:

<?php

namespace App\Pipelines\Registration;

use Closure;

class BlockDisposableEmails
{
    public function handle(array $data, Closure $next)
    {
        $blockedDomains = ['mailinator.com', 'example.net'];

        $domain = substr(strrchr($data['email'] ?? '', '@') ?: '', 1);

        if (in_array($domain, $blockedDomains, true)) {
            return response()->json([
                'message' => 'Disposable email addresses are not allowed.'
            ], 422);
        }

        return $next($data);
    }
}

Tip: In HTTP flows, returning a response early is common. In service-layer pipelines, you might throw a domain exception instead.


8) Pipelines for Query Filtering (Common Real-World Use)

Pipelines shine when applying a set of optional filters to an Eloquent query.

Example: Product search filters

Pass an Eloquent builder through multiple filter pipes:

use Illuminate\Pipeline\Pipeline;
use App\Pipelines\Products\FilterByCategory;
use App\Pipelines\Products\FilterByPriceRange;
use App\Pipelines\Products\SortBy;

$query = app(Pipeline::class)
    ->send(\App\Models\Product::query())
    ->through([
        FilterByCategory::class,
        FilterByPriceRange::class,
        SortBy::class,
    ])
    ->thenReturn();

$products = $query->paginate();

A filter pipe might look like:

<?php

namespace App\Pipelines\Products;

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

class FilterByCategory
{
    public function handle(Builder $query, Closure $next)
    {
        $category = request('category');

        if ($category) {
            $query->where('category_slug', $category);
        }

        return $next($query);
    }
}

9) Testing Pipes (Quick Example)

Because pipes are small and focused, they’re easy to unit test. Here’s a simple example for the normalizer:

public function test_normalize_input_trims_and_lowercases_email()
{
    $pipe = new \App\Pipelines\Registration\NormalizeInput();

    $data = ['name' => '  Jane  ', 'email' => '  [email protected] '];

    $result = $pipe->handle($data, function ($data) {
        return $data;
    });

    $this->assertSame('Jane', $result['name']);
    $this->assertSame('[email protected]', $result['email']);
}

10) Best Practices for Laravel Pipelines

  • Keep pipes small: one responsibility per pipe.
  • Prefer immutable-ish flow: return the modified passable rather than relying on global state.
  • Be consistent: always pass the same type through the pipeline (array vs DTO vs builder).
  • Use exceptions for domain errors in service pipelines; return HTTP responses early in controller pipelines.
  • Name pipes clearly: NormalizeInput, ValidateRegistration, CreateUser, etc.

Conclusion

Laravel Pipelines provide a structured, elegant way to process data through a series of steps. Whether you’re handling registration, processing payments, or applying query filters, pipelines help you keep code modular, testable, and easy to extend.

If you want, I can also provide a version of this example using a dedicated DTO object instead of arrays, plus a clean service class (e.g., RegisterUserAction) to keep controllers even thinner.