The communication capability your app uses to reach people — email, SMS, and push — through a choice of providers, with a fluent sending API and optional async queueing.
ForgeNotification gives your app one consistent way to send outbound messages, no matter the channel. Email, SMS, and push are modeled as channels; each channel routes to a concrete provider that actually delivers the message.
Two layers keep things separate and swappable:
You can send right away (synchronously) or hand the message to the event system to send asynchronously — the same builder describes both, so switching is natural.
Framing: this is a component of your app, not a framework mailer. You configure a provider or two, describe your messages with the fluent API, and decide sync or async — the delivery details stay out of your way.
php forge.php package:install-module --module=forge-notification
The capability relies on the event system to run its async queue, so that capability should be present too. There's no database work required on install — message history is left to your own tables.
You pick a default channel and, for each channel, a default provider plus that provider's credentials. Everything is driven by a small set of environment variables:
| Variable | Default | Effect |
|---|---|---|
NOTIFICATION_DEFAULT_CHANNEL |
email |
Which channel is considered the default |
NOTIFICATION_QUEUE_ENABLED / ..._QUEUE_NAME |
true / notifications |
Whether async sending is on, and the queue it uses |
NOTIFICATION_QUEUE_PRIORITY / ..._MAX_RETRIES / ..._DELAY |
normal / 3 / 0s |
Queue priority, retry count, and delay for queued sends |
NOTIFICATION_EMAIL_PROVIDER |
smtp |
Default email provider |
SMTP_HOST / SMTP_PORT |
localhost / 1025 |
SMTP server and port (1025 suits local mail catchers like Mailpit) |
SMTP_USERNAME / SMTP_PASSWORD |
— | Optional SMTP login (skipped when empty) |
SMTP_ENCRYPTION |
none |
TLS (tls or ssl) or plain — most servers use tls |
SMTP_FROM_ADDRESS / SMTP_FROM_NAME |
noreply@localhost / Forge |
Default sender when a message doesn't set its own |
NOTIFICATION_SMS_PROVIDER |
twilio |
Default SMS provider |
TWILIO_ACCOUNT_SID / TWILIO_AUTH_TOKEN / TWILIO_FROM |
— | Twilio credentials and the sending number |
NOTIFICATION_PUSH_PROVIDER |
firebase |
Default push provider |
The provider list is a plain map keyed per channel. That same config also declares SendGrid and Mailgun (email), Vonage (SMS), and Firebase and OneSignal (push) — you can wire those up by swapping the default provider — but only SMTP and the Twilio SMS provider ship working implementations today, so those are the two you can rely on out of the box.
Each channel is an object you reach through the
notification service (or directly through the channel
manager). They all share the same shape: build it up
with fluent calls, then send() or
queue().
The workhorse channel. Accepts recipients,
subject, plain-text and HTML bodies, sender,
CC/BCC, attachments, and reply-to. When you
give HTML but no plain text, the plain
text/body you supply
is used for the text part; with both, the
message is sent as a multi-part alternative.
Short text messages sent to one or more phone numbers through a provider like Twilio. Takes a recipient, the message, and an optional sender.
Mobile/desktop push notifications. Takes recipient device tokens or user IDs, a title, a body, and optional presentation extras — badge, sound, icon, image, click action — plus a custom data payload.
Picking a provider: use
via('provider') on a channel to route
that one message through a specific provider,
overriding the channel's default.
The most direct path is the fluent API. Resolve the
notification service, pick a channel with
email(), sms(), or
push(), describe the message, and call
send():
use Forge\Core\Contracts\NotificationInterface;
$notifications = $container->get(NotificationInterface::class);
$notifications->email()
->to('ada@example.com')
->subject('Welcome to Acme')
->html('<h1>Hello Ada</h1><p>Thanks for joining.</p>')
->text('Hello Ada, thanks for joining.')
->send();
Email also supports cc(),
bcc(), replyTo(),
from(), and attachments().
SMS takes message(), push takes
title() plus the extras above:
$notifications->sms()
->to('+15551234567')
->message('Your code is 123456')
->send();
$notifications->push()
->to('device-token-abc')
->title('New message')
->body('Ada replied to your post')
->data(['post_id' => 42])
->send();
send() returns a boolean so you can react
to success or failure directly in your code. Required
fields are enforced — for example, an email without a
recipient, or a push without a title or body, throws
before anything is attempted.
When you'd rather describe a message as data than with
a chain of calls, build a typed payload and send it
through the channel plus send(). This
form is handy when your messages are generated from
records or templates:
use Modules\ForgeNotification\Enums\NotificationChannel;
use Modules\ForgeNotification\Payload\EmailPayload;
use Modules\ForgeNotification\Payload\SmsPayload;
use Modules\ForgeNotification\Payload\PushPayload;
$email = new EmailPayload(
to: 'ada@example.com',
subject: 'Your receipt',
text: 'Thanks for your order.',
from: 'billing@acme.com',
via: 'smtp',
);
$notifications->send(NotificationChannel::email, $email);
Three payload classes mirror the channels:
EmailPayload (to, subject, html, text,
from, cc, bcc, replyTo, attachments, via),
SmsPayload (to, message, from, via), and
PushPayload (to, title, body, data, via).
The via field, when set, selects the
provider just like the fluent
via() call.
A global sendNotification($channel,
$payload) helper wraps this resolution for
one-liners, and a
SendsNotifications trait drops the same
method onto your own classes. Any failure is caught
and routed to the error handler rather than thrown.
For work that shouldn't block a request — welcome
mail, receipt texts, digests — hand the message to the
event queue instead of sending immediately. Call
queue() on a built-up channel:
$notifications->email()
->to('ada@example.com')
->subject('Welcome to Acme')
->html('<h1>Hello Ada</h1>')
->queue();
Or pass a typed DTO to the queueing methods on the
service, queueEmail(),
queueSms(), or queuePush().
Queued messages become events on the
notifications queue. A listener picks each
one up and routes it to its channel for delivery.
Through the queue capabability you control retries
(default 3), priority (default normal), and any initial
delay — so a busy provider or a momentary failure
doesn't lose a message.
One builder, two behaviors: the
exact same description becomes a synchronous call
with send() or an async job with
queue(). Your message logic stays
unchanged when you switch a message to run in the
background.
The notification service registers itself under the
NotificationInterface contract, so your
code can depend on the interface and stay free of
module internals. Resolve it and send:
use Forge\Core\Contracts\NotificationInterface;
public function __construct(
private readonly NotificationInterface $notifications,
) {}
public function sendWelcome(string $email): void
{
$this->notifications->email()
->to($email)
->subject('Welcome!')
->html('<p>Thanks for signing up.</p>')
->queue();
}
For custom delivery logic you can work with the channel manager and provider resolver directly, but for nearly all app code the interface plus the fluent API is all you need. Where you store sent messages for your own record — a notifications or outbox table — is entirely up to you.