Applications grow, specs change, bugs happen, and our code can quickly get out of hand. Duplicated code, ifs, elses, switches, and statements like “I used this there, but it needs to be slightly different here”, help turn our work of art into a garbled mess. But what if we could fix that?
That’s where Pipelines come in. We can break out our code into smaller chunks, called stages, that we can group or combine into configurations called pipelines. Separating our code into stages allows for easier and isolated testing. Reassembling stages sequentially into a pipeline allows us to have consistent results.
In this talk, we’ll define what stages and pipelines are. We'll examine when pipelines can help us and when they are not the right solution. We will look at example pipelines ranging from simple to multi-stage reusable pipelines. We'll implement what we've learned by walking through a refactor and discover how testing becomes easier with stages. You will walk away with an understanding of the what the Pipeline pattern is and when it can benefit your application.
2. Steven Wade - @stevenwadejrhttps://joind.in/talk/4c853
About me
Steven Wade
• Husband, father
• Founder/Organizer of UpstatePHP
Twitter: @stevenwadejr
Email: stevenwadejr@gmail.com
3. Steven Wade - @stevenwadejrhttps://joind.in/talk/4c853
Problem
4. Steven Wade - @stevenwadejrhttps://joind.in/talk/4c853
Problem
class OrderProcessController
{
public function processOrder(Request $request)
{
$order = new Order;
$order->billing = $request->get('billing');
$order->shipping = $request->get('shipping');
$order->products = $request->get('products');
$order->save();
return response(null);
}
}
5. Steven Wade - @stevenwadejrhttps://joind.in/talk/4c853
Problem
class OrderProcessController
{
public function processOrder(Request $request)
{
$order = new Order;
$order->billing = $request->get('billing');
$order->shipping = $request->get('shipping');
$order->products = $request->get('products');
// Calculate sub-total
$productsTotal = 0;
foreach ($request->get('products', []) as $product) {
$productsTotal += ($product['price'] * $product['quantity']);
}
$order->order_total += round($productsTotal, 2);
$order->save();
return response(null);
}
}
6. Steven Wade - @stevenwadejrhttps://joind.in/talk/4c853
Problem
class OrderProcessController
{
public function processOrder(Request $request)
{
$order = new Order;
$order->billing = $request->get('billing');
$order->shipping = $request->get('shipping');
$order->products = $request->get('products');
// Calculate sub-total
$productsTotal = 0;
foreach ($request->get('products', []) as $product) {
$productsTotal += ($product['price'] * $product['quantity']);
}
$order->order_total += round($productsTotal, 2);
// Process payment
$receipt = $this->paymentGateway->process($order);
$order->confirmation = $receipt->transaction_id;
event(new OrderProcessed($order));
$order->save();
return response(null);
}
}
10. Steven Wade - @stevenwadejrhttps://joind.in/talk/4c853
11. Steven Wade - @stevenwadejrhttps://joind.in/talk/4c853
12. Steven Wade - @stevenwadejrhttps://joind.in/talk/4c853
Pipelines!*(possibly)
13. Steven Wade - @stevenwadejrhttps://joind.in/talk/4c853
Goals
• Understand what a pipeline is, and what stages are
• Learn to recognize a pipeline in our code
• Refactor code to stages
• See how stages make testing easier
• Understand when a pipeline is not the appropriate option
14. Steven Wade - @stevenwadejrhttps://joind.in/talk/4c853
What is a pipeline?
15. Steven Wade - @stevenwadejrhttps://joind.in/talk/4c853
|
17. Steven Wade - @stevenwadejrhttps://joind.in/talk/4c853
So, what is a pipeline?
18. Steven Wade - @stevenwadejrhttps://joind.in/talk/4c853
Pipeline
A series of processes chained together to where
the output of each is the input of the next
32. Steven Wade - @stevenwadejrhttps://joind.in/talk/4c853
Nested Function Calls
function timesTwo($payload) {
return $payload * 2;
}
function addOne($payload) {
return $payload + 1;
}
// outputs 21
echo addOne(
timesTwo(10)
);
33. Steven Wade - @stevenwadejrhttps://joind.in/talk/4c853
Looping Through Stages
$stages = ['timesTwo', 'addOne'];
$payload = 10;
foreach ($stages as $stage) {
$payload = call_user_func($stage, $payload);
}
// outputs 21
echo $payload;
34. Steven Wade - @stevenwadejrhttps://joind.in/talk/4c853
35. Steven Wade - @stevenwadejrhttps://joind.in/talk/4c853
–Martin Fowler
“Any fool can write code that a computer can
understand. Good programmers write code that
humans can understand.”
36. Steven Wade - @stevenwadejrhttps://joind.in/talk/4c853
LeaguePipeline
37. Steven Wade - @stevenwadejrhttps://joind.in/talk/4c853
LeaguePipeline
Frank de Jonge
@frankdejonge
Woody Gilk
@shadowhand
38. Steven Wade - @stevenwadejrhttps://joind.in/talk/4c853
LeaguePipeline - Functional
$pipeline = (new Pipeline)
->pipe('timesTwo')
->pipe('addOne');
// Returns 21
$pipeline->process(10);
39. Steven Wade - @stevenwadejrhttps://joind.in/talk/4c853
LeaguePipeline - Class Based
$pipeline = (new Pipeline)
->pipe(new TimeTwoStage)
->pipe(new AddOneStage);
// Returns 21
$pipeline->process(10);
40. Steven Wade - @stevenwadejrhttps://joind.in/talk/4c853
Putting it into practice
41. Steven Wade - @stevenwadejrhttps://joind.in/talk/4c853
eCommerce
• Create the order
• Calculate the total
• Process the payment
• Subtract coupons from total
• Add appropriate taxes
• Calculate and add shipping costs
42. Steven Wade - @stevenwadejrhttps://joind.in/talk/4c853
eCommerce
• Create the order
• Calculate the sub-total
• Subtract coupons from total
• Add appropriate taxes
• Calculate and add shipping costs
• Process the payment
52. Steven Wade - @stevenwadejrhttps://joind.in/talk/4c853
eCommerce - Testing
public function __construct(CouponRepository $couponRepository)
{
$this->couponRepository = $couponRepository;
}
53. Steven Wade - @stevenwadejrhttps://joind.in/talk/4c853
eCommerce
public function processOrder(Request $request)
{
$order = $this->createOrder($request);
$this->calculateSubTotal($order);
$this->applyCoupon($request, $order);
$this->applyTaxes($order);
$this->calculateShipping($order);
$this->processPayment($order);
$order->save();
return response(null);
}
54. Steven Wade - @stevenwadejrhttps://joind.in/talk/4c853
eCommerce - Testing
public function __construct(
CouponRepository $couponRepository,
ShippingCalculator $shippingCalculator,
PaymentGateway $paymentGateway
) {
$this->couponRepository = $couponRepository;
$this->shippingCalculator = $shippingCalculator;
$this->paymentGateway = $paymentGateway;
}
55. Steven Wade - @stevenwadejrhttps://joind.in/talk/4c853
eCommerce
public function processOrder(Request $request)
{
$order = $this->createOrder($request);
$this->calculateSubTotal($order);
$this->applyCoupon($request, $order);
$this->applyTaxes($order);
$this->calculateShipping($order);
$this->processPayment($order);
$order->save();
return response(null);
}
56. Steven Wade - @stevenwadejrhttps://joind.in/talk/4c853
Stages!
57. Steven Wade - @stevenwadejrhttps://joind.in/talk/4c853
eCommerce - Refactored
class OrderProcessController
{
protected $stages = [
CalculateSubTotal::class,
ApplyCoupon::class,
ApplyTaxes::class,
CalculateShipping::class,
ProcessPayment::class,
];
public function processOrder(Request $request)
{
$order = OrderFactory::fromRequest($request);
$pipeline = new LeaguePipelinePipeline;
foreach ($this->stages as $stage) {
$stage = app($stage, ['request' => $request]);
$pipeline->pipe($stage);
}
$pipeline->process($order);
$order->save();
return response(null);
}
}
58. Steven Wade - @stevenwadejrhttps://joind.in/talk/4c853
Testing Stages
class OrderProcessTest
{
public function test_sales_tax()
{
$subTotal = 100.00;
$taxRate = 0.06;
$expected = 106.00;
$order = new Order;
$order->order_total = $subTotal;
$order->billing['state'] = 'SC';
$stage = new ApplyTaxes;
$stage($order);
$this->assertEquals($expected, $order->order_total);
}
}
59. Steven Wade - @stevenwadejrhttps://joind.in/talk/4c853
Recap
• Pipeline: a series of processes (stages) chained together to
where the output of each is the input of the next.
• Stages create readability, reusability, and testability
60. Steven Wade - @stevenwadejrhttps://joind.in/talk/4c853
61. Steven Wade - @stevenwadejrhttps://joind.in/talk/4c853
62. Steven Wade - @stevenwadejrhttps://joind.in/talk/4c853
Don't
63. Steven Wade - @stevenwadejrhttps://joind.in/talk/4c853
Choose wisely
64. Steven Wade - @stevenwadejrhttps://joind.in/talk/4c853
Encore!
65. Steven Wade - @stevenwadejrhttps://joind.in/talk/4c853
Fun Stuff - Variable Pipeline
class RelatedData
{
protected $stages = [
LatestActivityPipeline::class,
ListMembershipsStage::class,
WorkflowMembershipsStage::class,
DealsStage::class,
];
public function process()
{
$pipeline = new Pipeline;
foreach ($this->stages as $stage) {
if ($this->stageIsEnabled()) {
$pipeline->pipe(new $stage);
}
}
return $pipeline->process([]);
}
}
66. Steven Wade - @stevenwadejrhttps://joind.in/talk/4c853
Fun Stuff - Async Stages
class DealsStage
{
public function __invoke(array $payload)
{
if ($cache = $this->cache->get('deals')) {
$promise = new FulfilledPromise($cache);
} else {
$promise = $this->api->getDeals();
$promise = $promise->then(function ($deals) {
$this->cache->set('deals', $deals);
return $deals;
});
}
$payload['deals'] = $promise;
return $payload;
}
}
67. Steven Wade - @stevenwadejrhttps://joind.in/talk/4c853
Fun Stuff - Async Stages
class RelatedData
{
public function process()
{
$promises = $this->pipeline->process([]);
return GuzzleHttpPromiseunwrap($promises);
}
}
72. Steven Wade - @stevenwadejrhttps://joind.in/talk/4c853
Suggested Questions
• Is it better to have dynamic stages (e.g. - conditions are run in advance
to determine the steps) or pass all the steps and let the stage contain
it's own condition?
• When should one stage be broken into 2?