Relying on a single SMS provider introduces a single point of failure. This guide demonstrates building a multi-provider SMS gateway system in Laravel that supports Twilio, Vonage (formerly Nexmo), AWS SNS, and custom providers — with automatic fallback between gateways.
The strategy pattern is ideal for this use case. Define a common interface, implement it for each provider, and use Laravel’s service container to resolve the appropriate gateway at runtime.
Start with a clean interface:
<?php
namespace App\Contracts;
use App\DataTransferObjects\SmsMessage;
use App\DataTransferObjects\SmsResult;
interface SmsGateway
{
public function send(SmsMessage $message): SmsResult;
public function getName(): string;
}
---
Define DTOs for the message and result:
```php
<?php
namespace App\DataTransferObjects;
class SmsMessage
{
public function __construct(
public readonly string $to,
public readonly string $content,
public readonly ?string $from = null,
public readonly ?array $options = [],
) {}
public static function fromArray(array $data): self
{
return new self(
to: $data['to'],
content: $data['content'],
from: $data['from'] ?? null,
options: $data['options'] ?? [],
);
}
}
---
```php
<?php
namespace App\DataTransferObjects;
class SmsResult
{
public function __construct(
public readonly bool $success,
public readonly string $provider,
public readonly ?string $messageId = null,
public readonly ?string $error = null,
public readonly array $raw = [],
) {}
}
---
## Implementing TwilioGateway
```php
<?php
namespace App\Services\SmsGateways;
use App\Contracts\SmsGateway;
use App\DataTransferObjects\SmsMessage;
use App\DataTransferObjects\SmsResult;
use Twilio\Rest\Client;
class TwilioGateway implements SmsGateway
{
protected Client $client;
protected string $fromNumber;
public function __construct()
{
$this->client = new Client(
config('gateways.twilio.account_sid'),
config('gateways.twilio.auth_token')
);
$this->fromNumber = config('gateways.twilio.from_number');
}
public function send(SmsMessage $message): SmsResult
{
try {
$response = $this->client->messages->create(
$message->to,
[
'from' => $message->from ?? $this->fromNumber,
'body' => $message->content,
] + ($message->options ?? [])
);
return new SmsResult(
success: true,
provider: $this->getName(),
messageId: $response->sid,
raw: $response->toArray(),
);
} catch (\Exception $e) {
return new SmsResult(
success: false,
provider: $this->getName(),
error: $e->getMessage(),
);
}
}
public function getName(): string
{
return 'twilio';
}
}
---
## Implementing VonageGateway
```php
<?php
namespace App\Services\SmsGateways;
use App\Contracts\SmsGateway;
use App\DataTransferObjects\SmsMessage;
use App\DataTransferObjects\SmsResult;
use Vonage\Client as VonageClient;
use Vonage\Client\Credentials\Basic;
use Vonage\SMS\Message\SMS;
class VonageGateway implements SmsGateway
{
protected VonageClient $client;
protected string $fromNumber;
public function __construct()
{
$basic = new Basic(
config('gateways.vonage.api_key'),
config('gateways.vonage.api_secret')
);
$this->client = new VonageClient($basic);
$this->fromNumber = config('gateways.vonage.from_number');
}
public function send(SmsMessage $message): SmsResult
{
try {
$response = $this->client->sms()->send(
new SMS(
$message->to,
$message->from ?? $this->fromNumber,
$message->content
)
);
$result = $response->current();
return new SmsResult(
success: $result->getStatus() === 0,
provider: $this->getName(),
messageId: $result->getMessageId(),
raw: $result->toArray(),
);
} catch (\Exception $e) {
return new SmsResult(
success: false,
provider: $this->getName(),
error: $e->getMessage(),
);
}
}
public function getName(): string
{
return 'vonage';
}
}
---
## Using Laravel's Service Container for Binding
Register gateways in a service provider:
```php
<?php
namespace App\Providers;
use App\Contracts\SmsGateway;
use App\Services\SmsGateways\TwilioGateway;
use App\Services\SmsGateways\VonageGateway;
use App\Services\SmsGateways\AwsSnsGateway;
use App\Services\SmsManager;
use Illuminate\Support\ServiceProvider;
class SmsServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->bind('gateway.twilio', TwilioGateway::class);
$this->app->bind('gateway.vonage', VonageGateway::class);
$this->app->bind('gateway.aws-sns', AwsSnsGateway::class);
$this->app->singleton(SmsManager::class, function ($app) {
$manager = new SmsManager();
$default = config('gateways.default', 'twilio');
$fallback = config('gateways.fallback');
$manager->setDefault($app->make("gateway.{$default}"));
if ($fallback) {
$manager->setFallback($app->make("gateway.{$fallback}"));
}
return $manager;
});
}
}
---
Configuration file at `config/gateways.php`:
```php
<?php
return [
'default' => env('SMS_GATEWAY', 'twilio'),
'fallback' => env('SMS_FALLBACK_GATEWAY', 'vonage'),
'twilio' => [
'account_sid' => env('TWILIO_ACCOUNT_SID'),
'auth_token' => env('TWILIO_AUTH_TOKEN'),
'from_number' => env('TWILIO_FROM_NUMBER'),
],
'vonage' => [
'api_key' => env('VONAGE_API_KEY'),
'api_secret' => env('VONAGE_API_SECRET'),
'from_number' => env('VONAGE_FROM_NUMBER'),
],
'aws-sns' => [
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_SNS_REGION', 'us-east-1'),
'sender_id' => env('AWS_SNS_SENDER_ID'),
],
];
---
## Automatic Fallback Between Gateways
The `SmsManager` orchestrates primary and fallback gateways:
```php
<?php
namespace App\Services;
use App\Contracts\SmsGateway;
use App\DataTransferObjects\SmsMessage;
use App\DataTransferObjects\SmsResult;
use Illuminate\Support\Facades\Log;
class SmsManager implements SmsGateway
{
protected SmsGateway $default;
protected ?SmsGateway $fallback = null;
public function setDefault(SmsGateway $gateway): void
{
$this->default = $gateway;
}
public function setFallback(?SmsGateway $gateway): void
{
$this->fallback = $gateway;
}
public function send(SmsMessage $message): SmsResult
{
$result = $this->default->send($message);
if ($result->success) {
return $result;
}
Log::warning('Primary SMS gateway failed', [
'gateway' => $this->default->getName(),
'error' => $result->error,
]);
if ($this->fallback) {
$fallbackResult = $this->fallback->send($message);
Log::info('Fallback SMS gateway used', [
'gateway' => $this->fallback->getName(),
'success' => $fallbackResult->success,
]);
return $fallbackResult;
}
return $result;
}
public function getName(): string
{
return $this->default->getName();
}
}
---
### Usage in Application Code
```php
use App\DataTransferObjects\SmsMessage;
use App\Services\SmsManager;
$manager = app(SmsManager::class);
$message = new SmsMessage(
to: '+15559876543',
content: 'Your appointment is confirmed for tomorrow at 2 PM.',
from: '+15551234567',
);
$result = $manager->send($message);
if ($result->success) {
echo "Sent via {$result->provider}, ID: {$result->messageId}";
} else {
echo "Failed: {$result->error}";
}
---
### Selecting a Specific Gateway
When you need to force a specific provider (e.g., for cost routing):
```php
// Direct usage without fallback
$twilio = app('gateway.twilio');
$result = $twilio->send($message);
// Or via a helper
public function sendVia(string $gateway, SmsMessage $message): SmsResult
{
return app("gateway.{$gateway}")->send($message);
}
---
## Testing with Mock Gateways
Create a fake gateway for testing:
```php
<?php
namespace App\Services\SmsGateways;
use App\Contracts\SmsGateway;
use App\DataTransferObjects\SmsMessage;
use App\DataTransferObjects\SmsResult;
class FakeGateway implements SmsGateway
{
public bool $shouldFail = false;
public function send(SmsMessage $message): SmsResult
{
if ($this->shouldFail) {
return new SmsResult(
success: false,
provider: $this->getName(),
error: 'Simulated failure',
);
}
return new SmsResult(
success: true,
provider: $this->getName(),
messageId: 'fake_' . uniqid(),
);
}
public function getName(): string
{
return 'fake';
}
}
---
Swap gateways in tests:
```php
<?php
namespace Tests\Feature;
use App\Contracts\SmsGateway;
use App\Services\SmsGateways\FakeGateway;
use App\Services\SmsManager;
use Tests\TestCase;
class SmsSendingTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
$fake = new FakeGateway();
$manager = app(SmsManager::class);
$manager->setDefault($fake);
}
public function test_sends_sms_successfully(): void
{
$this->post('/send-sms', [
'to' => '+15559876543',
'message' => 'Test message',
])->assertOk();
}
}
---
### Testing Fallback Logic
```php
public function test_fallback_gateway_is_used_when_primary_fails(): void
{
$primary = new FakeGateway();
$primary->shouldFail = true;
$fallback = new FakeGateway();
$manager = app(SmsManager::class);
$manager->setDefault($primary);
$manager->setFallback($fallback);
$result = $manager->send(
new SmsMessage('+15559876543', 'Hello')
);
$this->assertTrue($result->success);
$this->assertEquals('fake', $result->provider);
}
---
## Related Articles
- [Twilio SMS Integration with Laravel (Complete Guide 2026)](/blog/laravel-twilio-sms-integration)
- [Building a Bulk SMS Platform with Laravel](/blog/building-bulk-sms-platform-laravel)
- [Laravel Custom SMS Channel for Notifications](/blog/laravel-custom-sms-channel)