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.
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.
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:
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.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.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.user->notify()
method with the Notification class and the notifiable Model. This will send a Notification to the user.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.