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 → ResultEach pipe:
- Receives the DTO and a
$nextclosure - Performs its operation (may modify the DTO)
- 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
| Scenario | Use Pipeline? |
|---|---|
| Multi-step creation with side effects (uploads, code generation) | Yes |
| Multi-step auth flows (reset password, verify email) | Yes |
| Simple CRUD operations | No — use Actions |
| Single-step operations | No — use direct service method |
| Operations needing database transactions | No — use DB::transaction() in service |
Creating a New Pipeline
- Create a DTO in
app/Dtos/with public properties for each step to populate - Create pipe classes in
app/Pipelines/{Module}/ - Each pipe class needs a
handle($data, Closure $next)method - 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);
}
}