Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Notifier Laravel Package

symfony/notifier

Symfony Notifier sends notifications through multiple channels like email and SMS, with a flexible, extensible architecture for adding custom transports and integrations.

View on GitHub
Deep Wiki
Context7

Getting Started

symfony/notifier is a standalone, framework-agnostic component — there is nothing Laravel-specific to install, and no Symfony app is required. Treat it as a provider-agnostic message transport layer that you wire into Laravel's container yourself.

1. Install the core plus the bridges you actually use. The core package ships zero transports; every channel is a separate symfony/*-notifier package:

composer require symfony/notifier symfony/http-client
composer require symfony/slack-notifier      # chat
composer require symfony/twilio-notifier     # sms
composer require symfony/brevo-notifier      # sms + email alternative
composer require symfony/mercure-notifier    # push / realtime

Pin symfony/notifier and every bridge to the same minor line. Bridges lag the core (the Novu bridge was pinned to notifier 6.x and unusable on 7.x until 6.4.12 fixed it), and the php constraint of Symfony 8 (PHP >= 8.4) will silently exclude bridges still declaring PHP 8.1.

2. First place to look: Symfony\Component\Notifier\Transport (the concrete class, not TransportInterface). It is the DSN router. Everything — failover, round-robin, named transports — is built there, and Transport::getSupportedSchemes() tells you exactly which bridges are installed and discoverable.

3. Minimum viable send. The fastest correct path is a typed message + a Transport::fromDsn(). This skips channels entirely and goes straight to one transport:

use Symfony\Component\Notifier\Chatter;
use Symfony\Component\Notifier\Transport;
use Symfony\Component\Notifier\Message\ChatMessage;

$chatter = new Chatter(Transport::fromDsn('slack://xoxb-token@default?channel=%23alerts'));
$chatter->send(new ChatMessage('Deploy 3.4.1 is live'));

Chatter, Texter (SMS), and Mailer-style classes each wrap a TransportInterface and give you a one-channel façade — that is usually all a Laravel app needs for a first use case: "alert the ops Slack channel when a deploy or a failed job happens."

4. Bind it once in a service provider. Use Laravel config, not env, so config:cache cannot bite you:

// config/notifier.php
return [
    'transports' => [
        'slack'  => env('NOTIFIER_SLACK_DSN'),
        'twilio' => env('NOTIFIER_TWILIO_DSN'),
    ],
];
// app/Providers/NotifierServiceProvider.php
public function register(): void
{
    $this->app->singleton(TransportInterface::class, fn () => Transport::fromDsn(
        config('notifier.transports.slack'),
    ));

    $this->app->singleton(Chatter::class, fn ($app) => new Chatter($app->make(TransportInterface::class)));
}

Register one named transport per business purpose (ops, billing, oncall) rather than one per provider — you want to swap Twilio for Brevo without touching call sites.

Implementation Patterns

Pattern A — typed messages for one-off sends. This is the workhorse. Each message type carries its own options object, so provider-specific behaviour is explicit rather than stringly-typed:

use Symfony\Component\Notifier\Message\SmsMessage;
use Symfony\Component\Notifier\Bridge\Twilio\TwilioOptions;

$sms = new SmsMessage('+15551234567', 'Your code is 1234');
$sms->options(new TwilioOptions(/* provider-specific flags */));
$texter->send($sms);

Pattern B — Notification + Recipient for fan-out. Use this when one business event must reach chat and SMS and email. The Notification is a value object; channels are resolved per recipient:

use Symfony\Component\Notifier\Notification\Notification;
use Symfony\Component\Notifier\Recipient\Recipient;

$notification = (new Notification('Invoice overdue', ['chat', 'sms', 'email']))
    ->content('Invoice #8821 is 7 days overdue.')
    ->importance(Notification::IMPORTANCE_HIGH)
    ->emoji('⚠️');

$notifier->send($notification, new Recipient('[email protected]', '+15551234567'));

Use EmailRecipient or SmsRecipient when the recipient only has one contact detail — Recipient's constructor requires both.

Pattern C — a Laravel notification channel (best day-to-day fit). Don't fight Illuminate\Notifications; extend it. A custom channel gives you Laravel's queueing, via() per-notifiable routing, ShouldQueue, and database logs for free, while delegating delivery to Notifier:

namespace App\Notifications\Channels;

use Illuminate\Notifications\Notification as LaravelNotification;
use Symfony\Component\Notifier\Chatter;
use Symfony\Component\Notifier\Message\ChatMessage;

class ChatChannel
{
    public function __construct(private Chatter $chatter) {}

    public function send(object $notifiable, LaravelNotification $notification): void
    {
        if (!method_exists($notification, 'toChatMessage')) {
            return;
        }

        /** @var ChatMessage $message */
        $message = $notification->toChatMessage($notifiable);
        $this->chatter->send($message);
    }
}

Then via() returns [ChatChannel::class, 'mail'], and implements ShouldQueue makes it asynchronous through Laravel workers — no Messenger involved.

Pattern D — failover and round-robin via DSN composition. The transport layer composes providers for you:

// backup provider if the primary errors
'failover(twilio://SID:TOKEN@default?from=%2B15550000000 brevo://APIKEY@default?from=15550000000)'

// load-balance across two SMS providers
'roundrobin(twilio://... brevo://...)'

