1naturalway / laravel-sms
A driver-based SMS abstraction layer for Laravel, following the MailManager pattern.
Requires
- php: ^8.4 || ^8.5
- illuminate/contracts: ^12.0 || ^13.0
- illuminate/http: ^12.0 || ^13.0
- illuminate/log: ^12.0 || ^13.0
- illuminate/support: ^12.0 || ^13.0
Requires (Dev)
- larastan/larastan: ^3.9
- mockery/mockery: ^1.4.4
- orchestra/testbench: ^10.0 || ^11.0
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^11.0
- squizlabs/php_codesniffer: ^4.0
- twilio/sdk: ^8.11
Suggests
- twilio/sdk: Required to use the Twilio SMS driver (^8.0).
This package is auto-updated.
Last update: 2026-03-29 13:32:45 UTC
README
A driver-based SMS abstraction layer for Laravel, following the same architectural pattern as Laravel's built-in Mail system. Swap between Twilio, logging, or a null driver with a single config change.
Installation
composer require 1naturalway/laravel-sms
Publish the config file:
php artisan vendor:publish --tag=sms-config
Configuration
Set the driver and credentials in your .env file:
# Choose your driver: twilio, log, null SMS_DRIVER=null # Default "from" number (used by drivers that don't define their own) SMS_FROM=+15551234567
Twilio
SMS_DRIVER=twilio TWILIO_SID=your-account-sid TWILIO_TOKEN=your-auth-token TWILIO_FROM=+15551234567
Requires the Twilio SDK:
composer require twilio/sdk
Log
SMS_DRIVER=log SMS_LOG_CHANNEL=stack
Writes every SMS to a Laravel log channel. Useful for debugging and verifying sends in staging or CI.
Null
SMS_DRIVER=null
Silently discards all messages. This is the default — no accidental sends in development.
Usage
Basic Send
use OneNaturalWay\Sms\Facades\Sms; $result = Sms::send('+15559876543', 'Your verification code is 123456'); if ($result->successful()) { // Store $result->messageId for tracking // Check $result->status // For MMS: $result->hasMedia(), $result->mediaUrls }
Every send() call returns an SmsResult DTO with these properties:
| Property | Type | Description |
|---|---|---|
messageId |
?string |
Provider message ID (e.g., Twilio SID) |
status |
?string |
Provider status (e.g., queued, captured, logged) |
to |
?string |
Recipient number |
from |
?string |
Sender number |
body |
?string |
Message body |
mediaCount |
?int |
Number of media attachments (Twilio) |
mediaUrls |
array |
URLs of attached media (Twilio MMS) |
raw |
array |
Full raw response from the provider |
Use $result->successful() to check if the message was accepted — returns true when messageId is present. Use $result->hasMedia() to check if media was attached. The null driver always returns an unsuccessful result since nothing was sent.
Error Handling
The Twilio driver wraps API errors into typed SmsException instances so you can handle specific failure modes:
use OneNaturalWay\Sms\Exceptions\SmsException; try { $result = Sms::send('+15551234567', 'Hello!'); } catch (SmsException $e) { if ($e->isBlacklisted()) { // Number opted out / on a blacklist — stop sending to this number } if ($e->isInvalidNumber()) { // Bad phone number format — flag for correction } // All SmsExceptions carry context: // $e->errorType — 'blacklisted', 'invalid_number', or 'provider_error' // $e->phoneNumber — the "to" number that failed // $e->providerCode — the raw error code from Twilio (e.g., '21610') // $e->getPrevious() — the original Twilio RestException }
SmsException extends RuntimeException, so uncaught errors will still surface normally in your exception handler.
With Options
$result = Sms::send('+15559876543', 'Hello!', [ 'from' => '+15550001111', // Override the default "from" number 'mediaUrl' => 'https://example.com/image.jpg', // Twilio MMS ]);
Driver Switching
// Use a specific driver for this call $result = Sms::driver('log')->send('+15559876543', 'This goes to the log'); // Use the default driver $result = Sms::send('+15559876543', 'This uses the configured default');
Dependency Injection
use OneNaturalWay\Sms\SmsManager; class NotificationService { public function __construct( protected SmsManager $sms, ) {} public function sendWelcome(string $phone): void { $result = $this->sms->send($phone, 'Welcome to our service!'); if ($result->successful()) { logger()->info('Welcome SMS queued', ['sid' => $result->messageId]); } } }
Testing
The package ships with a FakeSmsProvider that captures all messages in memory, just like Laravel's Mail::fake().
use OneNaturalWay\Sms\Facades\Sms; public function test_sends_verification_sms(): void { $fake = Sms::fake(); // ... trigger your code that sends SMS ... $fake->assertSentTo('+15559876543'); $fake->assertSentTo('+15559876543', 'verification code'); $fake->assertSentCount(1); } public function test_no_sms_sent_on_invalid_input(): void { $fake = Sms::fake(); // ... trigger code that should NOT send SMS ... $fake->assertNothingSent(); } public function test_sms_body_matches_pattern(): void { $fake = Sms::fake(); // ... trigger your code ... $fake->assertSentToWithBody('+15559876543', function (SmsResult $result) { return preg_match('/\d{6}/', $result->body) === 1; }); }
Available Assertions
| Method | Description |
|---|---|
assertSentTo($number, $bodyContains?) |
A message was sent to this number, optionally containing text |
assertNothingSent() |
No messages were sent |
assertSentCount($n) |
Exactly N messages were sent |
assertSentToWithBody($number, $callback) |
A message to this number passes the callback (receives SmsResult) |
getSent() |
Returns an array of SmsResult objects |
Custom Drivers
Register a custom driver using extend(), typically in a service provider's boot() method:
use OneNaturalWay\Sms\Facades\Sms; use OneNaturalWay\Sms\Contracts\SmsProvider; Sms::extend('vonage', function ($app, $config) { return new VonageSmsProvider( apiKey: $config['api_key'], apiSecret: $config['api_secret'], from: $config['from'] ?? $app['config']['sms.from'], ); });
Add the driver config to config/sms.php:
'drivers' => [ // ... existing drivers ... 'vonage' => [ 'api_key' => env('VONAGE_API_KEY'), 'api_secret' => env('VONAGE_API_SECRET'), 'from' => env('VONAGE_FROM'), ], ],
Your custom provider must implement the SmsProvider interface:
use OneNaturalWay\Sms\Contracts\SmsProvider; use OneNaturalWay\Sms\SmsResult; class VonageSmsProvider implements SmsProvider { public function send(string $to, string $body, array $options = []): SmsResult { // Your implementation — return an SmsResult with the provider's response data } }
UAT Strategy
For UAT environments, we recommend Log driver:
- Log writes messages to your Laravel log files for easy inspection. Use
SMS_DRIVER=logwhen you need to verify sends in CI or staging. - Null (the default) ensures no SMS is sent in development or CI unless explicitly configured.
This layered approach guarantees:
- Production uses Twilio (or your chosen carrier) via
SMS_DRIVER=twilio - UAT uses logs to capture and inspect messages
- Development and CI use
nullorlog— zero accidental sends
License
MIT