Your first flow
Build the checkout from the introduction, send three orders through it, and read what each one did. About ten minutes.
1. The thing that flows
A flow works on your own object. Here it's an Order: a few fields (items, shippingCountry, billingCountry, discountCode, ...) and a few methods the flow calls (customerIsTrusted(), addRiskSignal(), holdForReview(), isOnHold(), markPlaced()). It's a plain PHP class, nothing to extend: docs/examples/Order.php.
2. Write a rule
A rule is one small class that makes one decision. The hub calls the method named after the class, and the rule ends in one of four ways:
| The rule... | Write | On the receipt |
|---|---|---|
| passes | return self::SUCCESS; |
✓ |
| raises a concern, and the flow carries on | $this->warning('why'); return self::FAIL; or $this->error('why'); |
✗ |
| has nothing to do | return $this->skip('why'); |
↷ |
| stops the flow | $this->fatal('why'); |
■ |
Here's the stock check:
<?php
use Illuminate\Support\Str;
use Keaton\PipeDream\PipeRule;
class ConfirmItemsInStock extends PipeRule
{
public static $name = 'Confirm items in stock';
public static $description = 'Every item in the cart can actually ship';
public function confirmItemsInStock($order, Warehouse $warehouse)
{
$missing = array_filter($order->items, fn ($item) => ! $warehouse->inStock($item['sku']));
if ($missing !== []) {
$this->fatal('Out of stock: '.implode(', ', array_column($missing, 'name')));
}
$this->info(count($order->items).' '.Str::plural('item', count($order->items)).' in stock');
return self::SUCCESS;
}
}- The first parameter is your order (inside its envelope).
- Every parameter after it comes from Laravel's container, so the
Warehouseis injected. $this->info()writes the note you'll see next to the rule on the receipt.
3. Arrange rules into a flow
Group rules into named hubs (the stages) and stack the hubs in a manager (the flow). How each stage behaves is written on the stage itself: skipIf() to skip it, then() to act once it's done, errorsAreFatal() to make any failure stop the flow.
<?php
use Keaton\PipeDream\PipeHub;
use Keaton\PipeDream\PipeManager;
$envelope = PipeManager::create([
PipeHub::create()
->name('Validate the order')
->description('Is this a complete order for items we can actually ship?')
->addPipes([
RequireShippingAddress::class,
ConfirmItemsInStock::class,
ApplyDiscountCodes::class,
CalculateTax::class,
]), // a broken order ends the whole flow here
PipeHub::create()
->name('Fraud screen')
->description('Does this look like the real cardholder?')
->addPipes([
MatchBillingToShippingCountry::class,
CheckCardVelocity::class,
ScoreDeviceFingerprint::class,
])
->skipIf(fn ($order) => $order->customerIsTrusted(), 'Trusted customer')
->then(fn ($order) => $order->riskSignals() >= 2 ? $order->holdForReview('Two or more fraud signals') : null),
PipeHub::create()
->name('Charge')
->description('Take the money, only for orders we will ship')
->addPipes([
AuthorizePayment::class,
CapturePayment::class,
])
->skipIf(fn ($order) => $order->isOnHold() ? "Held for review: {$order->holdReason}" : null)
->errorsAreFatal(), // a failed charge ends the flow
PipeHub::create()
->name('Fulfil')
->description('Reserve stock, tell the warehouse, email the receipt')
->addPipes([
ReserveInventory::class,
SendToWarehouse::class,
EmailReceipt::class,
])
->skipIf(fn ($order) => $order->isOnHold() ? "Held for review: {$order->holdReason}" : null),
])
->name('Checkout')
->subject($order)
->then(fn ($order) => $order->markPlaced())
->sendIt();sendIt() runs it and returns the envelope: your order, plus the record of everything that happened.
4. Read the receipt
Send a suspicious order through (a US card shipping to the Netherlands, after four cards were tried in ten minutes) and echo its receipt:
<?php
$order = new Order(
id: 'ord_48213',
items: [
['sku' => 'tee-blk-m', 'name' => 'Black tee', 'price' => 30.00],
['sku' => 'hoodie-gry-l', 'name' => 'Grey hoodie', 'price' => 65.00],
['sku' => 'cap-nvy', 'name' => 'Navy cap', 'price' => 25.00],
],
shippingAddress: 'Keizersgracht 123, Amsterdam',
shippingCountry: 'NL',
billingCountry: 'US',
discountCode: 'SPRING20',
cardsTriedRecently: 4,
);
require __DIR__.'/checkout.php';
echo $envelope->receipt();
Checkout · On hold: Two or more fraud signals (2.62 ms)
Validate the order — Is this a complete order for items we can actually ship?
✓ RequireShippingAddress (0.07 ms)
✓ ConfirmItemsInStock (1.79 ms) 3 items in stock
✓ ApplyDiscountCodes (0.03 ms) SPRING20 applied: -$24.00
✓ CalculateTax (0.02 ms) Tax: $7.92
Fraud screen — Does this look like the real cardholder?
✗ MatchBillingToShippingCountry (0.02 ms) Billing US, shipping NL
✗ CheckCardVelocity (0.01 ms) 4 cards tried on this account in 10 minutes
✓ ScoreDeviceFingerprint (0.01 ms)
Charge — Take the money, only for orders we will ship · skipped: Held for review: Two or more fraud signals
↷ AuthorizePayment skipped
↷ CapturePayment skipped
Fulfil — Reserve stock, tell the warehouse, email the receipt · skipped: Held for review: Two or more fraud signals
↷ ReserveInventory skipped
↷ SendToWarehouse skipped
↷ EmailReceipt skippedRead it top to bottom: the fraud screen raised two concerns, its then() put the order on hold, and Charge and Fulfil were skipped, each saying why. Nothing was charged. The header says "On hold" because Order implements the optional ReceiptSummary interface; The receipt shows how.
5. Same flow, different stories
A trusted customer's order skips the fraud screen and goes all the way through:
Checkout · Placed (0.28 ms)
Validate the order — Is this a complete order for items we can actually ship?
✓ RequireShippingAddress (0.02 ms)
✓ ConfirmItemsInStock (0.02 ms) 1 item in stock
↷ ApplyDiscountCodes skipped No discount code
✓ CalculateTax (0.01 ms) Tax: $5.36
Fraud screen — Does this look like the real cardholder? · skipped: Trusted customer
↷ MatchBillingToShippingCountry skipped
↷ CheckCardVelocity skipped
↷ ScoreDeviceFingerprint skipped
Charge — Take the money, only for orders we will ship
✓ AuthorizePayment (0.01 ms)
✓ CapturePayment (0.01 ms)
Fulfil — Reserve stock, tell the warehouse, email the receipt
✓ ReserveInventory (0.01 ms)
✓ SendToWarehouse (0.01 ms)
✓ EmailReceipt (0.01 ms)An order with something out of stock stops at the first stage. Nothing after the fatal runs, so the later stages don't appear at all:
Checkout · stopped (0.22 ms)
Validate the order — Is this a complete order for items we can actually ship?
✓ RequireShippingAddress (0.02 ms)
■ ConfirmItemsInStock (0.13 ms) Out of stock: Limited-run posterOne flow, three outcomes, and each receipt explains itself.
Next
- Rules: everything a rule can do.
- Saving and replaying runs: pull any order's receipt back up weeks later.
- Testing flows: assert on the receipt.