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.
Top comments (4)
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
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
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?
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?