Demystifying Laravel Queues

At some point, your application will need to listen for cues and emit an event for them. Those cues could be birthday or appointment reminders or expiry reminders for users of the trial version of your saas app. Unsurprisingly, Laravel has a built-in way to handle this graciously. Here, we would be sending out reminders to users of our amazing saas app who are on the trial version 15 days before their trial expires.

First of All...Drivers

True to Laravel's reputation for including as many batteries as possible (which can definitely be good or bad), the framework comes with out-of-the-box support for different queue handlers including redis, beanstalk, amazon SQS and of course, relational databases, which we will be using here. There is also an amazing documentation on queue drivers here so feel free to explore them.

To use the database driver, we will need to create a table in our application database to hold the jobs and laravel provides a clean helper command for that. So we will create and run the necessary migrations like so:

php artisan queue:table
php artisan migrate

Diving In

We want to equip our job with the ability of being continuously run in the background so we will create a new artisan command to trigger the task. Artisan commands are laravel's way of extending the already powerful command-line interface and there is an extensive documentation for it living here . Creating a new one is as simple as running:

php artisan make:command SendExpiryCommand

which generates a new class in app/Console/Commands . We will modify the signature and description attribute and the handle() method of this new class before using.

The signature is what we call from the CLI and has the format command:name , - think db:seed , config:clear , etc, as such, let's change our signature to trial_expiry:notify and the description to something actually more descriptive. Now the handle() method is what gets called when we run our shiny command and it is where our code (or most of it anyway) will live. Now, we are going to assume that our app has a Trials model with an expires_in field which holds the date the trial is supposed to expire as well as a user_id field which references our User model. So here is what our final handle() code looks like:

/**
* Execute the console command.
*
* @return mixed
*/
public function handle(){
    //get the date of 15 days from now
    $actualDate = Carbon::now()->addDays(15);
    $actualDate = $actualDate->format('Y-m-d');
    $candidates = Trial::where('expires_in', '=', $actualDate)->get();      
    foreach ($candidates as $candidate) {
        SendExpiryNotification::dispatch($candidate);
    }
}

Our SendExpiryNotification is the actual queue-able job but it doesn't exist yet so let's create that:

php artisan make:job SendExpiryNotification

The above command will create a new class in app/Jobs which implements a ShouldQueue indicating that the job is to be run asynchronously. Don't worry if the Jobs folder doesn't exist yet, artisan will create it if it's not there. In the new class we meet another handle() method which like the command, is the sauce of our class which gets called when we run dispatch and it where our code will live.

Hey there! Now the some of the reasons we are passing a Trial instance instead of say, email, might be obvious but in addition to what is in your head, we want a fresh instance of our eloquent model just before running it from the queue which is exactly what this does. Jacob Bennet has an amazing blog post that details this. Also, our handle method won't take a trial instance as a parameter directly, instead, we feed the instance to the job constructor and call it from the handler.

Enough talk, show me the code! Alright, so here is what our SendExpiryNotification job looks like

<?php

namespace App\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Support\Facades\Mail;

class SendExpiryNotification implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public $trial;

    public function __construct($candidate) {
        $this->trial = $candidate;
    }

    public function handle()
    {
        $user = User::find($this->trial->user_id);
        Mail::raw('Hello, Your trial would be expiring soon and we wanted you to know!', function($message) {
            $message->to($user->email);
        });
    }
}

...hang in there

Now that the big part is done, let's

see

make it work. We will start our queue listen with:

php artisan queue:listen

or:

php artisan queue:work --daemon

though general opinion is that the second works much better. So while our queue listeners are still running, we launch our command using the command signature which in this case would be

php artisan trial_expiry:notify

Easy-peasy innit? Oh and by the way, here is laravel's queue documentation if you are that kind of person.