symfony/twilio-notifier
Send SMS messages via Twilio from your Symfony app with the Twilio Notifier bridge. Configure it using a simple DSN (twilio://SID:TOKEN@default?from=FROM) and attach extra options like webhook URLs to individual messages through the TwilioOptions class.
This is a Symfony Notifier bridge, so in Laravel you are not pulling in a Laravel notification channel — you are running symfony/notifier standalone and letting this bridge supply the Twilio transport. Treat it as a small, framework-agnostic SMS library.
Install:
composer require symfony/twilio-notifier symfony/notifier
Add the credential to .env (the DSN is the whole config — there is no config/twilio.php in this package):
TWILIO_DSN="twilio://SID:TOKEN@default?from=+15551234567"
Wire it once in a service provider. Transport::fromDsn() auto-discovers the Twilio factory from the installed bridge:
// app/Providers/TwilioNotifierServiceProvider.php
public function register(): void
{
$this->app->singleton(\Symfony\Component\Notifier\Texter::class, function ($app) {
$transport = \Symfony\Component\Notifier\Transport::fromDsn(
config('services.twilio.dsn'),
null, // Symfony EventDispatcher (optional)
null, // Symfony HttpClient (defaults to HttpClient::create())
$app->make(\Psr\Log\LoggerInterface::class),
);
return new \Symfony\Component\Notifier\Texter($transport);
});
}
First use case — an SMS from a queued job or listener:
$sms = new SmsMessage('+14155550123', 'Your order #1042 has shipped.');
$texter->send($sms);
Look here first, in order: TwilioTransportFactory (to see exactly which DSN params are accepted), TwilioTransport (how the API call is built and which exceptions are thrown), then TwilioOptions (the per-message knobs). Skip the Symfony framework docs — you only need the Notifier component's Texter/SmsMessage surface.
Bind an interface, not Texter. The single highest-value move for day-to-day use is to hide Texter behind your own contract so tests, jobs, and console commands never touch Symfony classes:
interface SmsSender { public function send(string $to, string $body, ?TwilioOptions $options = null): void; }
Implement it with the Texter, and swap in a null/logging implementation in testing.
Per-message options are fluent and transport-specific. Options are attached to the message, not to the transport, so you set them at the call site:
use Symfony\Component\Notifier\Bridge\Twilio\TwilioOptions;
$sms = new SmsMessage($to, $body);
$sms->options(
(new TwilioOptions())->webhookUrl(route('webhooks.twilio.status'))
);
$texter->send($sms);
Options must be a TwilioOptions instance — a generic MessageOptionsInterface from another bridge will be rejected at send time.
Queue at the Laravel layer, not the Symfony layer. The transport is synchronous and has no retry, so wrap the send in a job with $tries, backoff(), and ShouldBeUnique where relevant. Do not try to reuse Symfony Messenger for this.
final class SendSms implements ShouldQueue
{
use Queueable;
public int $tries = 3;
public array $backoff = [10, 60, 300];
public function handle(SmsSender $sms): void { $sms->send($this->to, $this->body); }
}
Model notifications as plain classes/Markdown, not SmsMessage. Keep SmsMessage construction inside the adapter. Your app code should pass a domain object or rendered string; the adapter handles E.164 normalisation, truncation, and options. That keeps the Symfony dependency to one file.
Inbound/status webhooks. The bridge ships a TwilioRequestParser used to validate and parse callbacks. In Laravel, expose a route, exclude it from CSRF in bootstrap/app.php ($middleware->validateCsrfTokens(except: ['webhooks/twilio/*'])), and validate the HMAC signature before trusting any field. Pass the secret from config — do not derive it at request time.
Events. If you want observability, pass an event dispatcher into Transport::fromDsn() and listen for the Notifier's sent/failed message events. Laravel's dispatcher is not PSR-14 compliant, so either construct a symfony/event-dispatcher instance for this purpose only, or — simpler — log from your adapter around $texter->send().
laravel-notification-channels/twilio is far less friction. Reach for this bridge when you specifically want the Notifier abstraction (multiple transports behind one DSN-driven interface, TwilioOptions, or an existing Symfony codebase).from query param is mandatory. A DSN without ?from= fails at transport construction, not at send time — so a boot-time resolution (see the singleton above) surfaces misconfiguration early, which is what you want.@, /, or :, the DSN must be percent-encoded or the parser will split it wrong.default in the DSN is just the transport name. It is not a Twilio concept; it means "unnamed transport". Multi-transport setups use notifier://twilio://... style routing, which you almost certainly don't need in a Laravel app.TrustProxies or a forced-scheme mismatch produces valid requests that fail validation. Log the URL you validated against when debugging.TransportExceptionInterface, not Laravel's Http exceptions. Catch Symfony\Component\Notifier\Exception\TransportExceptionInterface (and the more specific FailedMessageException) and rethrow as a queued-job failure so retries apply. Rate limits and trial-account rejections both land here.Http::fake() will not intercept these calls. For tests, either fake your SmsSender interface (recommended) or inject MockHttpClient from symfony/http-client into Transport::fromDsn().SmsSender implementation or supply a custom HttpClientInterface — don't fork the bridge.How can I help you explore Laravel packages today?