Qu’est-ce que le pattern pipeline
Le pattern pipeline fait circuler un objet à traiter (passable) au travers d’une série de pipes (étapes de traitement). Chaque pipe reçoit la sortie de l’étape précédente, y ajoute son traitement et passe le résultat au pipe suivant.
Dans Laravel, la classe Illuminate\Pipeline\Pipeline implémente ce pattern. Le traitement des middlewares fonctionne exactement de cette manière.
requête → [middleware 1] → [middleware 2] → [contrôleur] → réponse
Utilisations dans le cœur de Laravel
Le pipeline est largement utilisé au cœur de Laravel.
Middleware HTTP — Illuminate\Foundation\Http\Kernel fait passer la requête dans un pipeline de middlewares.
Commandes de console — pré/post-traitement des commandes Artisan.
Middlewares du Router — application des middlewares d’un groupe de routes.
Regardons comment le kernel HTTP utilise un pipeline.
// Extrait 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 ());
}
Utilisation de base
send / through / thenReturn
C’est la forme la plus simple.
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) — spécifie l’objet à faire passer dans le pipeline.
through($pipes) — spécifie le tableau de pipes.
thenReturn() — exécute le pipeline et retourne le résultat.
then
Pour spécifier le traitement final via une closure, utilisez then().
$result = app ( Pipeline :: class )
-> send ( $request )
-> through ([ AuthPipe :: class , LogPipe :: class ])
-> then ( function ( $request ) {
return 'processed: ' . $request ;
});
pipe
Pour ajouter un pipe après coup, utilisez pipe().
$pipeline = app ( Pipeline :: class )
-> send ( $data )
-> through ([ FirstPipe :: class ]);
// Insertion conditionnelle d'un pipe supplémentaire
if ( $needsExtra ) {
$pipeline -> pipe ( ExtraPipe :: class );
}
$result = $pipeline -> thenReturn ();
Pipes basés sur des classes
En utilisant des classes dédiées à la place de closures, la réutilisabilité augmente. Chaque classe implémente une méthode 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 === '<script>alert("xss")</script>'
via — changer la méthode appelée
Par défaut, la méthode handle du pipe est invoquée, mais via() permet d’utiliser un autre nom de méthode.
class ValidatePipe
{
public function process ( array $data , Closure $next ) : array
{
// Traitement de validation
return $next ( $data );
}
}
$result = app ( Pipeline :: class )
-> send ( $data )
-> through ([ ValidatePipe :: class ])
-> via ( 'process' )
-> thenReturn ();
Passer des paramètres aux pipes
Vous pouvez passer des paramètres à un pipe en utilisant : et , dans le nom de classe. C’est le même mécanisme que la syntaxe throttle:60,1 des middlewares.
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 — traitement après le pipeline
finally() permet d’enregistrer un callback exécuté systématiquement après le pipeline, qu’il ait réussi ou échoué.
$result = app ( Pipeline :: class )
-> send ( $request )
-> through ([ LogPipe :: class , AuthPipe :: class ])
-> finally ( function ( $passable ) {
// Journalisation, libération de ressources, etc.
logger () -> info ( 'Pipeline completed' , [ 'request' => $passable ]);
})
-> thenReturn ();
withinTransaction — exécution dans une transaction
Depuis Laravel 11, le pipeline entier peut s’exécuter à l’intérieur d’une transaction de base de données.
$result = app ( Pipeline :: class )
-> send ( $order )
-> through ([
CreateOrderPipe :: class ,
ChargePaymentPipe :: class ,
SendConfirmationPipe :: class ,
])
-> withinTransaction ()
-> thenReturn ();
Vous pouvez aussi spécifier une connexion de base de données particulière.
-> withinTransaction ( 'mysql' )
Si l’un des pipes lève une exception, toute la transaction est annulée.
Exemple pratique : pipeline de traitement de commande
Voici un exemple de traitement de commande e-commerce structuré en plusieurs pipes.
Créer les classes de 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 insuffisant : { $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 );
}
}
Exécuter le 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 ();
}
}
Implémentation interne du Pipeline
La méthode carry() est le cœur du pipeline. Elle utilise array_reduce pour replier les pipes de droite à gauche et construire une chaîne de closures.
// Interne 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 );
};
};
}
En inversant l’ordre des pipes via array_reverse avant de les passer à array_reduce, l’ordre d’appel est préservé : la pile de closures se construit depuis l’intérieur, de sorte que le premier pipe s’exécute en premier.
Le pipeline est aussi appelé architecture « oignon ». La requête traverse depuis la couche externe (le premier pipe) vers l’intérieur, la réponse remonte de l’intérieur vers l’extérieur. C’est pourquoi chaque middleware peut ajouter du traitement avant et après l’appel à $next().
Combinaison avec le trait Macroable
La classe Pipeline utilise le trait Macroable, ce qui vous permet d’ajouter vos propres méthodes.
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 ();
Étapes suivantes
Trait Macroable Découvrez comment ajouter vos propres méthodes aux classes existantes avec le trait Macroable.