Vai al contenuto principale

Cos’è il pattern pipeline

Il pattern pipeline è un design pattern che fa passare un oggetto da elaborare (passable) attraverso una serie ordinata di pipe (step). Ogni pipe riceve l’output dello step precedente, aggiunge la propria elaborazione e passa al successivo. In Laravel il pattern è implementato dalla classe Illuminate\Pipeline\Pipeline; è esattamente il meccanismo che regge l’elaborazione dei middleware.
richiesta → [middleware 1] → [middleware 2] → [controller] → risposta

Punti di utilizzo nel core di Laravel

La pipeline è usata in modo diffuso nel cuore di Laravel.
  • Middleware HTTP — in Illuminate\Foundation\Http\Kernel la richiesta passa attraverso la pipeline dei middleware
  • Comandi console — pre e post elaborazione dei comandi Artisan
  • Middleware del Router — applicazione dei middleware ai gruppi di rotte
Osserviamo il punto in cui l’HTTP kernel usa la pipeline.
// da 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 di base

send / through / thenReturn

Ecco la struttura più semplice di una 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 l’oggetto che attraverserà la pipeline
  • through($pipes) — indica l’array di pipe
  • thenReturn() — esegue la pipeline e restituisce il risultato

then

Se vuoi indicare l’elaborazione finale con una closure, usa then().
$result = app(Pipeline::class)
    ->send($request)
    ->through([AuthPipe::class, LogPipe::class])
    ->then(function ($request) {
        return 'processed: ' . $request;
    });

pipe

Per aggiungere pipe successivamente usa pipe().
$pipeline = app(Pipeline::class)
    ->send($data)
    ->through([FirstPipe::class]);

// inserisci pipe aggiuntivi in base a una condizione
if ($needsExtra) {
    $pipeline->pipe(ExtraPipe::class);
}

$result = $pipeline->thenReturn();

Pipe class-based

Usando classi dedicate al posto di closure aumenti la riusabilità. Ogni classe pipe implementa il metodo 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 — cambiare il metodo invocato

Di default viene chiamato il metodo handle della pipe, ma con via() puoi indicarne un altro.
class ValidatePipe
{
    public function process(array $data, Closure $next): array
    {
        // logica di validazione
        return $next($data);
    }
}

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

Passare parametri alla pipe

Puoi passare parametri alle pipe usando : e , nel nome della classe. È lo stesso meccanismo della sintassi dei 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 — elaborazione al termine della pipeline

Con il metodo finally() registri un callback che verrà eseguito sempre al termine della pipeline, indipendentemente da successo o fallimento.
$result = app(Pipeline::class)
    ->send($request)
    ->through([LogPipe::class, AuthPipe::class])
    ->finally(function ($passable) {
        // logging, rilascio di risorse, ecc.
        logger()->info('Pipeline completed', ['request' => $passable]);
    })
    ->thenReturn();

withinTransaction — esecuzione dentro una transazione

Da Laravel 11 puoi eseguire l’intera pipeline all’interno di una transazione del database.
$result = app(Pipeline::class)
    ->send($order)
    ->through([
        CreateOrderPipe::class,
        ChargePaymentPipe::class,
        SendConfirmationPipe::class,
    ])
    ->withinTransaction()
    ->thenReturn();
Puoi anche indicare una specifica connessione al database.
->withinTransaction('mysql')
Se una qualsiasi pipe solleva un’eccezione, l’intera transazione viene rollbackata.

Esempio pratico: pipeline di gestione ordini

Un esempio in cui l’elaborazione degli ordini di un e-commerce è composta da più pipe.
1

Creare le classi 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("Scorte insufficienti: {$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

Eseguire la 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();
    }
}

Implementazione interna di Pipeline

Il metodo carry() è il cuore della pipeline. Con array_reduce piega le pipe da destra a sinistra e costruisce la catena di closure.
// interno di 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);
        };
    };
}
Passando le pipe in ordine inverso ad array_reduce con array_reverse si mantiene l’ordine corretto di invocazione. Lo stack di closure viene costruito dall’interno verso l’esterno, così che la prima pipe venga eseguita per prima.
La pipeline è chiamata anche architettura “a cipolla”. La richiesta attraversa gli strati dall’esterno (la prima pipe) verso l’interno, e la risposta torna indietro dall’interno verso l’esterno. È per questo che ogni middleware può aggiungere logica prima e dopo la chiamata a $next().

Combinazione con il trait Macroable

La classe Pipeline usa il trait Macroable, quindi puoi aggiungere metodi personalizzati.
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();

Prossimi passi

Trait Macroable

Impara come aggiungere metodi personalizzati alle classi esistenti con il trait Macroable.
Ultima modifica il 13 luglio 2026