The Pipeline Pattern — for fun and profit

Builds passing, code quality and coverage.

it’s that good.

From the authors:“This package provides a plug and play implementation of the Pipeline Pattern.

It’s an architectural pattern which encapsulates sequential processes.

When used, it allows you to mix and match operation, and pipelines, to create new execution chains.

The pipeline pattern is often compared to a production line, where each stage performs a certain operation on a given payload/subject.

Stages can act on, manipulate, decorate, or even replace the payload.

If you find yourself passing results from one function to another to complete a series of tasks on a given subject, you might want to convert it into a pipeline.


how do we use it?Composer installRun composer require league/pipeline in your project folder to add it to your project.


we’re ready to go!Assemble your classWe’re going to need a few pieces, so let me first show you how it all goes together, then we’ll look at the individual class requirements.

Here’s a new class which we’ll call RunAllTheThings — all this class will do is when we call the RunAllTheThings->doIt method, we will execute the pipeline and return the result.

<?phpnamespace ExamplePipeline;/** * Class RunAllTheThings * @package ExamplePipeline */class RunAllTheThings{ /** * @return Payload */ public function doIt() { // Define the pipeline stages $pipeline = (new Pipeline) ->pipe(new SegmentDoStage1)) ->pipe(new SegmentDoStage2)) ->pipe(new SegmentDoStage3)); // The payload is an object that's passed between stages $payload = new Payload(); // Run the pipeline $pipeline->process($payload); return $payload; }}Simple, eh?.Let’s have a look at some of the sub classes.

PayloadThe payload is something I really like to add in here as it keeps the data being passed though the pipe clean and readable.

In this example, payload is just a simple entity with a protected property and some getters and setters.

It gives us a clean way to not only update the results, but an expected format at the end.

Because the doIt method returns this object, we know that we can do something like $result->getResult() to get what we need.

This payload can be as simple of as complex as you need.

<?phpnamespace ExamplePipeline;/** * Class Payload * @package ExamplePipeline */class Payload{ /** * @var null|string */ protected $result = null; /** * @return null */ public function getResult() { return $this->result; } /** * @param string $result * @return static */ public function setResult($result) { $this->result = $result; return $this; } /** * @param $result * @return $this */ public function addResult($result) { $this->result .

= $result; return $this; }}Stage 1Stages need to be callable this means that they need to either be a closure, a callback or have an __invoke method.

We’re going to use the latter for this example.

For more about the callable type, please see the following reference.





php<?phpnamespace ExamplePipeline;/** * Class Stage1 * @package ExamplePipeline */class Stage1{ public function __invoke(Payload $payload) { $payload->addResult('all'); return $payload; }}In this stage, we’re adding the word all to the result string.

Once we’ve modified the result, you simply return the payload and the next stage will run.

Stage 2You may start to see the pattern emerging here.

This stage takes the payload, adds the word the and continues.

<?phpnamespace ExamplePipeline;/** * Class Stage2 * @package ExamplePipeline */class Stage2{ public function __invoke(Payload $payload) { $payload->addResult('the'); return $payload; }}Stage 3Guess what this stage does?.Yep, we add the word things to the result.

<?phpnamespace ExamplePipeline;/** * Class Stage3 * @package ExamplePipeline */class Stage3{ public function __invoke(Payload $payload) { $payload->addResult('things'); return $payload; }}The ResultRight, so let’s run our code.

<?php$allThethings = new ExamplePipelineRunAllTheThings();$result = $allThethings->doIt();var_dump($result->getResult());This will print the the following array to the screen.

string "allthethings"Short-circuitingSometimes you simply don’t want to continue processing.

If an order is invalid why would you attempt to capture payment?How do we tackle that?The simplest way is with a try/catch.

Now just throw a LogicException in one of your stages.

try { $pipeline->process($payload);catch (LogicException $e) { // Do something else!}Dynamic PipelinesThere’s another useful aspect of LeaguePipeline that I love which is the PipeBuilder — This allows you to add logic as to whether to add a stage to the pipeline.

use LeaguePipelinePipelineBuilder;// Instantiate the PipelineBuilder$builder = (new PipelineBuilder) ->add(new CreateOrder);// Conditional stageif ($order->getOrigin() === 'New Zealand') { $builder->add(new PreBookCarrier);}// Continue adding more stages$builder->add(new processPayment) ->add(new sendInvoice) ->add(new exportOrder);// Assemble to pipeline$pipeline = $builder->build();// Process$pipeline->process($order);Reusing PipesIt may be useful sometimes to reuse a pipe inside another pipe!.Easy enough since the pipe method can accept a callable OR another pipe.

$createOrder = (new Pipeline) ->pipe(new CreateOrder) ->pipe(new GenerateInvoice);$pipeline = (new Pipeline) ->pipe($createOrder) ->pipe(new ProcessPayment) ->pipe(new SendInvoice) ->pipe(new ExportOrder);$pipeline->process($order);ConclusionWhile the pipeline pattern isn’t for every occasion, there is a LOT of ugly code out there that could benefit from the simplicity and readability this pattern provides.

Next time you find yourself building a multi-stage piece of code try the pattern out and see how it goes.


. More details

Leave a Reply