Saltar al contenido principal

Qué es el patrón Pipeline

El patrón Pipeline consiste en hacer pasar un objeto (el «passable») por una serie de pipes (pasos de procesamiento) en orden. Cada pipe recibe la salida del paso anterior, la procesa y la pasa al siguiente. En Laravel, este patrón lo implementa la clase Illuminate\Pipeline\Pipeline y es exactamente el mecanismo que utilizan los middlewares.
Request → [Middleware 1] → [Middleware 2] → [Controlador] → Response

Dónde lo usa el core de Laravel

El pipeline se usa mucho en el corazón de Laravel:
  • Middlewares HTTPIlluminate\Foundation\Http\Kernel hace pasar la petición por el pipeline de middlewares.
  • Comandos de consola — procesamiento antes y después de los comandos Artisan.
  • Middlewares del Router — aplicación de middlewares a grupos de rutas.
Veamos la parte del HTTP kernel donde se usa el pipeline.
// De Illuminate\Foundation\Http\Kernel
protected function sendRequestThroughRouter($request)
{
    return (new Pipeline($this->app))
        ->send($request)
        ->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
        ->then($this->dispatchToRouter());
}

Uso básico

send / through / thenReturn

La forma más sencilla de configurar un pipeline.
use Illuminate\Pipeline\Pipeline;

$result = app(Pipeline::class)
    ->send('hello')
    ->through([
        function (string $passable, Closure $next): string {
            return $next(strtoupper($passable));
        },
        function (string $passable, Closure $next): string {
            return $next($passable . '!');
        },
    ])
    ->thenReturn();

// $result === 'HELLO!'
  • send($passable) — indica el objeto que atravesará el pipeline.
  • through($pipes) — indica el array de pipes.
  • thenReturn() — ejecuta el pipeline y devuelve el resultado.

then

Para definir el procesamiento final con un closure, usa then().
$result = app(Pipeline::class)
    ->send($request)
    ->through([AuthPipe::class, LogPipe::class])
    ->then(function ($request) {
        return 'processed: ' . $request;
    });

pipe

Para añadir pipes a posteriori, usa pipe().
$pipeline = app(Pipeline::class)
    ->send($data)
    ->through([FirstPipe::class]);

// Añade pipes adicionales según una condición
if ($needsExtra) {
    $pipeline->pipe(ExtraPipe::class);
}

$result = $pipeline->thenReturn();

Pipes basados en clase

Usar clases dedicadas en vez de closures mejora la reutilización. Cada clase implementa el método handle.
namespace App\Pipes;

use Closure;

class TrimPipe
{
    public function handle(string $passable, Closure $next): string
    {
        return $next(trim($passable));
    }
}
namespace App\Pipes;

use Closure;

class SanitizePipe
{
    public function handle(string $passable, Closure $next): string
    {
        $sanitized = htmlspecialchars($passable, ENT_QUOTES, 'UTF-8');

        return $next($sanitized);
    }
}
use App\Pipes\TrimPipe;
use App\Pipes\SanitizePipe;
use Illuminate\Pipeline\Pipeline;

$result = app(Pipeline::class)
    ->send('  <script>alert("xss")</script>  ')
    ->through([TrimPipe::class, SanitizePipe::class])
    ->thenReturn();

// $result === '&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;'

via — cambiar el método invocado

Por defecto se llama al método handle del pipe, pero con via() puedes indicar otro nombre.
class ValidatePipe
{
    public function process(array $data, Closure $next): array
    {
        // Lógica de validación
        return $next($data);
    }
}

$result = app(Pipeline::class)
    ->send($data)
    ->through([ValidatePipe::class])
    ->via('process')
    ->thenReturn();

Pasar parámetros a los pipes

Con : y , en el nombre de la clase puedes pasar parámetros al pipe. Es el mismo mecanismo que en el middleware throttle:60,1.
namespace App\Pipes;

use Closure;

class LimitLengthPipe
{
    public function handle(string $passable, Closure $next, int $max = 100): string
    {
        return $next(substr($passable, 0, $max));
    }
}
$result = app(Pipeline::class)
    ->send($longText)
    ->through(['App\Pipes\LimitLengthPipe:50'])
    ->thenReturn();

finally — código posterior al pipeline

Con finally() registras un callback que se ejecuta siempre al terminar el pipeline, tanto si hay éxito como si hay excepción.
$result = app(Pipeline::class)
    ->send($request)
    ->through([LogPipe::class, AuthPipe::class])
    ->finally(function ($passable) {
        // Log, liberar recursos, etc.
        logger()->info('Pipeline completed', ['request' => $passable]);
    })
    ->thenReturn();

