-
Notifications
You must be signed in to change notification settings - Fork 0
Notification
Razy provides a channel-based notification system for sending notifications through multiple delivery channels (mail, database, etc.). The system supports lifecycle hooks, error handling, audit logging, and pluggable channel architecture.
use Razy\Notification\NotificationManager;
use Razy\Notification\Channel\{MailChannel, DatabaseChannel};
// Set up notification manager
$manager = new NotificationManager();
$manager->registerChannel(new MailChannel(function (string $to, array $data) {
mail($to, $data['subject'], $data['body']);
}));
$manager->registerChannel(new DatabaseChannel());
// Create and send a notification
$notification = new WelcomeNotification($user->name);
$manager->send($user, $notification);Extend the abstract Notification class and define which channels to use:
use Razy\Notification\Notification;
class OrderShippedNotification extends Notification
{
public function __construct(
private readonly string $orderId,
private readonly string $trackingNumber
) {}
// Required: specify delivery channels
public function via(object $notifiable): array
{
return ['mail', 'database'];
}
// Channel-specific data → method name: to{ChannelName}()
public function toMail(object $notifiable): array
{
return [
'subject' => "Order #{$this->orderId} Shipped!",
'body' => "Your tracking number is: {$this->trackingNumber}",
];
}
public function toDatabase(object $notifiable): array
{
return [
'order_id' => $this->orderId,
'tracking_number' => $this->trackingNumber,
'message' => 'Your order has been shipped.',
];
}
// Fallback for channels without a specific method
public function toArray(object $notifiable): array
{
return [
'order_id' => $this->orderId,
'tracking_number' => $this->trackingNumber,
];
}
}The getData($channel, $notifiable) method follows this resolution order:
-
Call
to{Channel}($notifiable)→ e.g.toMail(),toDatabase() -
Fall back to
toArray($notifiable)if the channel-specific method doesn't exist -
Return empty array if neither exists
Each notification automatically receives a unique 32-character hex ID:
$notification = new OrderShippedNotification('ORD-123', 'TRK-456');
echo $notification->getId(); // e.g. 'a1b2c3d4e5f6...'
echo $notification->getType(); // 'OrderShippedNotification' (class name)
// Override ID for idempotency
$notification->setId('custom-unique-id');The manager routes notifications to the appropriate channels:
use Razy\Notification\NotificationManager;
$manager = new NotificationManager(logging: true);
// Register channels
$manager->registerChannel($mailChannel);
$manager->registerChannel($databaseChannel);
// Inspect channels
$manager->hasChannel('mail'); // true
$manager->getChannelNames(); // ['mail', 'database']
$channel = $manager->getChannel('mail');$user = new User(['id' => 42, 'email' => 'john@example.com']);
$notification = new OrderShippedNotification('ORD-123', 'TRK-456');
$manager->send($user, $notification);
// Dispatches to 'mail' and 'database' channels (as defined by via())$users = [$user1, $user2, $user3];
$notification = new SystemMaintenanceNotification('2026-03-01 02:00 UTC');
$manager->sendToMany($users, $notification);The notifiable object needs to expose the data each channel requires:
For MailChannel:
-
A
getEmail(): stringmethod, or -
A public
$emailproperty
For DatabaseChannel:
-
A
getId(): string|intmethod, or -
A public
$idproperty -
Falls back to
spl_object_id()if neither exists
class User {
public function __construct(
public readonly int $id,
public readonly string $email,
public readonly string $name,
) {}
// Or use methods:
// public function getId(): int { return $this->id; }
// public function getEmail(): string { return $this->email; }
}Delegates email delivery to a callable you provide:
use Razy\Notification\Channel\MailChannel;
// Simple mailer
$mailChannel = new MailChannel(function (string $to, array $data) {
mail($to, $data['subject'] ?? 'Notification', $data['body'] ?? '');
});
// With recording (for testing)
$mailChannel = new MailChannel(
mailerFn: function (string $to, array $data) { /* ... */ },
recording: true
);
// After sending
$sent = $mailChannel->getSent();
// [['to' => 'john@example.com', 'data' => [...]], ...]
$mailChannel->clearSent();Stores notification records in memory, with an optional persistence callback:
use Razy\Notification\Channel\DatabaseChannel;
// In-memory only (for testing)
$dbChannel = new DatabaseChannel();
// With persistence callback
$dbChannel = new DatabaseChannel(function (array $record) {
DB::table('notifications')->insert($record);
});
// After sending
$all = $dbChannel->getRecords();
// Each record: {id, type, notifiable_type, notifiable_id, data, created_at}
$userRecords = $dbChannel->getRecordsFor($user);
$count = $dbChannel->count();
$dbChannel->clearRecords();Implement NotificationChannelInterface for custom delivery:
use Razy\Notification\NotificationChannelInterface;
use Razy\Notification\Notification;
class SlackChannel implements NotificationChannelInterface
{
public function __construct(
private readonly string $webhookUrl
) {}
public function getName(): string
{
return 'slack';
}
public function send(object $notifiable, Notification $notification): void
{
$data = $notification->getData('slack', $notifiable);
// POST to Slack webhook
$payload = json_encode([
'channel' => $data['channel'] ?? '#general',
'text' => $data['message'] ?? '',
]);
$ch = curl_init($this->webhookUrl);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_exec($ch);
curl_close($ch);
}
}
// Register the custom channel
$manager->registerChannel(new SlackChannel('https://hooks.slack.com/...'));Then in your notification:
class DeployNotification extends Notification
{
public function via(object $notifiable): array
{
return ['slack', 'database'];
}
public function toSlack(object $notifiable): array
{
return [
'channel' => '#deployments',
'message' => "Deployment v{$this->version} completed!",
];
}
}The NotificationManager provides three lifecycle hooks:
// Before each channel send
$manager->beforeSend(function (object $notifiable, Notification $notification, string $channel) {
echo "Sending {$notification->getType()} via {$channel}\n";
});
// After each successful channel send
$manager->afterSend(function (object $notifiable, Notification $notification, string $channel) {
echo "Sent {$notification->getType()} via {$channel}\n";
});
// On channel error → IMPORTANT: swallows exceptions when hook is present
$manager->onError(function (object $notifiable, Notification $notification, string $channel, \Throwable $e) {
error_log("Failed to send via {$channel}: {$e->getMessage()}");
// Without this hook, the exception would propagate and stop remaining channels
});Note: When
onErroris registered, exceptions from individual channels are caught and passed to the hook instead of propagating. This allows remaining channels to still be processed. Without the hook, exceptions propagate normally.
Enable audit logging to track all sent notifications:
$manager = new NotificationManager(logging: true);
// ... send some notifications ...
$log = $manager->getSentLog();
// [
// [
// 'notification_id' => 'a1b2c3d4...',
// 'notification_type' => 'OrderShippedNotification',
// 'notifiable_type' => 'User',
// 'channel' => 'mail',
// 'sent_at' => '2026-02-23T14:30:00+00:00',
// ],
// ...
// ]
$manager->clearSentLog();| Method | Signature | Description |
| --- | --- | --- |
| via (abstract) | (object $notifiable): array | Return channel names |
| getId | (): string | Auto-generated 32-char hex ID |
| setId | (string $id): static | Override ID |
| getType | (): string | Class name |
| getData | (string $channel, object $notifiable): array | Calls to{Channel}() or toArray() |
| Method | Signature | Description |
| --- | --- | --- |
| __construct | (bool $logging = false) | Enable audit logging |
| registerChannel | (NotificationChannelInterface $channel): static | Add a channel |
| getChannel | (string $name): ?NotificationChannelInterface | Get channel by name |
| getChannelNames | (): array | All registered channel names |
| hasChannel | (string $name): bool | Check channel exists |
| send | (object $notifiable, Notification $notification): void | Send to one |
| sendToMany | (iterable $notifiables, Notification $notification): void | Send to many |
| beforeSend | (callable $callback): static | Pre-send hook |
| afterSend | (callable $callback): static | Post-send hook |
| onError | (callable $callback): static | Error hook (swallows exceptions) |
| getSentLog | (): array | Audit log entries |
| clearSentLog | (): static | Clear audit log |
| Method | Signature | Description |
| --- | --- | --- |
| send | (object $notifiable, Notification $notification): void | Deliver notification |
| getName | (): string | Channel identifier |