Back to Course |
Laravel User Timezones Project: Convert, Display, Send Notifications

Sending Notifications: Scheduled Job

We have fully prepared our system for the Notifications, and we have some data stored in our database. The next step is to create a system that would send them out at specific times.


Creating the Command

For this, we will create a new command:

app/Console/Commands/SendScheduledNotificationsCommand.php

<?php
 
namespace App\Console\Commands;
 
use App\Models\ScheduledNotification;
use Exception;
use Illuminate\Console\Command;
 
class SendScheduledNotificationsCommand extends Command
{
protected $signature = 'send:scheduled-notifications';
 
protected $description = 'Sends scheduled notifications to the users';
 
public function handle(): void
{
$notificationsToSend = ScheduledNotification::query()
->where('sent', false)
->where('processing', false)
->where('tries', '<=', config('app.notificationAttemptAmount'))
->where('scheduled_at', '<=', now()->format('Y-m-d H:i'))
->get();
 
// Lock jobs as processing
ScheduledNotification::query()
->whereIn('id', $notificationsToSend->pluck('id'))
->update(['processing' => true]);
 
foreach ($notificationsToSend as $notification) {
try {
dispatch(new ProcessNotificationJob($notification->id));
} catch (Exception $exception) {
$notification->increment('tries');
$notification->update(['processing' => false]);
}
}
}
}

Which will contain the logic to check for any Notifications that need to be sent and trigger their sending. We will also lock the Notifications as processing so that we don't send the same Notification twice.

Let's add the missing configuration option:

config/app.php

// ...
'aliases' => Facade::defaultAliases()->merge([
// 'Example' => App\Facades\Example::class,
])->toArray(),
 
'notificationAttemptAmount' => 5
// ...

And trigger the new command in our Scheduler to run every minute:

app/Console/Kernel.php

protected function schedule(Schedule $schedule): void
{
// ...
$schedule->command('send:scheduled-notifications')->everyMinute();
}

This ensures that we will check for any Notifications that need to be sent every minute and won't miss any Notifications.


Processing the Notifications

Now that we have the command, we need to implement the Job that will process the Notifications and send them out:

app/Jobs/ProcessNotificationJob.php

 
use App\Models\ScheduledNotification;
use App\Notifications\BookingReminder1H;
use Exception;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
 
class ProcessNotificationJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
 
private ScheduledNotification $notification;
 
public function __construct(int $notificationID)
{
try {
$this->notification = ScheduledNotification::query()
->with(['user'])
->withWhereHas('notifiable')
->findOrFail($notificationID);
} catch (Exception $exception) {
// Backup, just try to get the notification by id and fail the job
$this->notification = ScheduledNotification::query()
->find($notificationID);
$this->fail($exception);
}
}
 
public function handle(): void
{
if ($this->notification->sent || $this->notification->tries >= config('app.notificationAttemptAmount')) {
return;
}
if (!$this->notification->notifiable) {
// Makes sure that the notifiable is still available
$this->fail();
return;
}
try {
switch ($this->notification->notification_class) {
case BookingReminder1H::class:
$this->notification->user->notify(new BookingReminder1H($this->notification->notifiable));
break;
}
 
$this->notification->update(['processing' => false, 'sent' => true, 'sent_at' => now()]);
} catch (Exception $exception) {
$this->fail($exception);
}
}
 
public function fail($exception = null)
{
$this->notification->update(['processing' => false]);
$this->notification->increment('tries');
}
}

It might seem like a lot is going on here, but I'll try to explain the basic concept of what you see:

  • Construct - takes the ScheduledNotification ID and attempts to retrieve the information. If that fails - it will call the fail() method and fail the Job.
  • fail() method - increments the tries and sets the processing to false to allow another worker to pick up the Job and try again.
  • At the beginning of the handle() method we are looking at the Notification and checking if it's already sent or if it has reached the maximum amount of tries. If so - we just return and do nothing.
  • There's a switch statement that will trigger the Notification based on the class name. While it's not the perfect solution - it is great to illustrate the principles.
  • Each case calls a user->notify() method with the Notification class and the notifiable Model. This will send a Notification to the user.
  • After the Notification is sent, we update the Notification to be marked as sent and set the sent_at timestamp. This will allow us to track the Notifications that were sent and when.
  • If any of the above fails, we will call the fail() method and increment the tries.

That's it! We have a fully working Notification system that will send out Notifications to our users based on the schedule we have set up.