DEV Community

Cover image for Demystifying Laravel Queues
Michael Okoko
Michael Okoko

Posted on

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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

php artisan make:job SendExpiryNotification
Enter fullscreen mode Exit fullscreen mode

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);
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

...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
Enter fullscreen mode Exit fullscreen mode

or:

php artisan queue:work --daemon
Enter fullscreen mode Exit fullscreen mode


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
Enter fullscreen mode Exit fullscreen mode

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

Top comments (4)

Collapse
 
okolbay profile image
andrew • Edited

would be nice to know, how it works under the hood?

does this listener requires additional dependencies (supervisor, cron)
or its a long-running php-cli process (hence no opcache), is it restarting itself of leaking? Im curious because from my experinece long-running processes (queue listeners) is one of the last thing I would implement in php

Collapse
 
xanadev profile image
Elhaj Abid

You'll need to setup supervisor to start as many workers as you need and watch them, a sample is provided in the laravel doc here

Collapse
 
ccoeder profile image
H. Can Yıldırım

Hey,

Let's say I want to monitor websites and check them every minute, simple http request but thousands of urls. With that approach, you explained in the above, is it applicable to my case?

Collapse
 
idoko profile image
Michael Okoko

I think for your use case, you'd want to use a cloud monitoring tool instead of inflicting that much traffic on your servers. Out of curiosity though, why would you want to do that?