DEV Community

Cover image for Notify users by SMS - with a custom Laravel notification channel
Grafstorm
Grafstorm

Posted on • Edited on

Notify users by SMS - with a custom Laravel notification channel

As I was working with an application that needed to send sms notifications to new users signing up. Instead of using the built in Nexmo driver(which I havn't tried) or any of the great notification channels at https://laravel-notification-channels.com/.
I of course, as any normal developer would :-), decided to try to write my own notification channel... 🙄

Okay. So we're going to create a custom Laravel notification channel for sending sms notifications to our users. The provider we will be using for text messages is 46elks.se. But the steps to use any other provider should be similar.

How sending email notifications looks like

Sending a email notification to a user in laravel can be achieved like this:

// Part of our WelcomeNewUser notification class.
// More on creating a notification below..
public function toMail($notifiable)
{
    $url = url('/invoice/'.$this->invoice->id);

    return (new MailMessage)
                ->greeting('Hello there!')
                ->line('Thanks for registering!')
                ->action('Visit our site', $url)
                ->line('Thank you for using our application!');
}

Enter fullscreen mode Exit fullscreen mode

Sending it can look like this using the notifiable trait on the User model.

$user->notify(new WelcomeNewUser());
Enter fullscreen mode Exit fullscreen mode

We want to be able to use our new sms notification channel in a similar fashion. The fluent methods on the mail message are quite nice, and would be nice to have on our upcoming sms notification as well.

Ok, now that we have what we want to achieve laid out. Let's get to it.

We start off with having a quick look at our goto place as Laravel developers. The docs :-)

Creating a new Notification

https://laravel.com/docs/8.x/notifications#generating-notifications
So... let's create a new notification that should be sent when new users sign up to our service.

php artisan make:notification WelcomeNewUserWithSms
Enter fullscreen mode Exit fullscreen mode

Now let's open our new Notification in our favorite editor.(PhpStorm to the rescue 🙌)

This is how our newly notification looks like with no modification whatsoever.

<?php

namespace App\Notifications;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;

class WelcomeNewUserWithSms extends Notification
{
    use Queueable;

    /**
     * Create a new notification instance.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * Get the notification's delivery channels.
     *
     * @param  mixed  $notifiable
     * @return array
     */
    public function via($notifiable)
    {
        return ['mail'];
    }

    /**
     * Get the mail representation of the notification.
     *
     * @param  mixed  $notifiable
     * @return \Illuminate\Notifications\Messages\MailMessage
     */
    public function toMail($notifiable)
    {
        return (new MailMessage)
                    ->line('The introduction to the notification.')
                    ->action('Notification Action', url('/'))
                    ->line('Thank you for using our application!');
    }

    /**
     * Get the array representation of the notification.
     *
     * @param  mixed  $notifiable
     * @return array
     */
    public function toArray($notifiable)
    {
        return [
            //
        ];
    }
}

Enter fullscreen mode Exit fullscreen mode

To start off with, we want our notifications to be queued by default. If we were to send many notifications in one go, we wouldn't want our page or command to freeze up until they were all sent. Let's change that.

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;

// We already have ShouldQueue imported in.
// Let our class implement that interface
// This works because we already have use Queueable; in the class.
// So thats all we need for Laravel to automatically queue the Notification by default.
class WelcomeNewUserWithSms extends Notification implements ShouldQueue
{
    use Queueable;

Enter fullscreen mode Exit fullscreen mode

Nice, now we want our notification to send SMS instead of emails to our users. For that to work we will point our notification to use our new driver. (which we will create in a bit). This is done in the via method.

    public function via($notifiable)
    {
        // So instead of the "default" mail channel.. Lets change our notification to notify user with the SmsChannel class.
        // We also need to import our SmsChannel at the top of our file for this to work.
        return [SmsChannel::class];
        // return ['mail'];
    }
Enter fullscreen mode Exit fullscreen mode

Now we also need to specify what our SmsChannel need to receive to send the sms. Like a telephone number and a message for example.

This is done with a method we call toSms() instead of the default toMail() in our Notification. In reality we can call this method whatever we like. But I believe a good practice is to name it the same as our new notification channel. So SmsChannel -> toSms feels good to me.
So let's remove the toMail method and add a toSms method.
And let's see if we can create the same nice feeling with the fluent methods we can use when we send a mail notification.

