1naturalway/laravel-sms

A driver-based SMS abstraction layer for Laravel, following the MailManager pattern.

Maintainers

Package info

github.com/1naturalway/laravel-sms

pkg:composer/1naturalway/laravel-sms

Statistics

Installs: 19

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.0 2026-03-29 13:22 UTC

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=log when 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:

  1. Production uses Twilio (or your chosen carrier) via SMS_DRIVER=twilio
  2. UAT uses logs to capture and inspect messages
  3. Development and CI use null or log — zero accidental sends

License

MIT