withinTransaction — ejecutar dentro de una transacción

Desde Laravel 11, puedes ejecutar todo el pipeline dentro de una transacción de base de datos.
$result = app(Pipeline::class)
    ->send($order)
    ->through([
        CreateOrderPipe::class,
        ChargePaymentPipe::class,
        SendConfirmationPipe::class,
    ])
    ->withinTransaction()
    ->thenReturn();
Puedes indicar una conexión concreta.
->withinTransaction('mysql')
Si alguno de los pipes lanza una excepción, se hace rollback de toda la transacción.

Ejemplo práctico: pipeline de procesamiento de pedidos

Ejemplo que compone el procesamiento de un pedido en un e-commerce mediante varios pipes.
1

Crea las clases pipe

namespace App\Pipes\Order;

use App\Models\Order;
use Closure;

class ValidateInventoryPipe
{
    public function handle(Order $order, Closure $next): Order
    {
        foreach ($order->items as $item) {
            if ($item->product->stock < $item->quantity) {
                throw new \RuntimeException("Stock insuficiente: {$item->product->name}");
            }
        }

        return $next($order);
    }
}
namespace App\Pipes\Order;

use App\Models\Order;
use Closure;

class ReserveInventoryPipe
{
    public function handle(Order $order, Closure $next): Order
    {
        foreach ($order->items as $item) {
            $item->product->decrement('stock', $item->quantity);
        }

        return $next($order);
    }
}
namespace App\Pipes\Order;

use App\Models\Order;
use App\Services\PaymentService;
use Closure;

class ProcessPaymentPipe
{
    public function __construct(
        protected PaymentService $payment,
    ) {}

    public function handle(Order $order, Closure $next): Order
    {
        $this->payment->charge($order->total, $order->payment_token);
        $order->update(['status' => 'paid']);

        return $next($order);
    }
}
2

Ejecuta el pipeline

use App\Models\Order;
use App\Pipes\Order\ValidateInventoryPipe;
use App\Pipes\Order\ReserveInventoryPipe;
use App\Pipes\Order\ProcessPaymentPipe;
use Illuminate\Pipeline\Pipeline;

class OrderService
{
    public function process(Order $order): Order
    {
        return app(Pipeline::class)
            ->send($order)
            ->through([
                ValidateInventoryPipe::class,
                ReserveInventoryPipe::class,
                ProcessPaymentPipe::class,
            ])
            ->withinTransaction()
            ->thenReturn();
    }
}

Implementación interna de Pipeline

El método carry() es el corazón del pipeline. Usa array_reduce para plegar los pipes de derecha a izquierda y construir una cadena de closures.
// Interior de Pipeline::then()
protected function carry()
{
    return function ($stack, $pipe) {
        return function ($passable) use ($stack, $pipe) {
            if (is_callable($pipe)) {
                return $pipe($passable, $stack);
            } elseif (! is_object($pipe)) {
                [$name, $parameters] = $this->parsePipeString($pipe);
                $pipe = $this->getContainer()->make($name);
                $parameters = array_merge([$passable, $stack], $parameters);
            } else {
                $parameters = [$passable, $stack];
            }

            return $pipe->{$this->method}(...$parameters);
        };
    };
}
Al invertir los pipes con array_reverse y pasarlos a array_reduce, se conserva el orden de invocación correcto. La pila de closures se apila de dentro hacia fuera para que el primer pipe se ejecute primero.
El pipeline también se conoce como arquitectura «cebolla». La request atraviesa las capas de fuera adentro (empezando por el primer pipe) y la response vuelve de dentro hacia fuera. Por eso cada middleware puede añadir lógica antes y después de llamar a $next().

Uso combinado con el trait Macroable

La clase Pipeline usa el trait Macroable, así que puedes añadirle métodos propios.
use Illuminate\Pipeline\Pipeline;

Pipeline::macro('sendThroughLogging', function (array $pipes) {
    /** @var Pipeline $this */
    return $this->through(array_map(function ($pipe) {
        return function ($passable, $next) use ($pipe) {
            logger()->debug("Entering pipe: {$pipe}");
            $result = $next($passable);
            logger()->debug("Exiting pipe: {$pipe}");
            return $result;
        };
    }, $pipes));
});

$result = app(Pipeline::class)
    ->send($data)
    ->sendThroughLogging([TrimPipe::class, SanitizePipe::class])
    ->thenReturn();

Próximos pasos

Trait Macroable

Aprende cómo usar el trait Macroable para añadir métodos propios a clases existentes.
Última modificación el 13 de julio de 2026