Send a templated WhatsApp message through Twilio in Laravel

Published:
Author: Ally

Table of Contents

  1. Validation
  2. Permission
  3. Templating
  4. Dispatching
  5. Command

Imagine you need to send (on some schedule) a notification, of, say, a users' portfolio value, then a WhatsApp message might be ideal.

I used the following packages to help:

Validation

I use giggsey/libphonenumber-for-php for validation of a given number.

First, some things for DI:

app/Providers/AppServiceProvider.php:

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use libphonenumber\PhoneNumberUtil;

class AppServiceProvider extends ServiceProvider
{
    public function boot()
    {
        $this->app->bind(PhoneNumberUtil::class, function () {
            return PhoneNumberUtil::getInstance();
        });
    }
}

I use the following accessors in the model, so we can use it in a validation rule, or in Collection’s reject/filter in future scenarios.

app/Models/Portfolio.php:

    public function getHasValidPhoneNumberAttribute(): bool
    {
        if (blank($this->phone)) {
            return false;
        }

        $helper = app(PhoneNumberUtil::class);

        try {

You might want to exchange the GB with a relevant ISO 3316-2 country code if the number is not written in E.164 format.

            $phone = $helper->parse($this->phone, 'GB');

            return $helper->isValidNumber($phone);
        } catch (Throwable $e) {
            return false;
        }
    }

    public function getWhatsappPhoneNumberAttribute(): ?string
    {
        if (!$this->getHasValidPhoneNumberAttribute()) {
            return null;
        }

        $helper = app(PhoneNumberUtil::class);

        try {
            $phone = $helper->parse($this->phone, 'GB');

            return (string) Str::of($helper->format($phone, PhoneNumberFormat::E164))

When sending API requests to twilio for sending a WhatsApp message, the number requires this whatsapp: prefix.

                ->prepend('whatsapp:');
        } catch (Throwable $e) {
            return null;
        }
    }

Permission

I use spatie/laravel-permissions at the user level to determine whether they have opted-in to receive WhatsApp messages.

It’s as simple as:

$user->can('receive whatsapp messages');

Templating

Templating was a little weird to me at first.

You specify a template on twilio something like this:

📊 {{1}} portfolio has {{2}} in value. It is now worth: {{3}}

However, unlike most other APIs where you specify the template ID and then the template model/data like:

[
    1 => 'Crypto',
    2 => $direction < 1 ? 'decreased' : 'increased',
    3 => '£' . number_format($value, 2),
]

You however need to construct the template yourself, and then send that as the payload. This gets a little awkward when using emoji (some editors might strip it out, achieving correct spacing might be difficult, etc), so I use elvanto/litemoji to make that easier.

use Illuminate\Support\Str;
use LitEmoji\LitEmoji;

$template = Str::of('? ? portfolio has ? in value. It is now worth: ?');

$message = $template->replaceArray([
    LitEmoji::shortcodeToUnicode(':bar_chart:'),
    'Crypto',
    'decreased',
    '£187.51'
]);

So the body in the payload you will send will be something like this:

📊 Crypto portfolio has decreased in value. It is now worth: £187.51

Dispatching

I will dispatch a job for each message, the job will be fairly simple:

app/Jobs/SendWhatsAppMessage.php:

<?php

namespace App\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Twilio\Rest\Client;

class SendWhatsAppMessage implements ShouldQueue
{
    use InteractsWithQueue;
    use Queueable;

    public string $from;
    public string $to;
    public string $message;

    public function __construct(string $from, string $to, string $message)
    {
        $this->from = $from;
        $this->to = $to;
        $this->message = $message;
    }

    public function handle()
    {

This Client dependency injection will be left to the user, basically ssid and token passed into Twilio\Rest\Client constructor.

        $twilio = app(Client::class);

        $twilio->messages->create($this->to, [
            'from' => $this->from,
            'body' => $this->message,
        ]);
    }
}

Command

There is a command to run on a schedule (the command signature or how to add on the schedule are not important).

You might want to consider some options/arguments to:

Its handle might look something like this, using chunkById to get some better performance.

Portfilio::query()
    ->whereHas('user', fn (Builder $query) => $query->whereNotNull('phone'))
    ->chunkById(100, function (Collection $portfolios) {
        $portfolios
            ->filter(fn (Portfolio $portfolio) => $portfolio->user->can('receive whatsapp messages'))
            ->filter(fn (Portfolio $portfolio) => $portfolio->user->has_valid_phone_number)
            ->map(fn (Portfolio $portfolio) => PortfolioUpdateDto::fromPortfolio($portfolio))
            ->each(function (PortfolioUpdateDto $dto) {
                dispatch(new SendWhatsAppMessage($dto->from, $dto->to, $dto->message));
            });
    });
Create a GitHub workflow to run PHP linters, tests, and generate coverage report
Consume raw SQS messages from another application with Laravel's queue
To bottom
To top
< SM
max-width: 640px