Integrate Twilio WhatsApp Business API with a Symfony Application

September 27, 2022
Written by
Joseph Udonsak
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by

Integrate Twilio WhatsApp Business API with a Symfony Application

With a monthly active user base of over 2 billion people, WhatsApp has risen to one of the most popular messaging platforms in the world today. This has established it as a viable means of sending notifications to clients.

What's more, by using the WhatsApp Business API by Twilio, you can establish a two-way communication channel with your customer and improve your service offering, such as order processing and management for instance.

In this article, you will learn how to do this by integrating a PHP implementation of the Eliza program with the WhatsApp Business API, making it possible to chat with Eliza via WhatsApp.

Prerequisites

To follow this tutorial, you need the following things:

Set up the WhatsApp Testing Sandbox

The first thing to do is to set up a Twilio Sandbox via the developer console, which you can find under "Explore Products > Messaging > Try it out > Send a WhatsApp message". To do this, send a message with the specified code to the displayed phone number, as shown in the screenshot below.

Confirmation that a user can send a WhatsApp message from Twilio WhatsApp Sandbox

Once this is done, you will see a message similar to the one shown below:

Twilio WhatsApp Sandbox message confirmation

Keep this window or tab open until the end of the tutorial, otherwise you will have to perform this step again.

Create a new Symfony application

In the terminal, create a new Symfony application and change into the newly created application directory by running the following commands.

symfony new whats_app_demo
cd whats_app_demo

Set the required environment variables

Next, make a local copy of .env, to later store the required environment variables file, by running the following command.

cp .env .env.local

.env.local files are ignored by Git as an accepted best practice for storing credentials outside of code to keep them safe. You could also store the application credentials in a secrets manager, if you're really keen.

After that, add the following to the end of the newly created .env.local file

TWILIO_WHATSAPP_NUMBER="<TWILIO_WHATSAPP_NUMBER>"
TWILIO_ACCOUNT_SID="<TWILIO_ACCOUNT_SID>"
TWILIO_AUTH_TOKEN="<TWILIO_AUTH_TOKEN>"

With that done, you need to retrieve three key parameters:

  1. The phone number provided for testing
  2. Your Twilio Account SID
  3. Your Twilio Auth Token

Copy the WhatsApp phone number, as shown in the earlier screenshot, and paste it in place of <TWILIO_WHATSAPP_NUMBER> in .env.

Next, retrieve your Twilio Account SID and Auth Token from your Twilio Console dashboard.

The Twilio Console showing where to retrieve the Twilio Auth Token and Account SID details

Login to the Twilio Console dashboard, which contains your Twilio Auth Token and Account SID. Copy them and paste them in place of <TWILIO_ACCOUNT_SID> and <TWILIO_AUTH_TOKEN> respectively, in .env.

Implement "Eliza"

Next, implement the Eliza model to process the user’s message and provide a response. Start by creating a new folder named Model in the src folder. Then, in src/Model, create a new file named Eliza.php and add the following code to it.

<?php

declare(strict_types=1);

namespace App\Model;

abstract class Eliza 
{
    private const HELLO_RESPONSES   = 37;
    private const GOODBYE_RESPONSES = 39;
    private const MATCHES = [
        "life",
        "i need",
        "why don't",
        "why can't",
        "i can",
        "i am",
        "i'm",
        "are you",
        "what",
        "how",
        "because",
        "sorry",
        "i think",
        "friend",
        "yes",
        "computer",
        "is it",
        "it is",
        "can you",
        "can i",
        "you are",
        "you're",
        "i don't",
        "i feel",
        "i have",
        "i've",
        "i would",
        "is there",
        "my",
        "you",
        "why",
        "i want",
        "mother",
        "father",
        "child",
        "?",
        "hello",
        "hi",
        "hey",
        "quit",
    ];

    private const REFLECTIONS = [
        "am"     => "are",
        "was"    => "were",
        "i"      => "you",
        "i'd"    => "you would",
        "i'll"   => "you will",
        "my"     => "your",
        "are"    => "am",
        "you've" => "I have",
        "your"   => "my",
        "yours"  => "mine",
        "you"    => "me",
        "i'm"    => "your",
    ];