    public function toSms($notifiable)
    {
        // We are assuming we are notifying a user or a model that has a telephone attribute/field. 
        // And the telephone number is correctly formatted.
        // TODO: SmsMessage, doesn't exist yet :-) We should create it.
        return (new SmsMessage)
                    ->from('ObiWan')
                    ->to($notifiable->telephone)
                    ->line("These aren't the droids you are looking for.");
    }
Enter fullscreen mode Exit fullscreen mode

Ok, we are more than halfway through! And reached the moment to actually create our notification channel. And after that we should create a SmsMessage class as well. And then... Texting begins..

Creating a custom notification channel

https://laravel.com/docs/8.x/notifications#custom-channels
Basically all we need for a custom notification channel driver to work is to have a class with a send() method. The send method should accept $notifiable and $notification according to the docs.

<?php

namespace App\Channels;

use Illuminate\Notifications\Notification;

class SmsChannel
{
    /**
     * Send the given notification.
     *
     * @param  mixed  $notifiable
     * @param  \Illuminate\Notifications\Notification  $notification
     * @return void
     */
    public function send($notifiable, Notification $notification)
    {
        // Remember that we created the toSms() methods in our notification class
        // Now is the time to use it.
        // In our example. $notifiable is an instance of a User that just signed up.
        $message = $notification->toSms($notifiable);

        // Now we hopefully have a instance of a SmsMessage.
        // That we are ready to send to our user.
        // Let's do it :-)
        $message->send();

       // Or use dryRun() for testing to send it, without sending it for real.
        $message->dryRun()->send();

        // Wait.. was that it?
        // Well sort of.. :-)
        // But we need to implement this magical SmsMessage class for it to work.

    }
}
Enter fullscreen mode Exit fullscreen mode

Implementing the SmsMessage class. This is where the magic happens.

Let's start by doing a recap of what we need to create.

// In our Notification we were using the SmsClass like this:
return (new SmsMessage)
    ->from('ObiWan')
    ->to($notifiable->telephone)
    ->line("These aren't the droids you are looking for.");

// And in our SmsChannel we use the SmsMessage like this:
$message = $notification->toSms($notifiable); // toSms() returns an instance of our SmsMessage class.
$message->send();

Enter fullscreen mode Exit fullscreen mode

So at the very least our SmsMessage class needs 4 methods:
from, to, line and send

// Lets start with and empty class with the four stubs and a constructor.
class SmsMessage {

    public function __construct()
    {

    }

    public function from()
    public function to()
    public function line()
    public function send()

}

Enter fullscreen mode Exit fullscreen mode

To get our fluent methods going for us, we can return the actual instance of the class by return $this from each of the methods. That way we can chain the methods nicely like so: $sms->to('+461')->from('sender')->line('Hello')->line('World!')->send()

So how about this:

// Let's start with and empty class with the four stubs and a constructor.
class SmsMessage {

    protected $lines = [];
    protected $from;
    protected $to;

    public function __construct($lines = [])
    {
         $this->lines = $lines;

         return $this;
    }

    public function from($from)
    {
        $this->from = $from;

        return $this;
    }

    public function to($to)
    {
      $this->to = $to;

         return $this;
    }

    public function line($line = '')
    {
       $this->lines[] = $line;

       return $this;
    }

    public function send() {
       // TODO: Implement logic to send the message.
    }

}


Enter fullscreen mode Exit fullscreen mode

Ok, now we have the basic methods done. Except the logic for the send method and dryrun().

For the send method we will be using the built in Http Facade in Laravel. As 46 elks uses BasicAuth, it is quite straight forward to send a API request with username and password.

Let's flesh out the SmsMessage class a bit more.

<?php


namespace Grafstorm\FortySixElksChannel;

use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Http;

class SmsMessage
{

