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!');
}
Sending it can look like this using the notifiable trait on the User model.
$user->notify(new WelcomeNewUser());
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
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 [
//
];
}
}
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;
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'];
}
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.");
}
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.
}
}
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();
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()
}
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.
}
}
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;
}
}
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'),
],
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 ##
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();
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)
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.
In SmsMessage I get message as empty.
Hi!
I think I was a bit quick on keyboard when I wrote this,
I updated the example with
So the message actually gets set :-)