Laravel Pipelines & Composable Job Middleware
Davey Shafik
Posted on October 2, 2024
A feature you are probably familiar with in Laravel — even if you don't know it — is the Pipeline.
Laravel features a Pipeline implementation that allows you to easily create a series of pipes through which a value should pass:
use \Illuminate\Pipeline\Pipeline;
$pipeline = new Pipeline(null);
$greeting = $pipeline->send(new Stringable(''))
->through(
fn (Stringable $greeting, Closure $next) =>
$next($greeting->append('Hello World!')
)
->then(fn (Stringable $greeting) => $greeting->toString());
// Hello World!
What the heck is a Pipe?
A pipe is a callable
, or any class with a specific public method (by default: handle()
) — it should take two arguments, a "passable", and a Closure. The passable is the value you are sending through the pipeline, and the Closure is a wrapper that will call the next pipe in the pipeline, passing through the argument passed to it (which should be the passable) and handle the return value.
In the example above, the pipe is a short closure that accepts our Stringable
passable, and then calls the $next
closure passing in a modified string.
A more complex set of pipes might look like this:
use Illuminate\Support\Arr;
class Greeting {
public function __invoke(Stringable $str, Closure $next)
{
$greetings = ['Hey', 'Hello', 'Hi', 'Hola', 'Howdy'];
$str = $str->append(Arr::random($greetings));
$str = $next($str);
return $str->finish('!');
}
}
use Illuminate\Support\Facades\Auth;
class AddName {
public function handle(Stringable $str, Closure $next)
{
if (Auth::hasUser()) {
$str = $str->append(' ')->append(Auth::user()->name);
}
return $next($str);
}
}
$pipeline = new Pipeline(null);
$greeting = $pipeline
->send(new Stringable(''))
->through([
Greeting::class,
AddName::class,
])
->then(fn (Stringable $greeting) => $greeting->toString());
These two pipes work together to construct a greeting:
- The first pipe (
Greeting
) will add a random friendly greeting word - Then it will call the second pipe (
AddName
) that will conditionally add the users name - Finally the updated string will be returned back to the first pipe to make sure it ends in an exclamation point.
It looks something like this:
Using a different handler method
If you look closely at the example above, I use __invoke()
with the first pipe, but handle()
on the second. The Pipeline
class will accept any valid callable, or, if it's a string, it will resolve the class using the service container and if invokable (has __invoke()
) it will execute that, otherwise it will use the handler method, which defaults to handle()
.
You can change the named handler method by calling Pipeline->via()
:
$result = $pipeline
->send($passable)
->through(…)
->via('METHOD NAME HERE')
->then(…);
Why Does This Look So Familiar?
If this is starting to look familiar, it's because it looks a heck of a lot like HTTP middleware… right? Well, that's because it is HTTP middleware… or rather, internally, Laravel uses Pipelines for multiple different things, including the HTTP request lifecycle:
// Illuminate\Foundation\Http\Kernel->sendRequestThroughRouter()
return (new Pipeline($this->app))
->send($request)
->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
->then($this->dispatchToRouter());
Job Middleware
What you might be surprised to find out is that Laravel Jobs also go through a pipeline when executed, and can also have middleware.
Job middleware is extremely powerful, it can be used to track metrics about your jobs, to handle errors, to verify that a job hasn't expired for some reason, or whatever else you can imagine.
Example: Authenticated Users in Jobs
A common problem that is encountered when reusing (particularly older code) within jobs is that they are tightly coupled to authentication using the Auth
facade to retrieve the user, and this fails because a job running in the background is no longer executing within the users authenticated web session.
We can use middleware to ensure that jobs that require an authenticated user are logged in during job execution:
use Illuminate\Queue\Jobs\Job;
class Auth {
public function __invoke(Job $job, Closure $next)
{
if (method_exists($job, 'getUser')) {
Auth::login($job->getUser());
}
return $next($job);
}
}
Composable Job Middleware
A common pattern in Laravel is to use traits to add features to classes, for example HasTimestamps
, SerializesModels
, etc.
This would be a great way to add middleware to our jobs. Laravel will automatically call a jobs middleware()
method which should return an array of middleware to add to the pipeline. We can use this feature to create composable middleware.
The HasMiddleware
Trait
The goal of the HasMiddleware
trait is to add a middleware()
method that will dynamically create an array of middleware based on the traits it uses. To do this, for each middleware we create a trait that has a middleware<TraitName>()
method — e.g. for the HasDeadline
middleware trait, it would have a middlewareHasDeadline()
method — which is called by the middleware()
function and it's result is added to the list of middleware.
namespace App\Jobs\Traits;
use function class_basename;
use function class_uses_recursive;
use function method_exists;
trait HasMiddleware
{
public function middleware(): array
{
$middleware = [];
if (method_exists(parent::class, 'middleware')) {
$middleware = parent::middleware();
}
foreach (class_uses_recursive($this) as $trait) {
$method = 'middleware' . class_basename($trait);
if (method_exists($trait, $method)) {
$middleware = array_merge($middleware, $this->{$method}());
}
}
return $middleware;
}
}
You can then add middleware by creating a trait:
namespace App\Jobs\Traits;
use App\Jobs\Middleware\Auth;
trait HasAuth {
use HasMiddleware;
protected function middlewareHasAuth(): array
{
return [Auth::class];
}
}
You Job class can then use
the HasAuth
trait:
use App\Jobs\Traits\HasAuth;
class MyJob extends Job {
use HasAuth;
public function __invoke()
{
// Auth::user()
}
}
This trait use
s the HasMiddleware
trait itself so that the job doesn't need to use
it explicitly in addition to the HasAuth
trait.
Creating Job Middleware
Starting in Laravel 11.26, you can now use the artisan make:job-middleware
command to generate Job middleware.
In Summary…
The Laravel Pipeline is very versatile — although for Bag I went with League\Pipeline as it was faster and functionally, it's almost identical — and can be used for any composable series of operation that act on a value — be that HTTP request middleware, Job middleware, or anything else you want.
Posted on October 2, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.