FailoverTransport retries on TransportExceptionInterface and rethrows only the last exception; RoundRobinTransport rotates per send. This is the cheapest resilience you will get without writing provider-specific retry code.

Pattern E — observe sends with events. Pass a Symfony\Component\EventDispatcher\EventDispatcher into Transport::fromDsn() and listen for MessageEvent, SentMessageEvent, and FailedMessageEvent. Bridge events into Laravel's dispatcher in the provider so the rest of the app listens to SentMessageEvent like any other Laravel event — this is where you centralise metrics, audit logs, and per-provider error budgets:

$symfony = new EventDispatcher();
$symfony->addListener(SentMessageEvent::class, fn (SentMessageEvent $e) =>
    event(new \App\Events\NotificationDelivered($e->getMessage()))
);

Pattern F — custom internal transport. If you have an in-house gateway, implement TransportInterface (send(), supports(), __toString()) and hand the instance to Notifier/Chatter instead of a DSN. Keep it behind the same TransportInterface contract so swapping it for a bridge later is a config change. For DSN-based custom transports, implement AbstractTransport + TransportFactoryInterface.

Pattern G — recipient resolution. Store transport identifiers on the user model (slack_user_id, phone_e164, telegram_chat_id) and build the Notifier Recipient/options per channel in one small RecipientResolver service. Keeping phone/email normalisation in one place prevents the "some numbers are +1 (555)… and Twilio silently drops them" class of bug.

Gotchas and Tips

Each bridge is its own package, and removals are real. Gitter was removed in 7.2, Sms77 deprecated in 7.3 and removed in 8.0. Check the bridge's release notes before you build a feature on it; prefer providers Symfony actively back (Brevo, Sweego, Twilio, Slack, Telegram).

UnsupportedSchemeException does not always mean "package missing". Until v8.1.2 the Prelude bridge was omitted from the scheme list, so a perfectly installed bridge could still be rejected. If a scheme is genuinely installed and still throws, Transport::getSupportedSchemes() is your source of truth — and a core upgrade may be the fix.

DSNs are URLs, so escape them. # in a Slack channel must be %23; + in a phone number must be %2B. Laravel's env() will happily pass an unescaped # through and turn the rest of your channel name into a URL fragment. Always quote env values and validate the DSN once at boot with Transport::fromDsn() in a test.

Specify channels explicitly on Notification. An empty channel list throws LogicException: The given notification has no channels to be notified to. — there is no sensible default. Related: the channel policy can skip channels for low-importance notifications; if a notification "sends" but nothing arrives, dump $notification->getChannels($recipient) and check ChannelPolicy/EmailChannelPolicy before blaming the provider.

No retries, no rate limiting, no circuit breaker. The component sends once and throws TransportExceptionInterface. Add retry() and RateLimiter around your Chatter/Texter calls, and catch TransportExceptionInterface explicitly — it is not a Laravel Exception, so blanket catch (\Exception) handlers may swallow it into a generic 500 without provider context.

Async is your problem in Laravel. Notifier's async story is symfony/messenger (MessageBusInterface) — which does not exist in a Laravel app. Do not try to bridge Messenger; queue the enclosing Laravel job or notification (ShouldQueue) instead. The only Notifier-side async machinery you'd care about is SendFailedMessageToNotifierListener, and that listener had a real bug rejecting any notifier but the concrete one — fixed in 7.4.17 / 8.1.5. Upgrade if you route failures back through it.

Long-running workers leak on older 7.4/8.1. Bug #65274 ("Avoid retaining messages without the profiler") means SentMessage objects are retained in memory when the profiler is off — exactly the case in queue:work. Pin >= 7.4.17 or >= 8.1.5 for any queue worker that sends notifications.

Provider quirks surface as bridge bugs, not your bugs. Telegram markdown escaping (6.4.13), Discord value limits (6.4.21), Clicksend transport (7.3.0-RC1), Twitter INIT upload (7.2.0-RC1). Keep markdown minimal on Telegram, and truncate Discord/Slack field values yourself rather than trusting the bridge to clip them.

Testing: write a fake transport, not HTTP fakes. TransportFactoryTestCase was deprecated in 7.2 and removed in 8.0, so the Symfony test scaffolding is no longer a dependency you can lean on. In Laravel, bind an ArrayTransport implementing TransportInterface that records messages and returns new SentMessage($message, 'array'), then assert with assertCount(1, $transport->messages). Far less brittle than Http::fake() against undocumented provider payloads.

SentMessage carries more than success. Use getMessageId() for correlation, and check bridge-specific additional info (Bluesky returns the record CID, added 7.3) — it is often the only handle you get for reconciling delivery webhooks later.

Extension points, in order of cost: (1) message options() objects for provider flags; (2) DSN composition (failover/roundrobin) for resilience; (3) Symfony event listeners for observability; (4) a custom TransportInterface; (5) a full bridge — only worth it if you're shipping it upstream.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect
youbar/easy-crud
c-point-labs/laravel-hostyourai
dbravoan/dbcms-admin-theme-dbcms
dbravoan/dbcms-theme-db-cyber
dbravoan/dbcms-blocks-basic
dbravoan/dbcms-module-content-audit
dbravoan/dbcms-module-ai-assistant
paygate-to/paygate-php
padosoft/askmydocs-connector-base
defstudio/supercharged-carbon
bernskioldmedia/laravel-matomo