Other packages’ notifications
Map a notification class another package sends to one of your types, and the package’s rules apply to it — preferences, mandatory flags, de-dup, quiet hours and the inbox. Or suppress it entirely:
// config/notifications.php
'interop' => [
'map' => [
RoundlyConsulting\Tickets\Notifications\TicketAssignedNotification::class => 'tickets.assigned',
RoundlyConsulting\Events\Notifications\EventReminderNotification::class => 'events.reminder',
],
'suppress' => [
RoundlyConsulting\Events\Notifications\EventUpdatedNotification::class, // every channel
RoundlyConsulting\Tickets\Notifications\TicketNoteAddedNotification::class => ['mail'],
],
'database' => 'redirect', // redirect | keep | both
'hold' => true, // hold mapped mail in quiet hours
'notifiables' => [], // extra recipient classes
],Or at runtime:
Notifications::interop()->map(TicketAssignedNotification::class, 'tickets.assigned');
Notifications::interop()->suppress(TicketNoteAddedNotification::class, ['mail']);
Notifications::interop()->mapped(); // class => type key
Notifications::interop()->suppressed();The mapped type describes the notification; subjectFrom() tells the package what it is about:
final class TicketAssigned extends NotificationType
{
public function key(): string { return 'tickets.assigned'; }
public function category(): string { return 'tickets'; }
public function subject(): ?string { return Ticket::class; }
public function subjectFrom(Notification $notification): ?Model { return $notification->ticket; }
public function window(): ?string { return 'PT15M'; }
public function title(NotificationPayload $p): string { return __('app.ticket_assigned'); } // when the notification's data has no "title"
}How it works
- A NotificationSending listener, asked per notifiable and per channel, after the notification’s own shouldSend(), at delivery time (on the worker for queued notifications). It cancels a channel or lets it through.
- Matching uses is_a(): a host subclass of a mapped class is mapped; an exact entry beats an ancestor, and the most specific ancestor wins.
- Only recipients — models using ReceivesNotifications, or a class in interop.notifiables — get preferences, de-dup, holds and the inbox redirect. Any other notifiable (a guest, a contact, an on-demand route) gets suppress only.
- Fail-open: apart from suppress, any exception in the listener is reported, NotificationInteropFailed fires, and the notification goes out untouched. An interop bug must never stop a password reset.
- De-dup is claim-first: two workers delivering two mapped notifications of one key at once send one mail.
- A held notification is stored encrypted and released by the sweep after re-checking access and the preference; a tampered or undecryptable payload fails as payload and is never unserialized. A notification that cannot be serialized is not held — it goes out at once.
- Mapped notifications are never digested; the doctor warns about a digestible mapped type. The package’s own mails are never touched.
- Laravel’s Notification::fake() dispatches no NotificationSending, so interop does not run under it.
Decision table (per notifiable × channel)
| Case | Result |
|---|---|
| The class is suppressed (all channels, or this one) | cancelled |
| Not mapped; not a recipient or an on-demand route; a channel other than mail / database | untouched |
| database, redirect (default) | An inbox item through the collapse step (title = the notification’s toDatabase()/toArray() title, else the type title); Laravel’s row cancelled. |
| database, keep / both | Untouched / inbox item + untouched. |
| mail, the subject (subjectFrom(), fresh) is not accessible | cancelled |
| mail, the type has a subject class but subjectFrom() returns null | Preferences and quiet hours apply; no access check, no de-dup (the doctor warns). |
| mail, the preference is off (not mandatory) | cancelled |
| mail, an open delivery in the window | Folded (count++), cancelled. |
| mail, quiet now, type not urgent, interop.hold on | Held (encrypted payload), cancelled now, released after quiet hours. |
| Otherwise | A sending delivery is claimed; Laravel sends; NotificationSent → sent, NotificationFailed → failed(transport). |
Time-critical guards
Anything that is_a auth-for-laravel’s AuthenticationNotification (OTPs, magic links, resets), Laravel’s ResetPassword or VerifyEmail may only map to a type that is urgent, has no window and is not digestible — otherwise a second code requested within the window would fold into the first and never arrive. Such a mapping is refused with InvalidInteropMappingException. The map is validated once every provider has booted (your types may be defined in AppServiceProvider::boot()), again lazily on first use, and by the doctor.
Ready map for Roundly packages
Suggested types for the Roundly packages’ notifications — none is auto-mapped, opt in per class. Class names are relative to RoundlyConsulting\<Package>\Notifications\ and end in Notification:
| Package | Classes | Suggested key | Priority | subjectFrom() |
|---|---|---|---|---|
tickets | TicketCreated, TicketAssigned, TicketReplied, TicketRequesterReplied, TicketNoteAdded, TicketMentioned, TicketStatusChanged, TicketMerged | tickets.created … | normal | $n->ticket |
tickets | TicketDueSoon, TicketOverdue | tickets.due_soon, tickets.overdue | normal / urgent | $n->ticket |
events | EventInvitation, RsvpConfirmed, WaitlistPromoted, EventUpdated, EventPostponed, EventRescheduled, EventMovedOnline, EventCancelled | events.invited … | normal (cancelled: urgent) | $n->event |
events | EventReminder | events.reminder | normal | $n->event |
messages | NewMessage | messages.new | normal (window PT10M) | $n->message->thread |
alerts | HealthCheckFailed | alerts.health_check_failed | urgent | — |
auth | any AuthenticationNotification (EmailOtp, MagicLink, ResetPassword, …) | auth.* | urgent, mandatory, no window | — |
Show your open-source love
This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.
More ways to support, including cryptoBy donating, you agree to our donation terms.
Want this built into your product?
We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.