Laravel’s notification system provides built-in channels for mail, database, broadcast, and Vonage SMS. But when you need to use a different provider or have custom delivery logic, building your own SMS channel gives you full control. This guide walks through creating a flexible, provider-agnostic custom SMS notification channel.
Every notification in Laravel is a class that extends Illuminate\Notifications\Notification. The via method returns an array of channel class names that the notification should be sent through:
public function via(object $notifiable): array
{
return [SmsChannel::class];
}
---
Each channel class must implement a `send` method that receives the notifiable entity and the notification instance. The notifiable entity typically defines a `routeNotificationForSms` method that returns the phone number.
## Creating a Custom Channel Class
Create `app/Notifications/Channels/SmsChannel.php`:
```php
<?php
namespace App\Notifications\Channels;
use App\Contracts\SmsGateway;
use App\DataTransferObjects\SmsMessage;
use Illuminate\Notifications\Notification;
use Illuminate\Support\Facades\Log;
class SmsChannel
{
public function __construct(
protected SmsGateway $gateway
) {}
public function send(object $notifiable, Notification $notification): void
{
$phoneNumber = $notifiable->routeNotificationForSms($notification);
if (!$phoneNumber) {
Log::warning('SMS notification skipped: no phone number', [
'notification' => get_class($notification),
'notifiable' => get_class($notifiable),
]);
return;
}
$message = $notification->toSms($notifiable);
$payload = new SmsMessage(
to: $phoneNumber,
content: $this->getContent($message),
from: $message->from ?? null,
options: $message->options ?? [],
);
$result = $this->gateway->send($payload);
if (!$result->success) {
throw new \RuntimeException(
"SMS delivery failed via {$result->provider}: {$result->error}"
);
}
}
protected function getContent(mixed $message): string
{
if (is_string($message)) {
return $message;
}
if (method_exists($message, 'toText')) {
return $message->toText();
}
if (isset($message->content)) {
return $message->content;
}
throw new \InvalidArgumentException('SMS message must have text content.');
}
}
---
## Routing Notifications to the Channel
The notifiable model needs a `routeNotificationForSms` method:
```php
<?php
namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
class User extends Authenticatable
{
use Notifiable;
public function routeNotificationForSms(Notification $notification = null): ?string
{
return $this->phone_number;
}
}
---
## The `toSms` Method on Notifications
Each notification defines the SMS content in a `toSms` method:
```php
<?php
namespace App\Notifications;
use App\Notifications\Channels\SmsChannel;
use Illuminate\Notifications\Notification;
class OrderConfirmation extends Notification
{
public function __construct(
public string $orderNumber
) {}
public function via(object $notifiable): array
{
return [SmsChannel::class, 'mail'];
}
public function toSms(object $notifiable): object
{
return (object) [
'content' => "Order #{$this->orderNumber} confirmed. Thank you for your purchase!",
'from' => config('services.sms.from_number'),
];
}
}
---
### Using a Message Class
For more structure, create a dedicated message class:
```php
<?php
namespace App\Notifications\Messages;
class SmsMessage
{
public function __construct(
public string $content = '',
public ?string $from = null,
public array $options = [],
) {}
public static function create(string $content = ''): static
{
return new static(content: $content);
}
public function content(string $content): static
{
$this->content = $content;
return $this;
}
public function from(string $from): static
{
$this->from = $from;
return $this;
}
public function options(array $options): static
{
$this->options = $options;
return $this;
}
public function toText(): string
{
return $this->content;
}
}
---
Update the notification:
```php
use App\Notifications\Messages\SmsMessage;
public function toSms(object $notifiable): SmsMessage
{
return SmsMessage::create("Order #{$this->orderNumber} confirmed!")
->from(config('services.sms.from_number'));
}
---
## Making It Provider-Agnostic
The channel should not care which provider sends the message. The `SmsGateway` interface abstracts this:
```php
<?php
namespace App\Contracts;
use App\DataTransferObjects\SmsMessage;
use App\DataTransferObjects\SmsResult;
interface SmsGateway
{
public function send(SmsMessage $message): SmsResult;
public function getName(): string;
}
---
Bind the gateway 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\Notifications\Channels\SmsChannel;
use Illuminate\Support\ServiceProvider;
class NotificationChannelServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->bind(SmsChannel::class, function ($app) {
$gateway = match (config('services.sms.default_gateway', 'twilio')) {
'twilio' => $app->make(TwilioGateway::class),
'vonage' => $app->make(VonageGateway::class),
default => $app->make(TwilioGateway::class),
};
return new SmsChannel($gateway);
});
}
}
---
## Adding Configuration Options
Create a configuration file at `config/services.php` (or a dedicated `config/sms-channel.php`):
```php
// config/services.php additions
'sms' => [
'default_gateway' => env('SMS_DEFAULT_GATEWAY', 'twilio'),
'from_number' => env('SMS_FROM_NUMBER'),
'from_name' => env('SMS_FROM_NAME', 'Laravel SMS'),
],
---
The channel can access configuration for defaults:
```php
public function send(object $notifiable, Notification $notification): void
{
$message = $notification->toSms($notifiable);
$from = $message->from ?? config('services.sms.from_number');
$payload = new SmsMessage(
to: $notifiable->routeNotificationForSms($notification),
content: $this->getContent($message),
from: $from,
);
// ...
}
---
## Testing Custom Channels
### Unit Testing the Channel
```php
<?php
namespace Tests\Unit\Notifications\Channels;
use App\Contracts\SmsGateway;
use App\DataTransferObjects\SmsMessage;
use App\DataTransferObjects\SmsResult;
use App\Notifications\Channels\SmsChannel;
use Illuminate\Notifications\Notification;
use Mockery;
use Tests\TestCase;
class SmsChannelTest extends TestCase
{
public function test_sends_sms_via_gateway(): void
{
$gateway = Mockery::mock(SmsGateway::class);
$gateway->shouldReceive('send')
->once()
->with(Mockery::type(SmsMessage::class))
->andReturn(new SmsResult(true, 'test', 'msg_123'));
$channel = new SmsChannel($gateway);
$notifiable = new \stdClass();
$notifiable->phone_number = '+15559876543';
$notification = Mockery::mock(Notification::class);
$notification->shouldReceive('toSms')
->once()
->with($notifiable)
->andReturn((object) ['content' => 'Test message']);
$notifiable->routeNotificationForSms = function () {
return $this->phone_number;
};
$channel->send($notifiable, $notification);
$this->expectNotToPerformAssertions();
}
public function test_skips_when_no_phone_number(): void
{
$gateway = Mockery::mock(SmsGateway::class);
$gateway->shouldNotReceive('send');
$channel = new SmsChannel($gateway);
$notifiable = new \stdClass();
$notifiable->phone_number = null;
$notification = Mockery::mock(Notification::class);
$notifiable->routeNotificationForSms = function () {
return null;
};
$channel->send($notifiable, $notification);
$this->expectNotToPerformAssertions();
}
}
---
### Feature Testing Notifications
```php
<?php
namespace Tests\Feature\Notifications;
use App\Contracts\SmsGateway;
use App\DataTransferObjects\SmsResult;
use App\Notifications\OrderConfirmation;
use App\Models\User;
use Illuminate\Support\Facades\Notification;
use Tests\TestCase;
class OrderConfirmationTest extends TestCase
{
public function test_sends_sms_notification(): void
{
Notification::fake();
$user = User::factory()->create([
'phone_number' => '+15559876543',
]);
$user->notify(new OrderConfirmation('ORD-123'));
Notification::assertSentTo(
$user,
OrderConfirmation::class,
function (OrderConfirmation $notification, array $channels) {
return in_array('sms', $channels);
}
);
}
public function test_sms_content_is_correct(): void
{
$gateway = $this->mock(SmsGateway::class);
$gateway->shouldReceive('send')
->once()
->andReturnUsing(function ($message) {
$this->assertStringContainsString('ORD-123', $message->content);
$this->assertEquals('+15559876543', $message->to);
return new SmsResult(true, 'test', 'msg_123');
});
$user = User::factory()->create(['phone_number' => '+15559876543']);
$user->notify(new OrderConfirmation('ORD-123'));
}
}
---
## Related Articles
- [SMS Gateway Integration in Laravel (Multi-Provider Guide)](/blog/laravel-sms-gateway-integration)
- [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)