laravel-notification-foundation maintained by thesetj
laravel-notification-foundation
A foundation package for building Laravel notifications. Instead of writing toDatabase(), toFcm(), and toSms() from scratch in every notification, you populate a shared data bag using fluent setters in the constructor — the package delivers it to each active channel automatically.
Channels are registered globally in a published service provider (similar to middleware in bootstrap/app.php) and can be toggled on or off per notification instance.
Installation
composer require thesetj/laravel-notification-foundation
Publish the service provider stub:
php artisan vendor:publish --tag=notification-foundation-provider
Register it in bootstrap/providers.php (Laravel 11+) or config/app.php:
App\Providers\NotificationFoundationServiceProvider::class,
The zero-override pattern
The default toDatabase(), toFcm(), and toSms() implementations read directly from what you set via the fluent API. For most notifications, no payload methods need to be written at all — the entire notification is just a constructor:
use NotificationFoundation\BaseNotification;
class OrderShippedNotification extends BaseNotification
{
public function __construct(Order $order, User $recipient)
{
// Shared data is merged into every active channel's payload automatically
$sharedData = [
'order_id' => (string) $order->id,
'order_code' => $order->code,
'user_name' => $recipient->name,
];
$this->setSharedData($sharedData)
// FCM gets a tailored title and body on top of shared data
->setDataForFcmChannel([
'title' => 'Your order has shipped',
'body' => "Order #{$order->code} is on its way.",
])
// Database relies on shared data alone — no extra data needed
// SMS is off by default in the service provider; turn it on for this notification
->useSmsChannel()
->setDataForSmsChannel([
'body' => "Hi {$recipient->name}, order #{$order->code} has shipped.",
])
// This order does not trigger an email
->useEmailChannel(false);
}
}
setSharedData() is merged into every active channel's payload automatically. Individual channels can add their own data on top, or rely on shared data alone. No toDatabase(), toFcm(), or toSms() methods needed.
Overriding a payload method
Override only what you need. The data bag and shared data are still available:
class OrderShippedNotification extends BaseNotification
{
public function __construct(private Order $order)
{
$this->setSharedData(['order_id' => (string) $order->id]);
}
public function toFcm(mixed $notifiable): FcmMessage
{
return FcmMessage::create()
->title('Your order has shipped')
->body("Order #{$this->order->id} is on its way.")
->setData($this->getSharedData())
->android(['priority' => 'high'])
->apns(['headers' => ['apns-priority' => '10']]);
}
}
Per-instance channel toggling
Every registered channel — built-in or custom — automatically gets fluent toggle methods:
(new OrderShippedNotification($order))
->useFcmChannel(false)
->useSmsChannel(true)
->useEmailChannel(); // any registered channel works the same way
$notification->enableSmsChannel()->disableFcmChannel();
The default active state per channel is set in the service provider (see below).
Channel registration
Edit the published App\Providers\NotificationFoundationServiceProvider. Comment out or remove what you don't need, and add your own channels:
public function boot(): void
{
BaseNotification::registerChannel('database', 'database', true);
BaseNotification::registerChannel('fcm', FcmChannel::class, true);
BaseNotification::registerChannel('sms', SmsChannel::class, false);
// Custom channels get the same toggle and data-setter API automatically
// BaseNotification::registerChannel('mail', 'mail', false);
// BaseNotification::registerChannel('slack', SlackChannel::class, false);
}
The third argument is the default state (true = active, false = inactive).
Driver binding
FcmChannel and SmsChannel delegate delivery to driver contracts. Bind your implementations in a service provider:
use NotificationFoundation\Contracts\FcmDriverContract;
use NotificationFoundation\Contracts\SmsDriverContract;
$this->app->bind(FcmDriverContract::class, YourFcmDriver::class);
$this->app->bind(SmsDriverContract::class, YourSmsDriver::class);
FCM driver
Implement FcmDriverContract. The contract's send() method receives either a single token string or an array of tokens, so it can serve as a gateway to whatever internal structure your provider requires:
use NotificationFoundation\Contracts\FcmDriverContract;
use NotificationFoundation\Messages\FcmMessage;
class MyFcmDriver implements FcmDriverContract
{
public function send(FcmMessage $message, string|array $tokens): void
{
if (is_array($tokens)) {
$this->sendMany($message, $tokens);
} else {
$this->sendOne($message, $tokens);
}
}
private function sendOne(FcmMessage $message, string $token): void
{
// send $message->toArray($token) to the FCM HTTP v1 API
}
private function sendMany(FcmMessage $message, array $tokens): void
{
// use a batch API if your provider supports it, or loop sendOne()
foreach ($tokens as $token) {
$this->sendOne($message, $token);
}
}
}
SMS driver
Implement SmsDriverContract. The send() method can likewise act as a gateway — if your SMS provider supports bulk sending, add a private sendMany() and dispatch from send() based on whether $message->getTo() is a single number or a list:
use NotificationFoundation\Contracts\SmsDriverContract;
use NotificationFoundation\Messages\SmsMessage;
class MyProvider implements SmsDriverContract
{
public function send(SmsMessage $message): void
{
// deliver $message->getContent() to $message->getTo()
// optionally route to sendOne() / sendMany() based on recipient type
}
}
FCM routing
Add routeNotificationForFcm() to your notifiable model:
public function routeNotificationForFcm($notification): string|array
{
return $this->fcm_tokens; // single token or array for multi-device
}
SMS routing
Add routeNotificationForSms() to your notifiable model, or set the recipient directly on the message:
// On the notifiable model:
public function routeNotificationForSms($notification): string
{
return $this->phone_number;
}
// Or in toSms():
return SmsMessage::create('Your OTP is 1234')->to($notifiable->phone);