Unconventional Laravel: Synchronous jobs for reusability

Code reusability is a big thing to consider when working on larger applications, but have you ever considered using synchronous jobs?

Published 1 month ago
Laravel

This article is part of the Unconventional Laravel series.

  1. Auto-validating models
  2. Synchronous jobs for reusability

  3. Route groups and `$router`

Most applications use jobs as a way of pushing heavy logic off of the main thread and doing work asynchronously, in the background.

After looking at some larger applications, I found a few that used synchronous jobs as a way of splitting up application logic into reusable components.

The idea

Laravel provides some convenient ways of dispatching jobs. If you had a job called CreateSubscription you could push it to the queue using CreateSubscription::dispatch(). You could also process it synchronously using CreateSubscription::dispatchNow().

A little known fact is that you can actually return things from the job itself.

Let's create a barebones job:

use App\Models\User;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Contracts\Queue\ShouldQueue;

class CreateSubscription implements ShouldQueue
{
    use Dispatchable;
    
    private $user;
  
    private $plan;
    
    public function __construct(User $user, string $plan)
    {
        $this->user = $user;
        $this->plan = $plan;
    }
}

The job will receive an instance of App\Models\User and store it in a private property. The visibility of this property isn't important since this job will be self handling.

Here's the handle method:

public function handle()
{
    $subscription = SomeSubscriptionService::create([
        'email' => $this->user->email,
        'plan' => $this->plan,
    ]);

    $this->user->update([
        'subscription_id' => $subscription->id,
    ]);

    return $subscription;
}

The logic isn't important for this article. The important part is the return $subscription at the end of the method.

If we were to dispatch this job inside of a controller:

class SubscriptionController
{
    public function store(Request $request)
    {
        $subscription = CreateSubscription::dispatchNow(
            $request->user(),
            $request->input('plan')
        );
        
        return redirect()->route('subscription.thank-you', [
            'plan' => $subscription->plan,
        ]);
    }    
}

The return value of the job will be given back to the controller, in this case assigned to a variable so that it can be used further along.

Pros

It's a job, queue it when you want

Since this is just a regular Laravel job that implements the ShouldQueue marker interface, you could just as easily push it to the queue using CreateSubscription::dispatch().

There's no need to change the logic at all, since the return value will just get thrown away, it won't harm the queue.

With the example above where you need the subscription afterwards, it doesn't make much sense, but for something like RenewSubscription, you could use the job synchronously when the user manually renews in the browser, then use the same job from a scheduled command that handles automatic renewals or something.

Reusability

These jobs are reusable and can be used anywhere in your applications (controllers, listeners, commands, etc). This benefit is a little less important because in reality you could create any sort of class and have it do the same thing, just like the "action" pattern that is quite popular.

Cons

It's different

Jobs aren't designed for this. They can do it, but according to convention they shouldn't. If a new developer comes on to a project and sees this, they're probably going to think "What the f&$*?".

It's not officially documented

This functionality won't be found anywhere in the official Laravel docs, so it could disappear in a major version update. I doubt it will, but it could.

Sign off

If you've ever used this pattern, let me know on Twitter because I'd love to know. Let me know if you have any questions or things you think I missed.

I'd like to shout out laravel.io too (Dries), since this is where I initially discovered this functionality. You can take a look at the GitHub repo to find out a bit more.

As always, thanks for reading! 👋