    private const RESPONSES = [
        [
            "Life? Don't talk to me about life.",
            "At least you have a life, I'm stuck inside this computer.",
            "Life can be good. Remember, 'this, too, will pass'.",
        ],
        ["Why do you need %1?", "Would it really help you to get %1?", "Are you sure you need %1?"],
        ["Do you really think I don't %1?", "Perhaps eventually I will %1.", "Do you really want me to %1?"],
        [
            "Do you think you should be able to %1?",
            "If you could %1, what would you do?",
            "I don't know -- why can't you %1?",
            "Have you really tried?",
        ],
        ["How do you know you can't %1?", "Perhaps you could %1 if you tried.", "What would it take for you to %1?"],
        ["Did you come to me because you are %1?", "How long have you been %1?", "How do you feel about being %1?"],
        [
            "How does being %1 make you feel?",
            "Do you enjoy being %1?",
            "Why do you tell me you're %1?",
            "Why do you think you're %1?",
        ],
        [
            "Why does it matter whether I am %1?",
            "Would you prefer it if I were not %1?",
            "Perhaps you believe I am %1.",
            "I may be %1 -- what do you think?",
        ],
        ["Why do you ask?", "How would an answer to that help you?", "What do you think?"],
        ["How do you suppose?", "Perhaps you can answer your own question.", "What is it you're really asking?"],
        [
            "Is that the real reason?",
            "What other reasons come to mind?",
            "Does that reason apply to anything else?",
            "If %1, what else must be true?",
        ],
        ["There are many times when no apology is needed.", "What feelings do you have when you apologize?"],
        ["Do you doubt %1?", "Do you really think so?", "But you're not sure %1?"],
        [
            "Tell me more about your friends.",
            "When you think of a friend, what comes to mind?",
            "Why don't you tell me about a childhood friend?",
        ],
        ["You seem quite sure.", "OK, but can you elaborate a bit?"],
        [
            "Are you really talking about me?",
            "Does it seem strange to talk to a computer?",
            "How do computers make you feel?",
            "Do you feel threatened by computers?",
        ],
        [
            "Do you think it is %1?",
            "Perhaps it is %1 -- what do you think?",
            "If it were %1, what would you do?",
            "It could well be that %1.",
        ],
        ["You seem very certain.", "If I told you that it probably isn't %1, what would you feel?"],
        ["What makes you think I can't %1?", "If I could %1, then what?", "Why do you ask if I can %1?"],
        ["Perhaps you don't want to %1.", "Do you want to be able to %1?", "If you could %1, would you?"],
        [
            "Why do you think I am %1?",
            "Does it please you to think that I'm %1?",
            "Perhaps you would like me to be %1.",
            "Perhaps you're really talking about yourself?",
        ],
        ["Why do you say I am %1?", "Why do you think I am %1?", "Are we talking about you, or me?"],
        ["Don't you really %1?", "Why don't you %1?", "Do you want to %1?"],
        [
            "Good, tell me more about these feelings.",
            "Do you often feel %1?",
            "When do you usually feel %1?",
            "When you feel %1, what do you do?",
        ],
        ["Why do you tell me that you've %1?", "Have you really %1?", "Now that you have %1, what will you do next?"],
        ["Why do you tell me that you've %1?", "Have you really %1?", "Now that you have %1, what will you do next?"],
        ["Could you explain why you would %1?", "Why would you %1?", "Who else knows that you would %1?"],
        ["Do you think there is %1?", "It's likely that there is %1.", "Would you like there to be %1?"],
        ["I see, your %1.", "Why do you say that your %1?", "When you're %1, how do you feel?"],
        ["We should be discussing you, not me.", "Why do you say that about me?"],
        ["Why don't you tell me the reason why %1?", "Why do you think %1?"],
        [
            "What would it mean to you if you got %1?",
            "Why do you want %1?",
            "What would you do if you got %1?",
            "If you got %1, then what would you do?",
        ],
        [
            "Tell me more about your mother.",
            "What was your relationship with your mother like?",
            "How do you feel about your mother?",
            "How does this relate to your feelings today?",
            "Good family relations are important.",
        ],
        [
            "Tell me more about your father.",
            "How did your father make you feel?",
            "How do you feel about your father?",
            "Does your relationship with your father relate to your feelings today?",
            "Do you have trouble showing affection with your family?",
        ],
        [
            "Did you have close friends as a child?",
            "What is your favorite childhood memory?",
            "Do you remember any dreams or nightmares from childhood?",
            "Did the other children sometimes tease you?",
            "How do you think your childhood experiences relate to your feelings today?",
        ],
        [
            "Why do you ask that?",
            "Please consider whether you can answer your own question.",
            "Perhaps the answer lies within yourself?",
            "Why don't you tell me?",
        ],
        [
            "Hello... I'm glad you could drop by today.",
            "Hello there... how are you today?",
            "Hello, how are you feeling today?",
        ],
        ["Hi... I'm glad you could drop by today.", "Hi there... how are you today?", "Hi, how are you feeling today?"],
        [
            "Hey... I'm glad you could drop by today.",
            "Hey there... how are you today?",
            "Hey, how are you feeling today?",
        ],
        ["Thank you for talking with me.", "Good-bye.", "Thank you, that will be $150.  Have a good day!"],
        [
            "Please tell me more.",
            "Let's change focus a bit... Tell me about your family.",
            "Can you elaborate on that?",
            "Why do you say that %1?",
            "I see.",
            "Very interesting.",
            "I see.  And what does that tell you?",
            "How does that make you feel?",
            "How do you feel when you say that?",
        ],
    ];
}

