A medida que los proyectos crecen, los flujos también se vuelven más enrevesados. De repente tienes servicios llenos de métodos, condicionales por todas partes y lógica difícil de seguir. Para evitar llegar a ese punto existe un patrón que encaja especialmente bien en flujos complejos: las pipelines de Laravel.
¿Por qué usar pipelines?
Las pipelines nos permiten fragmentar los procesos en pasos más simples, independientes y reutilizables, todo esto siguiendo un orden definido. En la versión actual de Laravel, que es la 12, las pipelines han sido refinadas para hacerlas más potentes:
- Se integran mejor con las clases invocables y con tipado estricto.
- Introduce el método finally, que permite definir un cierre en el flujo.
- Hace hincapié en la limpieza del código, la separación de responsabilidades y nos facilita el mantenimiento del flujo.
Un ejemplo básico podría ser un flujo de pago en el que necesitamos confirmar la identidad del usuario, preparar el payload para el proveedor y ejecutar el pago.
return app(Pipeline::class)
->send($paymentRequest)
->through([
CheckUserData::class,
BuildGatewayPayload::class,
ExecutePayment::class,
])
->thenReturn();Cada uno de estos pasos están aislados en su propia clase, las cuales hacen una sola cosa.
Pero, ¿qué pasa cuando nuestro proceso, por ejemplo, es diferente según el tipo de usuario? Sería tan fácil como definir nuestro proceso en un array de pasos, que luego pasaremos al método through.
$steps = [
CheckUserData::class,
];
if ($user->isEnterprise()) {
$steps[] = ApplyEnterpriseDiscounts::class;
}
if ($payment->amount > 5000) {
$steps[] = RequireAdditionalVerification::class;
}
return pipeline($payment)->through($steps)->thenReturn();
Método finally
Una de las mejoras que trae Laravel 12 es el método finally. Esto garantiza que, tanto si la pipeline falla como si se completa correctamente, se ejecute un cierre limpio.
class PaymentController {
public function pay(Payment $payment) {
return app(Pipeline::class)
->send($payment)
->through([
ValidatePayment::class,
CheckResponseStatus::class,
UpdateOrderInfo::class,
])
->finally(function ($payment) {
Storage::delete("temp/{$payment->id}");
Cache::forget("payment-lock:{$payment->id}");
$payment->update(['completed_at' => now()]);
})
->then(fn() => ['status' => 'deployed']);
}
}Manejando errores en pipelines
Cuando el flujo crece, el manejo de errores se vuelve aún más importante. Para manejar esto, podemos optar por varios enfoques.
El primer caso es parar una vez ocurra el error, por si no tiene sentido continuar.
class ExecutePayment {
public function __invoke($payload, Closure $next) {
if ($payload->amount > $payload->maxAllowed) {
throw new DailyLimitExceeded();
}
return $next($payload);
}
}El segundo caso es si necesitamos registrar el error y seguir.
class SafePipe {
public function __invoke($payload, Closure $next) {
try {
return $next($payload);
} catch (\Throwable $e) {
$payload->errors[] = $e->getMessage();
return $payload;
}
}
}Según lo que necesitemos, podemos combinar ambos casos.
Conclusión
Cuando tenemos flujos con muchos pasos, dependencias condicionales o lógica que cambia según el contexto, las pipelines se vuelven un patrón ágil y muy útil para mantener el código legible, mantenible y bien estructurado.