Laravel Custom SMS Channel for Notifications (Build Your Own)

Laravel Custom SMS Channel for Notifications (Build Your Own)
Laravel SMS Team
Laravel SMS Team
June 22, 2026

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.

Understanding Laravel’s Notification System

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)