# Rationale
Sometimes Pipeline Pattern is what does the job. The example described in this page takes a single input (a checkout model) and spits out a collection of shipping methods. What happens in-between is an combination of a group of small functions (pipes).
An important thing to note is all the pipe classes can be fully unit tested in isolation irrespective of the complexity.
// all pipes may access our context
$context = new class(){
public MessageBag $errors;
public function __construct()
{
$this->errors = new MessageBag();
}
};
$shippingMethods = Pipeline::with($checkout, $context)->pipe(
Pipeline::tap(ValidateCheckout::class), // validate items count, shipping address, etc.
GetShippableItems::class,
Pipeline::map(
// get available rates for each item
Pipeline::concat(GetLineItemShippingProfile::class), // add shipping profile
Pipeline::concat(GetLineItemLocations::class), // uses shipping profile to retrive locations
Pipeline::spreadArgs(GetLineItemZoneRates::class), // uses item, profile, and locations to get rates
),
Pipeline::collect(),
CombinRates::class, // combine all rates
Pipeline::tap(ValidateItemStock::class), // validate available stock for final rates
Pipeline::tap(function () {
// save any errors to the checkout instance
if ($context->errors->isNotEmpty()) {
$checkout->errors->fillFrom($context->errors);
}
}),
Pipeline::on(
fn () => $context->errors->isEmpty(),
CreateShippingMethods::class, // persist shipping methods if we've everything is ok
fn () => collect([]),
),
);
This being a simplied version of a much complex pipeline; the original solution uses switch
, pick
, nth
, merge
, omit
operators and even nested pipes to further get rates per location types and shipping profiles.