Skip to content

Pipelines

Pipelines provide a clean way to process complex, multi-step operations. Each step is a separate class with a single handle() method, and data flows through the chain via a DTO.

How Pipelines Work

DTO → Pipe 1 → Pipe 2 → Pipe 3 → Result

Each pipe:

  1. Receives the DTO and a $next closure
  2. Performs its operation (may modify the DTO)
  3. Calls $next($data) to pass control to the next pipe
php
Pipeline::send($dto)
    ->through([
        StepOne::class,
        StepTwo::class,
        StepThree::class,
    ])
    ->thenReturn();

Pipeline Structure

app/Pipelines/
├── Patient/
│   └── CreateNewPatient/
│       ├── UploadPhotoPipeline.php
│       ├── CreateNewPatientPipeline.php
│       └── GeneratePatientCodePipeline.php
├── Auth/
│   ├── ResetCode/
│   │   ├── ValidateUserExists.php
│   │   ├── InvalidateExistingCodes.php
│   │   ├── GenerateResetCode.php
│   │   ├── StoreResetCode.php
│   │   └── SendResetCodeEmail.php
│   ├── ResetPassword/
│   │   ├── FetchUser.php
│   │   ├── FindValidCode.php
│   │   ├── UpdatePassword.php
│   │   └── MarkCodeAsUsed.php
│   └── SwitchBranch/
│       └── VerifyBranchAccess.php
├── UploadImage/
│   └── ...
└── User/
    └── ...

Patient Creation Pipeline

Creates a patient in 3 steps:

DTO

php
class AddNewPatientData
{
    public Patient $patient; // Set by CreateNewPatientPipeline

    public function __construct(
        public array $params,
        public Branch $branch,
    ) {}
}

Step 1: UploadPhotoPipeline

Handles photo upload if present:

php
class UploadPhotoPipeline
{
    public function __construct(private UploadService $uploadService) {}

    public function handle(AddNewPatientData $data, Closure $next)
    {
        if (isset($data->params['photo']) && $data->params['photo'] instanceof UploadedFile) {
            $result = $this->uploadService->uploadImage(
                $data->params['photo'],
                'patients',
                ['50x50', '150x150']
            );
            $data->params['photo_path'] = $result['path'];
            unset($data->params['photo']);
        }

        return $next($data);
    }
}

Step 2: CreateNewPatientPipeline

Creates the patient record:

php
class CreateNewPatientPipeline
{
    public function __construct(private PatientRepository $repository) {}

    public function handle(AddNewPatientData $data, Closure $next)
    {
        $data->patient = $this->repository->create($data->params);

        return $next($data);
    }
}

Step 3: GeneratePatientCodePipeline

Generates the patient PIN from branch code + patient ID:

php
class GeneratePatientCodePipeline
{
    public function handle(AddNewPatientData $data, Closure $next)
    {
        $pin = $data->branch->code . '-' . str_pad($data->patient->id, 6, '0', STR_PAD_LEFT);
        $data->patient->update(['pin' => $pin]);

        return $next($data);
    }
}

PIN format: MAIN-000001, DWTN-000042, etc.

Usage in PatientService

php
public function create(array $params, Branch $branch): Patient
{
    $data = new AddNewPatientData($params, $branch);

    Pipeline::send($data)
        ->through([
            UploadPhotoPipeline::class,
            CreateNewPatientPipeline::class,
            GeneratePatientCodePipeline::class,
        ])
        ->thenReturn();

    return $data->patient;
}

Password Reset Pipeline

The reset code flow has 5 steps:

php
Pipeline::send(new ResetCodeData($email))
    ->through([
        ValidateUserExists::class,      // Find user by email, throw if not found
        InvalidateExistingCodes::class,  // Invalidate any previous unused codes
        GenerateResetCode::class,        // Generate 6-digit numeric code
        StoreResetCode::class,           // Save code with 15-min expiry
        SendResetCodeEmail::class,       // Email the code to the user
    ])
    ->thenReturn();

Each step follows the same pattern:

php
class ValidateUserExists
{
    public function __construct(private UserRepositoryInterface $repository) {}

    public function handle(ResetCodeData $data, Closure $next)
    {
        $user = $this->repository->findByEmail($data->email);

        if (!$user) {
            throw ValidationException::withMessages([
                'email' => ['No user found with this email address.'],
            ]);
        }

        $data->user = $user;

        return $next($data);
    }
}

When to Use Pipelines

ScenarioUse Pipeline?
Multi-step creation with side effects (uploads, code generation)Yes
Multi-step auth flows (reset password, verify email)Yes
Simple CRUD operationsNo — use Actions
Single-step operationsNo — use direct service method
Operations needing database transactionsNo — use DB::transaction() in service

Creating a New Pipeline

  1. Create a DTO in app/Dtos/ with public properties for each step to populate
  2. Create pipe classes in app/Pipelines/{Module}/
  3. Each pipe class needs a handle($data, Closure $next) method
  4. Call Pipeline::send($dto)->through([...])->thenReturn() in your service

Pipe Template

php
namespace App\Pipelines\Module;

use Closure;

class ExampleStep
{
    public function handle(ExampleData $data, Closure $next)
    {
        // Do work, modify $data as needed

        return $next($data);
    }
}

CPR - Clinical Patient Records