ez-php / notification
Multi-channel notification orchestration for the ez-php framework — mail, broadcast, push, and database channels with optional queue-backed async delivery
Requires
- php: ^8.5
- ez-php/broadcast: ^2.0
- ez-php/contracts: ^2.0
- ez-php/mail: ^2.0
- ez-php/push: ^2.0
- ez-php/queue: ^2.0
Requires (Dev)
- ez-php/docker: ^2.0
- ez-php/rate-limiter: ^2.0
- ez-php/webhook: ^2.0
- friendsofphp/php-cs-fixer: ^3.94
- phpstan/phpstan: ^2.1
- phpstan/phpstan-deprecation-rules: ^2.0
- phpstan/phpstan-strict-rules: ^2.0
- phpunit/phpunit: ^13.0
Suggests
- ez-php/rate-limiter: Needed for RateLimitedChannel
- ez-php/webhook: Needed for WebhookChannel (signed, queued, retried HTTP delivery)
Provides
None
Conflicts
None
Replaces
None
- dev-main
- 2.5.6
- 2.5.5
- 2.5.4
- 2.5.3
- 2.5.2
- 2.5.1
- 2.5.0
- 2.4.11
- 2.4.10
- 2.4.9
- 2.4.8
- 2.4.7
- 2.4.6
- 2.4.5
- 2.4.4
- 2.4.3
- 2.4.2
- 2.4.1
- 2.4.0
- 2.3.9
- 2.3.8
- 2.3.7
- 2.3.6
- 2.3.5
- 2.3.4
- 2.3.3
- 2.3.2
- 2.3.1
- 2.3.0
- 2.2.1
- 2.2.0
- 2.1.1
- 2.1.0
- 2.0.1
- 2.0.0
- 1.14.0
- 1.13.1
- 1.13.0
- 1.12.2
- 1.12.1
- 1.12.0
- 1.11.2
- 1.11.0
- 1.10.0
- 1.9.2
- 1.9.1
- 1.9.0
- 1.8.0
- 1.7.1
- 1.7.0
- 1.6.1
- 1.6.0
- 1.5.1
- 1.5.0
- 1.4.2
- 1.4.1
- 1.4.0
- 1.3.0
- 1.2.0
- 1.1.1
- 1.1.0
This package is auto-updated.
Last update: 2026-09-30 19:50:23 UTC
README
Multi-channel notification orchestration for ez-php applications. Routes a single notification to any combination of mail, broadcast, and database channels. Optionally dispatches deliveries asynchronously via the queue.
Installation
composer require ez-php/notification
Register the provider in provider/modules.php:
EzPhp\Notification\NotificationServiceProvider::class,
Quick Start
1 — Define a notifiable (e.g. your User model)
use EzPhp\Notification\NotifiableInterface; class User implements NotifiableInterface { public function __construct( public readonly int $id, public readonly string $email, ) {} public function routeNotificationFor(string $channel): string|int { return match ($channel) { 'mail' => $this->email, 'broadcast' => 'users.' . $this->id, 'database' => $this->id, }; } }
2 — Define a notification
use EzPhp\Mail\Mailable; use EzPhp\Notification\Channel\ToMailInterface; use EzPhp\Notification\NotifiableInterface; use EzPhp\Notification\NotificationInterface; class WelcomeNotification implements NotificationInterface, ToMailInterface { public function via(): array { return ['mail']; } public function toMail(NotifiableInterface $notifiable): Mailable { return (new Mailable()) ->to((string) $notifiable->routeNotificationFor('mail')) ->subject('Welcome!') ->text('Thanks for signing up.'); } }
3 — Send it
use EzPhp\Notification\Notification; Notification::send($user, new WelcomeNotification());
Channels
Delivers via ez-php/mail. The notification must implement ToMailInterface:
public function toMail(NotifiableInterface $notifiable): Mailable;
broadcast
Delivers via ez-php/broadcast. The notification must implement ToBroadcastInterface:
public function broadcastOn(NotifiableInterface $notifiable): string; // channel name public function broadcastAs(NotifiableInterface $notifiable): string; // event name public function broadcastWith(NotifiableInterface $notifiable): array; // payload
database
Persists to the notifications table. The notification must implement ToDatabaseInterface:
public function toDatabase(NotifiableInterface $notifiable): array; // JSON payload
The table is auto-created on first use. To create it via migration instead:
-- MySQL CREATE TABLE notifications ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, type VARCHAR(255) NOT NULL, notifiable_type VARCHAR(255) NOT NULL, notifiable_id VARCHAR(255) NOT NULL, data JSON NOT NULL, read_at DATETIME NULL, created_at DATETIME NOT NULL ); -- SQLite CREATE TABLE notifications ( id INTEGER PRIMARY KEY AUTOINCREMENT, type TEXT NOT NULL, notifiable_type TEXT NOT NULL, notifiable_id TEXT NOT NULL, data TEXT NOT NULL, read_at TEXT NULL, created_at TEXT NOT NULL );
webhook
Requires ez-php/webhook (and its WebhookServiceProvider): delivery is HMAC-signed, queued and
retried. Works for customer webhooks and Slack/Teams incoming-webhook URLs alike.
use EzPhp\Notification\Channel\ToWebhookInterface; final class OrderShipped implements NotificationInterface, ToWebhookInterface { public function via(): array { return ['webhook']; } public function webhookUrl(NotifiableInterface $notifiable): string { return $notifiable->webhookUrl; } public function webhookSecret(NotifiableInterface $notifiable): string { return $notifiable->webhookSecret; } public function toWebhook(NotifiableInterface $notifiable): array { return ['event' => 'order.shipped', 'id' => $this->orderId]; } }
Reading and marking database notifications
use EzPhp\Notification\Channel\DatabaseNotificationRepository; $notifications = $app->make(DatabaseNotificationRepository::class); $notifications->unreadFor($user); // newest first: [['id' => 3, 'type' => ..., 'data' => [...], 'created_at' => ...], ...] $notifications->unreadCount($user); // e.g. for a badge $notifications->markAsRead($user, $id); // false if it isn't $user's or was already read $notifications->markAllAsRead($user); // number marked
Every query is scoped to the recipient, so passing an id from a request can't touch another user's notifications.
Rate-limited delivery
Channel\RateLimitedChannel decorates any other channel and caps how often it delivers, via ez-php/rate-limiter (soft dependency — must be installed separately, since it's declared in require-dev here, not require):
use EzPhp\Notification\Channel\RateLimitedChannel; use EzPhp\RateLimiter\ArrayDriver; $throttledMail = new RateLimitedChannel( channel: new MailChannel(), limiter: new ArrayDriver(), maxAttempts: 5, decaySeconds: 3600, keyResolver: fn ($notifiable, $notification) => 'mail:' . $notifiable->routeNotificationFor('mail'), );
Register $throttledMail wherever a plain MailChannel would otherwise be wired into NotificationServiceProvider/Notifier. When the limit is hit, the notification is silently dropped for that attempt — it is not queued or retried.
Multi-channel
Return multiple channels from via() and implement the matching interfaces:
class OrderShippedNotification implements NotificationInterface, ToMailInterface, ToBroadcastInterface, ToDatabaseInterface { public function via(): array { return ['mail', 'broadcast', 'database']; } // toMail(), broadcastOn(), broadcastAs(), broadcastWith(), toDatabase() ... }
Async delivery (queue)
Add ShouldQueueInterface to defer mail and broadcast channels via the queue:
use EzPhp\Notification\ShouldQueueInterface; class WelcomeNotification implements NotificationInterface, ShouldQueueInterface, ToMailInterface { // ... }
When QueueInterface is bound (i.e. QueueServiceProvider is registered), the Notifier
pushes SendMailNotificationJob / SendBroadcastNotificationJob onto the queue instead of
delivering synchronously. The database channel always runs synchronously.
To force synchronous delivery regardless of ShouldQueueInterface:
Notification::sendNow($user, new WelcomeNotification());
Service Provider wiring
NotificationServiceProvider registers the following channels automatically:
| Channel | Requires | Optional |
|---|---|---|
mail |
MailServiceProvider |
— |
broadcast |
BroadcastServiceProvider |
— |
database |
DatabaseServiceProvider |
yes — omitted if not bound |
Queue support (ShouldQueueInterface) is also optional — if QueueServiceProvider is not
registered, all notifications are sent synchronously.
Testing
Inject a real Notifier with spy channels in your tests:
use EzPhp\Notification\Notification; use EzPhp\Notification\Notifier; protected function setUp(): void { $this->spyChannel = new SpyChannel(); Notification::setNotifier(new Notifier(['mail' => $this->spyChannel])); } protected function tearDown(): void { Notification::resetNotifier(); }