4.8 KiB
Notifications
Notifications are driven by NotificationRegistry in Modules\Core\Notification. Each notification class declares what event it listens to and who to notify, and the registry wires up the event listener automatically.
Async delivery
All notification classes extend BaseNotification, which implements ShouldQueue and uses the Queueable trait. This means every notification is delivered asynchronously via the queue — the HTTP request returns immediately and the email is sent by a queue worker.
In local development or environments without a queue worker, set QUEUE_CONNECTION=sync in .env to send emails inline.
Key naming convention
Notification keys follow the pattern module.event.refersTo.via:
| Segment | Description | Example |
|---|---|---|
module |
The module the notification belongs to | appointment |
event |
The event that triggers it | booked, cancelled, rescheduled |
refersTo |
Who the notification is about | customer, staff |
via |
The delivery channel | mail, slack |
Example: appointment.cancelled.customer.mail
Built-in notifications
| Key | Class | Event | Channel |
|---|---|---|---|
appointment.booked.customer.mail |
AppointmentBookedCustomerNotification |
AppointmentBooked |
|
appointment.booked.staff.mail |
AppointmentBookedStaffNotification |
AppointmentBooked |
|
appointment.cancelled.customer.mail |
AppointmentCancelledCustomerNotification |
AppointmentCancelled |
|
appointment.rescheduled.customer.mail |
AppointmentRescheduledCustomerNotification |
AppointmentRescheduled |
These are registered in AppointmentsServiceProvider::boot().
When AppointmentBooked fires
AppointmentBooked is not fired directly from book() or reassign(). It is fired by CreateGoogleMeetLinkListener after the Meet link has been created (or attempted). This means notifications always go out with the Meet link already in the event — if the link couldn't be created, meetUrl is null and the email templates handle that gracefully.
Resend behaviour
AppointmentBooked carries an isResend: bool flag (default false). When true:
AppointmentBookedStaffNotificationreturns an emptyvia()— no staff email is sentN8nWebhookListenershort-circuits — no webhook is fired
This flag is set by AppointmentService::resendConfirmation() so only the customer notification goes out on a manual resend.
Creating a notification
Extend BaseNotification:
use Illuminate\Support\Facades\Notification as NotificationFacade;
use Modules\Core\Notification\BaseNotification;
class AppointmentBookedAdminNotification extends BaseNotification
{
public function __construct(private readonly AppointmentBooked $event) {}
public static function getKey(): string
{
return 'appointment.booked.admin';
}
public static function listensTo(): string
{
return AppointmentBooked::class;
}
public function via(object $notifiable): array
{
return ['mail'];
}
public function notifiable(): \Illuminate\Notifications\AnonymousNotifiable
{
return NotificationFacade::route('mail', 'admin@example.com');
}
}
Then register it in your service provider:
NotificationRegistry::get()->register([AppointmentBookedAdminNotification::class]);
Notifiable: anonymous vs model
Return an AnonymousNotifiable (via Notification::route()) when there is no Eloquent model involved — e.g. a fixed email address or Slack webhook pulled from the event or config.
Return an Eloquent model when you have one. The model must implement routeNotificationFor for the relevant channel (Laravel does this automatically for mail via the Notifiable trait).
public function notifiable(): User
{
return User::find($this->event->userId);
}
Replacing a built-in notification
Register a new class with the same key. The registry replaces the previous class under that key, so the existing event listener will dispatch the new class instead.
NotificationRegistry::get()->register([MyCustomMailNotification::class]); // getKey() returns 'appointment.booked.customer.mail'
If overriding a module notification from AppServiceProvider, do it inside booted() to ensure module providers have already run:
public function boot(): void
{
$this->app->booted(function () {
NotificationRegistry::get()->register([MyCustomSlackNotification::class]);
});
}
Removing a notification
NotificationRegistry::get()->unregister('appointment.booked.customer.mail');
The event listener remains registered but short-circuits without dispatching anything.
Again, call this inside booted() when doing it from AppServiceProvider.