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.
Hi, I have some questions as below:
Hi,
In any case, thanks for the feedback! We'll see what we can adjust and I'll let you know if we do!
Hi, does the system keep sending notifications automatically or do we need to do more setups to send these notifications? Thanks