zing/laravel-sms
A Laravel SMS package with a clean, driver-based API for sending text messages through multiple gateways. Configure your providers, compose messages, and dispatch SMS from anywhere in your app with minimal setup and consistent syntax—ideal for OTP codes, alerts, and transactional notifications.
This package plugs into Laravel's existing notification system as a custom channel, so your first move is to treat it as a channel, not as a standalone SMS client.
composer require zingimmick/laravel-sms:^6.7 (the 6.7.0 release adds Laravel 13, PHP 8.5 and PHPUnit 12 support — pin to the 6.x line, see Gotchas). The service provider and Sms facade are auto-discovered. Run php artisan vendor:publish --tag=sms-config to get config/sms.php — this is the first file to read, because it defines the driver map (log, array, ctyun, and whatever gateway drivers the package ships or you register).User (or any model) needs a routeNotificationForSms() method returning the destination number (or an array of numbers, depending on driver). This mirrors routeNotificationForMail() — if it's missing, the channel will fall over silently or throw.php artisan make:notification OrderShipped, then:
public function via($notifiable) { return ['sms']; }
public function toSms($notifiable) { return (new SmsMessage)->content('Order #'.$this->order->id.' shipped.'); }
log driver first. Set SMS_DRIVER=log (or the equivalent config key) and trigger the notification. Check storage/logs/laravel.log — seeing the rendered message there confirms routing, message construction, and channel wiring before you touch a real gateway.Notification-as-the-unit-of-work. The idiomatic pattern is: one notification class per SMS event, via() returns ['sms'] (or [SmsChannel::class] for explicitness), and toSms() returns an SmsMessage. Do not inject a "sender" service into controllers — you lose queueing, retries, and Laravel's Notification::fake() test support.
Routing via routeNotificationForSms(). Centralise number resolution here (E.164 normalisation, fallback to a user profile field, suppression lists). If you support multiple drivers with different destination shapes, return the raw value and let the driver adapt; otherwise normalise early.
Driver registration for custom gateways. The package exposes an Sms::extend('name', fn ($app) => new MyGateway(...)) style hook (verify method name against the README). Register this in a dedicated SmsServiceProvider and bind the gateway's credentials from config/services.php — not config/sms.php — so secrets stay in the standard Laravel location. The 6.7.0 ctyun addition is a good worked example to copy: it ships a config block plus a driver, exactly the shape your own gateway adapter should take.
Queue everything. Implement ShouldQueue on your notification (or set $connection/$queue). SMS gateways are slow and rate-limited; synchronous sends block request workers. Pair this with retryUntil() and backoff() tuned to the gateway's 429 behaviour, not the default.
On-demand notifications work for one-offs (e.g. a welcome SMS before the user model exists): Notification::route('sms', '+15551234567')->notify(new WelcomeSms()). Use this for flows that are genuinely user-agnostic; avoid it as a shortcut in place of proper routing.
Compose messages with a fluent builder. Build SmsMessage in a private helper when templates get long, and keep user-supplied strings out of content() unless escaped — this is plain text, not HTML, so {name} interpolation should be your own explicit str_replace or a small renderer.
Testing pattern. Notification::fake() asserts dispatch without hitting the channel; assert Notification::assertSentTo($user, OrderShipped::class, fn ($n, $channels) => in_array('sms', $channels)). For channel-level tests, bind a fake SmsSender in the container so Sms::driver('log') becomes a spy you can assert against. If your own test suite extends the package's testbench-based scaffolding, note 6.7.0 bumps the orchestra/testbench constraint alongside PHPUnit 12 — align your require-dev before upgrading.
routeNotificationForSms() returning null is a silent-ish failure. The channel typically skips sending or throws a driver-specific exception; either way you get no SMS and a hard-to-spot log line. Add an explicit guard or a test that asserts the method exists on every notifiable you route to.Sms::extend(). Custom driver registrations happen at boot in a service provider, which survives config:cache — but any config values read inside the driver constructor must come from config(), not env(). env() returns null once config is cached; this bites people the first time they deploy.ctyun config block to config/sms.php. If you published the config in an earlier 6.x release, your copy won't gain the new keys — diff your published file against the package's stub after bumping, or vendor:publish --force into a scratch dir and merge by hand.+15551234567), local, or digits-only. Normalise in routeNotificationForSms() and document the expectation in a base notifiable trait so it's not reinvented per model.SmsMessage is likely a thin value object, so a ->length() or ->segments() accessor is easy to add via a small extension.ShouldQueue alone won't pace calls. Use RateLimited middleware or a per-driver ->throttle() if the package exposes it, and set notification backoff() to a few seconds, not the default exponential.Sms::driver('array') (or the package's equivalent) and dump the stored payload — it captures what would have been sent without a network call. If that looks correct, the fault is credentials or gateway config, not your message.Notification::fake() prevents the channel from running, so a test that only asserts the notification was sent will pass even if toSms() throws. Keep at least one integration-style test with a fake sender bound to the container.SmsProvider abstraction alongside it.ctyun driver — with no evident API breaks, but it also means the supported-runtime floor moves with each minor. Keep a ^6.7 constraint, and read the diff before minor bumps; the project is small and single-maintainer, so there's no large ecosystem to catch regressions for you. In particular, verify your CI matrix (PHP version, PHPUnit major, testbench) still resolves before merging the bump.How can I help you explore Laravel packages today?