The class is declared abstract because it won’t be initialised. The constant declarations will be used by the class to generate a response to the input from the user. The algorithm for responding to the user is roughly as follows:

  1. Remove punctuation marks from user input.
  2. Find a string in the MATCH constant array which is present in the user input.
  3. If a string in MATCH is present, remove it and then change the remaining words of the input (if any) with the appropriate reflection (from the REFLECTIONS constant array).
  4. If no string in MATCH is present in the user input, use the last array in the RESPONSES array to generate a response.

To implement this algorithm, add the following function to src/Model/Eliza.php

public static function respondTo(string $userInput): string
{
    // declare the two strings we need for output
    $output = $remainder = "";

    // strip out punctuation from user input
    $sanitizedInput = preg_replace('/\\p{P}/', '', $userInput);

    // Loop through the matches list. If there's a match, strip it out.
    // Change words in the remainder (if any)  of the input with the
    // corresponding entry from the reflections array.
    foreach (self::MATCHES as $matchIndex => $match) {
        $matchPosition = strpos($sanitizedInput, $match);

        if ($matchPosition !== false) {
            // we found the word in matches in the user input string, so now we need to
            // figure out how much to delete that input
            $contentAfterMatch = trim(
                substr(
                    $sanitizedInput,
                    $matchPosition,
                    strlen($match)
                )
            );
            $wordsInContentAfterMatch = explode(" ", $contentAfterMatch);

            // loop through array of words in our exploded variable, looking for one that
            // matches the key in the reflections map. This will change pronouns and verbs into
            // words appropriate for our response.
            foreach ($wordsInContentAfterMatch as $index => $value) {
                foreach (self::REFLECTIONS as $reflectionKey => $reflectionValue) {
                    if (strtolower($reflectionKey) === strtolower($value)) {
                        $wordsInContentAfterMatch[$index] = $reflectionValue;
                        break;
                    }
                }
            }

            // turn the array of words into a single string, and strip off extra spaces from beginning/end
            $remainder = trim(implode(" ", $wordsInContentAfterMatch));
            $randomResponseIndex = mt_rand(0, count(self::RESPONSES[$matchIndex]) - 1);
            $output = self::RESPONSES[$matchIndex][$randomResponseIndex];
            break;
        }
    }

    // If there wasn't a match, use the last item in the responses array.
    if ($output === "") {
        $indexOfLastSetOfResponses = count(self::RESPONSES) - 1;
        $randomResponseIndex = mt_rand(
            0,
            count(self::RESPONSES[$indexOfLastSetOfResponses]) - 1
        );
        $output = self::RESPONSES[$indexOfLastSetOfResponses][$randomResponseIndex];
    }
    // Build our final response and send it back. If the response contains %1, replace that
    // with the remainder of the input string.
    return str_replace('%1', $remainder, $output);
}

Next, add a function to generate the opening ("hello") response when communication is initiated by the user, by adding the following function to Eliza.php

public static function sayHello(): string 
{
    $helloResponses = self::RESPONSES[self::HELLO_RESPONSES];
    $randomHelloResponse = $helloResponses[mt_rand(0, count($helloResponses) - 1)];
    return <<<EOT

I'm Eliza
---------

Talk to the program by typing in plain English, using normal upper and lower-case letters and punctuation.  
Enter 'quit' when done.
        
$randomHelloResponse

EOT;
}

Finally, add a function to return a closing (goodbye) response when the appropriate text is sent from the user, by adding the following function to src/Model/Eliza.php.

public static function sayGoodBye(): string
{
    $goodbyeResponses = self::RESPONSES[self::GOODBYE_RESPONSES];
    $randomGoodbyeResponse = $goodbyeResponses[mt_rand(0, count($goodbyeResponses) - 1)];
    return <<<EOT
    
$randomGoodbyeResponse

EOT;
}

Implement a service to send WhatsApp messages

Having established a model to respond to user input, the next thing to do is implement a service which will send messages to the user via the Twilio WhatsApp Business API. The first thing to do is install the Twilio PHP Helper Library by running the following command.

composer require twilio/sdk

Next, in the src folder, create a new folder named Service. Then, in src/Service, create a new file named WhatsAppService.php and add the following code to it.

<?php

declare(strict_types=1);

namespace App\Service;

use Twilio\Rest\Client;

class WhatsAppService 
{
    private Client $twilio;

