Laravel: The hidden Pipeline [Part 3]

Serialized content processing, meet your new friend: the Pipeline.

Italo Baeza Cabrera
4 min readSep 30, 2019

In this series of articles, I will talk about the Pipeline package that comes with Laravel but nobody uses it and it’s not documented. Why? Because it’s amazing.

A Hub to rule them all

Well, we know about how the Pipeline class can help us, and now is the turn of the Hub.

The job of the Hub is to hold multiple pipelines, each one identifiable by a string. Yes, no need to create pipelines every time you need to use them. Just declare it one time in the Hub and you’re set to use it with an expressive one liner.

If you check the code, you will note that is bound to the Hub Contract, and this interface only ensures it can send an object through one of the Pipelines it holds. The full Hub class allows to actually use any of the pipelines that can be registered by a name. That’s the gist of it.

You can add your own Pipeline by passing a Closure, which will receive a fresh instance of a Pipeline and the object or “thing” to pass through each pipe as second argument.

The pipeline() will allow you to register a Pipeline through a custom name so you can use it later by it. The defaults() method will hold a Pipeline that you can use when you don’t specify a Pipeline to use.

$hub = app(\Illuminate\Pipeline\Hub::class);$hub->defaults(function ($pipeline, $object) {
return $pipeline
->send($object)
->through([
EncodeToHtml::class
AddPunctutation::class,
RemoveDuplicatesCharacters::class,
UppercaseFirstCharacterOfEachPhrase::class,
])
->thenReturn();
});
$hub->pipeline('encode-only', function ($pipeline, $object) {
return $pipeline
->send($object)
->through(EncodeToHtml::class)
->thenReturn();
});

Using this closure-based approach let us store how to build each Pipeline instead of saving the whole Pipeline instance altogether, for example in the AppServiceProvider. We could even use container events to push the pipelines only when the Hub is resolved. More about that later.

Later in our code we can just use the pipe() method to pass the object to pipeline, instead of writing it all over again. If we don’t issue the name as second argument, it will use the default Pipeline.

/**
* Updates an Article
*
* @param \Illuminate\Http\Request $request
* @param \App\Article $article
* @param \Illuminate\Pipeline\Hub $hub
* @return \Illuminate\Http\Response
*/
public function save(Request $request, Article $article, Hub $hub)
{
$request->validate([
'message' => 'required|string',
]);
$article->message = $hub->pipe($request->message); $article->save(); return response()
->view('article.saved')
->with('article', $article);
}

As you may guess, creating a Hub can be very handy.

What about that Service Provider?

Yes, there is one called PipelineServiceProvider and it does only one thing: it registers a single instance of the Hub class. In other words, every time the application needs a class implementing a Hub Contract, it will resolve the Hub class instance of this package.

Now, notice something very particular: this PipelineServiceProvider is not registered anywhere. In other words, your application boots but this Service Provider isn’t picked up. Is up to the developer to do it, hopefully in the app.php providers array, but this may or may be not be useful for us.

For example, if you use only one Hub, registering the Service Provider could allow us to call it anywhere and receive the same Hub, with the predefined Pipelines ready to be used. We could use the register() method of the AppServiceProvider to “extend” the Hub instance with our own pipelines. Just for illustration:

/**
* Register any application services.
*
*
@return void
*/
public function register()
{
$this->app->extend(
\Illuminate\Contracts\Pipeline\Hub::class,
function ($hub) {
$hub->defaults(function ($pipeline, $object) {
return $pipeline
->send($object)
->through(\App\Pipelines\EncodeToHtml::class)
->thenReturn();
});

return $hub;
}
)
}

But, if you must use multiple Hubs, each dealing with multiple data, it could be better to create your own. To put it into perspective, an ArticleHub would have pipelines for sanitizing the body and the excerpt, while the CommentHub would have the same for a more stricter sanitizing of comments texts and usernames.

/**
* Register any application services.
*
*
@return void
*/
public function register()
{
$this->app->singleton(\App\Hubs\ArticleHub::class);
$this->app->singleton(\App\Hubs\CommentHub::class);
}

No need to declare these pipelines anywhere, you can just create methods inside each hub that executes a pipeline. Here is an example of my ArticleHub that uses some black magic to create a pipe if it wasn’t registered.

Then, later in your code, you just call the Hub pipelines through pipeline(), with what to pipe and the name of the Pipeline.

/**
* Updates an Article
*
* @param \Illuminate\Http\Request $request
* @param \App\Article $article
* @param \App\Hubs\ArticleHub $hub
* @return \Illuminate\Http\Response
*/
public function save(Request $request,
Article $article,
ArticleHub $hub)
{
$request->validate([
'message' => 'required|string',
'excerpt' => 'required|string|max:255'
]);
$article->message = $hub->pipe($request->message, 'body');
$article->excerpt = $hub->pipe($request->excerpt, 'excerpt');
$article->save(); return response()
->view('article.saved')
->with('article', $article);
}

And that’s the Hub.

There is no much more to tell, unless you want to know how the Pipeline magic works, to finish this series.

--

--

Italo Baeza Cabrera
Italo Baeza Cabrera

Written by Italo Baeza Cabrera

Graphic Designer graduate. Full Stack Web Developer. Retired Tech & Gaming Editor. https://italobc.com

No responses yet