    protected string $user;
    protected string $password;
    protected string $to;
    protected string $from;
    protected array $lines;
    protected string $dryrun = 'no';

    /**
     * SmsMessage constructor.
     * @param array $lines
     */
    public function __construct($lines = [])
    {
        $this->lines = $lines;

        // Pull in config from the config/services.php file.
        $this->from = config('services.elks.from');
        $this->baseUrl = config('services.elks.base_url');
        $this->user = config('services.elks.user');
        $this->password = config('services.elks.password');
    }

    public function line($line = ''): self
    {
        $this->lines[] = $line;

        return $this;
    }

    public function to($to): self
    {
        $this->to = $to;

        return $this;
    }

    public function from($from): self
    {
        $this->from = $from;

        return $this;
    }

    public function send(): mixed
    {
        if (!$this->from || !$this->to || !count($this->lines)) {
            throw new \Exception('SMS not correct.');
        }

        return Http::baseUrl($this->baseUrl)->withBasicAuth($this->user, $this->pass)
            ->asForm()
            ->post('sms', [
                'from' => $this->from,
                'to' => $this->to,
                'message' => $this->lines->join("\n", ""),
                'dryryn' => $this->dryrun
            ]);
    }

    public function dryrun($dry = 'yes'): self
    {
        $this->dryrun = $dry;

        return $this;
    }
}

Enter fullscreen mode Exit fullscreen mode

Later on we should add better validation logic to validate our input and handle exceptions that could occur. Like network errors, API errors, improper telephone number or message formatting.
Also perhaps our SmsMessage Class should not be responsible for the actual sending of the text message, we could extract this to another class..

Configuration

And finally, add configuration to the config/services.php file and don't forget to add the right credentials to the .env file.

// Add this to the config/services.php file.
'elks' => [
        'from' => env('ELKS_FROM'),
        'base_url' => 'https://api.46elks.com/a1/',
        'user' => env('ELKS_USER'),
        'password' => env('ELKS_PASSWORD'),
    ],

Enter fullscreen mode Exit fullscreen mode

And the credentials to the .env file

ELKS_FROM=## THE SENDER YOU WANT AS DEFAULT ##
ELKS_USER=## THE USER NAME FOR YOUR 46 elks api account ##
ELKS_PASSWORD=## THE PASSWORD ##
Enter fullscreen mode Exit fullscreen mode

Now we have a working notification channel, with the added bonus that we can send sms anywhere in our application by using our SmsMessage class.

Grand finale, sending a sms!

// Notify a User with a welcome sms message
$user = User::first();
$user->notify(new WelcomeNewUserWithSms());

// Or send a sms directly
$sms = new SmsMessage;
// Replace with your telephone number :-)
$sms->to('+461')->line('Hello World.')->send();
Enter fullscreen mode Exit fullscreen mode

With any luck, you should now have received a sms message!

And thats a wrap... I hope this post can be of use to people new to Laravel.
Input is greatly appreciated since this is my first post :-)

Top comments (3)

Collapse
 
tmbenhura profile image
tmbenhura

Though the implementation works, I think that the SmsMessage class should be akin to a data transfer object, with the SmsChannel doing the heavy lifting of performing the actual sending of the message.

Currently, supposing you had a MmsMessage that could also be sent, the MmsMessage would need to somewhat replicate the sending functionality. Both sending implementations would also need to be tested independently.

Collapse
 
salimhosen profile image
Mohammad Salim Hosen

In SmsMessage I get message as empty.

Collapse
 
grafstorm profile image
Grafstorm • Edited

Hi!
I think I was a bit quick on keyboard when I wrote this,
I updated the example with

'message' => $this->lines->join("\n", ""),
Enter fullscreen mode Exit fullscreen mode

So the message actually gets set :-)