    public function __construct(
        private string $twilioWhatsAppNumber,
        private string $twilioAccountSID,
        private string $twilioAuthToken,
    ) {
        $this->twilio = new Client($twilioAccountSID, $twilioAuthToken);
    }

    public function send(string $message, string $to): void 
    {
        $this->twilio->messages->create($to, [
            'from' => "whatsapp:{$this->twilioWhatsAppNumber}",
            'body' => $message,
        ]);
    }
}

This service has a constructor that takes three parameters:

  • The verified WhatsApp Twilio number (or that of the sandbox for development)
  • The Account SID
  • The Auth Token for the Twilio account

Using the SID and Auth Token, a Twilio Client is created.

In the send() function, the Twilio client and verified WhatsApp Twilio number are used to send a WhatsApp message using the message content and phone number (in E.164 format) provided as function arguments.

 

The phone number is preceded by whatsapp: which specifies the delivery channel to the Twilio`Client.

Next, bind the environment variables to variable names which can then be made available by Symfony throughout the application. To do that, add the following to the _defaults key in config/services.yaml

bind:
     $twilioWhatsAppNumber: '%env(TWILIO_WHATSAPP_NUMBER)%'
     $twilioAccountSID: '%env(TWILIO_ACCOUNT_SID)%'
     $twilioAuthToken: '%env(TWILIO_AUTH_TOKEN)%'

Add a controller

Having prepared a model to respond to user input, and a service to dispatch the response via WhatsApp, the next thing to do is provide an endpoint for the Symfony application to receive requests from Twilio and handle accordingly.

For this, we will create a controller which will receive a POST request, process it, and trigger a response via WhatsApp to the user.

To do that, first add the required dependencies by running the following commands.

composer require doctrine/annotations
composer require --dev maker

The first dependency is Doctrine Annotations which provides support for implementing custom annotation functionality for PHP classes and functions. The second is Symfony's Maker bundle, which is used for creating controllers, entities, and the like.

Next, create a new controller by running the following command.

symfony console make:controller ElizaController

Then, open the newly created file (src/Controller/ElizaController.php) and update it to match the following.

<?php

namespace App\Controller;

use App\Model\Eliza;
use App\Service\WhatsAppService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class ElizaController extends AbstractController 
{
    #[Route('/eliza', name: 'app_eliza', methods: ['POST'])]
    public function index(Request $request, WhatsAppService $whatsApp): Response 
    {
        $recipient = $request->request->get('From');
        $userInput = $request->request->get('Body');
        $whatsApp->send(Eliza::respondTo($userInput), $recipient);

        return $this->json(null, Response::HTTP_NO_CONTENT);
    }
}

Twilio’s request to your application contains two key parameters:

  • From which contains the phone number of the user
  • Body which contains the message sent by the user

You can see the other parameters here.

Using these parameters, a message is sent via WhatsApp to the provided phone number. The content of the message is generated via the respondTo() function in the Eliza model.

Next, run the application using the following command.

symfony serve

If your local Symfony server has TLS support, add the --allow-http argument to the command. This will allow you to run ngrok using the HTTP protocol as the tls option is a premium offering.

By default, your application will run on port 8000. So next, expose port 8000 (or the revised port, if port 800 was already in use on your development machine) on ngrok by running the following command in a new terminal window (or tab).

ngrok http 8000

A UI will be displayed in your terminal with the public URL of your tunnel and other status and metrics information about connections made over your tunnel as shown below.

ngrok output showing the forwarding URL, session status, etc.

Next, head to the Twilio Sandbox for WhatsApp, which you can find under "Explore Products > Messaging > Settings > WhatsApp sandbox settings", and provide the ngrok Forwarding URL plus /eliza as the URL for the "WHEN A MESSAGE COMES IN" field.

The Twilio Sandbox for WhatsApp Sandbox Configuration form highlighting the value to set for "When a message comes in"

Scroll to the bottom of the page and click "Save".        

With this done, send a message to the earlier copied WhatsApp number to start a conversation with Eliza. A conversation with Eliza can be seen in the gif below.

A short animation showing a sample interaction with the application with WhatsApp

That's how to integrate Twilio's WhatsApp Business API with a Symfony application

In this tutorial, you learned how to integrate the Twilio WhatsApp Business API into a Symfony application. This opens up a host of possibilities, as it makes it possible to not only send notifications but also to respond to messages from customers. This makes your product/service much more accessible and valuable.

While you used a sandbox environment for development, you can enable your Twilio number for WhatsApp here. You can review the final codebase on GitHub.

Until next time, bye for now.

Joseph Udonsak is a software engineer with a passion for solving challenges – be it building applications, or conquering new frontiers on Candy Crush. When he’s not staring at his screens, he enjoys a cold beer and laughs with his family and friends.