How to Create a WhatsApp Survey Using Twilio and CakePHP

March 18, 2025
Written by
Popoola Temitope
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by

How to Create a WhatsApp Survey Using Twilio and CakePHP

A survey is a research method used to collect data from a predefined group of respondents to gain insights into various topics of interest. Collecting surveys through WhatsApp can be an efficient and easy way to gather feedback from people, particularly when the target respondents are already using the platform.

In this tutorial, you will learn how to create a WhatsApp survey application using Twilio and CakePHP to efficiently collect survey responses from users.

Prerequisites

To complete this tutorial, ensure that you have the following:

Create a new CakePHP project

Let’s scaffold a new CakePHP application using Composer. To do this, open your terminal, navigate to the directory where you want to scaffold the project, and run the command below.

​​composer create-project --prefer-dist cakephp/app:~5.0 whatsapp_survey_app
cd whatsapp_survey_app

The command above will prompt you to set folder permissions. Enter ' Y' to confirm.

Connect to a database

To interact with a MySQL database, you need to add the database connection details to the application's configuration file. To do this, open the project in your preferred code editor or IDE, navigate to the config folder, and open the app_local.php file.

Inside the app_local.php file, locate the default subsection of the Datasources section. This subsection contains the database connection details. Replace the host, username, password, and database name with your actual database values, as exemplified in the screenshot below.

Next, create a new database named survey in your MySQL database.

Create the database tables

Let's create the questions and answers database tables by generating the migration files for the two tables using CakePHP's migration features. To do that, run the following commands.

bin/cake bake migration questions
bin/cake bake migration answers
bin/cake bake migration contact

The command above will generate three migration files: _Questions.php, _Answers.php, and _Contact.php, which will be located in the config/Migrations folder.

Now, let’s define the tables' schema and properties. To do this, navigate to the config/Migrations folder and open the migration file that ends with _Contact.php. Then update the body of the change() function with the following code.

$table = $this->table('contact');
$table->addColumn('phone_no', 'string', [
    'limit' => 255,
    'null' => true,
]);
$table->addColumn('survey_status', 'string', [
    'limit' => 25,
    'null' => true,
]);
$table->addColumn('current_question', 'string', [
    'limit' => 25,
    'null' => true,
]);
$table->create();

Now, to define the schema for the "Questions" table, open the migration file ending in _Questions.php and add the following code to the change() function.

$table = $this->table('questions');
 $table->addColumn('question', 'string', [
    'limit' => 255,
    'null' => true,
]);
$table->create();

Next, open the second generated file that ends with _Answers.php and add the following code to the change() function.

$table = $this->table('answers');
$table->addColumn('questionID', 'string', [
    'limit' => 255,
    'null' => true,
]);
$table->addColumn('respondentID', 'string', [
    'limit' => 255,
    'null' => true,
]);
$table->addColumn('Answers', 'string', [
    'limit' => 255,
    'null' => true,
]);
$table->create();

With the updates made, run the command below to execute the database migrations.

bin/cake migrations migrate

Create the application model and entity

To allow the application's controller to easily interact with the database tables, you need to create CakePHP models and entities for each table. You can do this by running the following commands.

bin/cake bake model Contact
bin/cake bake model Answers
bin/cake bake model Questions

Set up the application's environment variables

Now, let's create environment variables for the application's API keys. To do this, run the command below.

cp config/.env.example config/.env

Then navigate to the config folder, open the .env file, and add the following at the end of the file.

export TWILIO_ACCOUNT_SID="<actual_account_sid>"
export TWILIO_AUTH_TOKEN="<actual_auth_token>"
export TWILIO_WHATSAPP_NUMBER="whatsapp:<actual_whatsapp_number>"

To load the environment variables into the application, open the bootstrap.php file inside the config folder and uncomment the following lines of code.

if (!env('APP_NAME') && file_exists(CONFIG . '.env')) {
    $dotenv = new \josegonzalez\Dotenv\Loader([CONFIG . '.env']);
    $dotenv->parse()
        ->putenv()
        ->toEnv()
        ->toServer();
}

Retrieve your Twilio API credentials

Next, let's retrieve the Twilio API credentials and add them to the environment variables. Log in to your Twilio console dashboard, and under the Account Info section, you'll find your Twilio Account SID and Auth Token, as shown in the screenshot below.

Now, replace the <actual_account_sid> and <actual_auth_token> placeholders in .env with your Twilio Account SID and Auth Token that you just copied.

Set up your Twilio WhatsApp Sandbox and retrieve your number

Let's connect to the Twilio WhatsApp sandbox. To do this, go back to your Twilio Console dashboard, and from the side menu navigate to Explore Products > Messaging > Try it Out > Send a WhatsApp Message, as shown in the screenshot below.

From the WhatsApp Sandbox page:

  • Copy the Twilio WhatsApp number and replace the <twilio_whatsapp_number> placeholder in .env
  • Send the displayed join message to the Twilio WhatsApp number via WhatsApp, as shown in the screenshot above

Install Twilio's PHP Helper Library

To enable easy interaction with the Twilio WhatsApp API, let’s install the Twilio PHP Helper Library using the command below in a new terminal tab or session.

composer require twilio/sdk

Create the survey controller

Let's create a controller to manage the application logic, such as adding new survey questions, processing incoming survey answers, and viewing collected data. To do this, run the command below in your terminal to create a controller named SurveyController.

bin/cake bake controller survey --no-actions

The command above will generate a SurveyController.php file inside the src/Controller folder. Open the controller file and update it with the following code.

<?php

declare(strict_types=1);

namespace App\Controller;

use Cake\ORM\TableRegistry;
use Cake\Http\Exception\NotFoundException;
use Twilio\Rest\Client;
use Cake\Http\Response;
use Cake\Event\EventInterface;

class SurveyController extends AppController
{
    public function invite()
    {
        $contactTable = TableRegistry::getTableLocator()->get('Contact');
        if ($this->request->is('post')) {
            $data = $this->request->getData();
            $phoneNumbers = preg_split("/\r\n|\n|\r/", trim($data['phone_no']));
            foreach ($phoneNumbers as $phoneNo) {
                $answer = $contactTable->newEntity([
                    'phone_no' => $phoneNo,
                    'survey_status' => 'Pending',
                    'current_question' => null,
                ]);
                if ($contactTable->save($answer)) {
                    $this->sendWhatsAppInvitation($phoneNo);
                }
            }
            $this->Flash->success(__('The invitations have been sent.'));
            return $this->redirect(['action' => 'invite']);
        }
    }

    private function sendWhatsAppInvitation(string $phoneNo): void
    {
        $sid = getenv('TWILIO_ACCOUNT_SID'); 
        $token = getenv('TWILIO_AUTH_TOKEN');
        $whatsappFrom = getenv('TWILIO_WHATSAPP_NUMBER'); 
        $client = new Client($sid, $token);
        $message = "You've been invited to participate in a survey. Reply with YES to start the survey.";
        try {
            $client->messages->create(
                'whatsapp:' . $phoneNo, 
                [
                    'from' => $whatsappFrom,  
                    'body' => $message
                ]
            );
        } catch (\Exception $e) {
            if ($phoneNo!="") {
                $this->Flash->error(__('Failed to send WhatsApp invitation to ' . $phoneNo  ));
            }
        }
    }

    public function processResponse(): ?Response
    {
        $phoneNo = str_replace('whatsapp:', '', $this->request->getData('From'));
        $messageBody = $this->request->getData('Body');
        $contactTable = TableRegistry::getTableLocator()->get('Contact');
        $contact = $contactTable->find()
            ->where(['phone_no' => $phoneNo])
            ->first();
        if (!$contact) {
            throw new NotFoundException(__('Contact not found.'));
        }
        if ($contact->survey_status === 'Pending' && strtolower($messageBody) === 'yes') {
            $contact->survey_status = 'In Progress';
            $contact->current_question = 1; 
            $contactTable->save($contact);
            $this->sendNextQuestion($phoneNo, 1);
        }
         elseif ($contact->survey_status === 'In Progress') {
            $this->saveAnswer($contact, $messageBody);
            $nextQuestionNumber = $contact->current_question + 1;
            $surveyQuestionsTable = TableRegistry::getTableLocator()->get('Questions');
            $totalQuestions = $surveyQuestionsTable->find()->count();
            if ($nextQuestionNumber <= $totalQuestions) {
                $contact->current_question = $nextQuestionNumber;
                $contactTable->save($contact);
                $this->sendNextQuestion($phoneNo, $nextQuestionNumber);
            } else {
                $contact->survey_status = 'Completed';
                $contact->current_question = null;
                $contactTable->save($contact);
                $this->sendWhatsAppMessage($phoneNo, 'Thank you for completing the survey.');
            }
        } else {
            $this->sendWhatsAppMessage($phoneNo, 'Please reply with YES to start the survey.');
        }
        return $this->response->withStringBody('OK');
    }

    private function sendNextQuestion(string $phoneNo, int $questionNumber): void
    {
        $surveyQuestionsTable = TableRegistry::getTableLocator()->get('Questions');
        $question = $surveyQuestionsTable->find()
            ->where(['id' => $questionNumber]) 
            ->first();
        if ($question) {
            $this->sendWhatsAppMessage($phoneNo, $question->question); 
        } else {
            $this->sendWhatsAppMessage($phoneNo, 'No more questions available.');
        }
    }

    private function saveAnswer($contact, string $answer): void
    {
        $surveyAnswersTable = TableRegistry::getTableLocator()->get('Answers');
        $surveyAnswer = $surveyAnswersTable->newEntity([
            'questionID' => $contact->current_question,
            'respondentID' => $contact->id,
            'Answers' => $answer,
        ]);
        $surveyAnswersTable->save($surveyAnswer);
    }

    private function sendWhatsAppMessage(string $phoneNo, string $message): void
    {
        $sid = getenv('TWILIO_ACCOUNT_SID'); 
        $token = getenv('TWILIO_AUTH_TOKEN');
        $whatsappFrom = getenv('TWILIO_WHATSAPP_NUMBER'); 
        $client = new Client($sid, $token);
        try {
            $client->messages->create(
                'whatsapp:' . $phoneNo, 
                [
                    'from' => $whatsappFrom,
                    'body' => $message
                ]
            );
        } catch (\Exception $e) {
            $this->Flash->error(__('Failed to send WhatsApp message to ' . $phoneNo));
        }
    }

    public function addQuestion()
    {
        $questionsTable = TableRegistry::getTableLocator()->get('Questions');
        if ($this->request->is('post')) {
            $questionsText = $this->request->getData('questions');
            $questionsArray = preg_split('/\r\n|\r|\n/', trim($questionsText));
            $savedQuestions = 0;
            foreach ($questionsArray as $questionText) {
                $questionText = trim($questionText);
                if (!empty($questionText)) {
                    $question = $questionsTable->newEntity(['question' => $questionText]);
                    if ($questionsTable->save($question)) {
                        $savedQuestions++;
                    }
                }
            }
            if ($savedQuestions > 0) {
                $this->Flash->success(__('The questions have been saved.'));
            } else {
                $this->Flash->error(__('Unable to save the questions.'));
            }
        }
    }

    public function viewStatistics()
    {
        $questionsTable = TableRegistry::getTableLocator()->get('Questions');
        $answersTable = TableRegistry::getTableLocator()->get('Answers');
        $questions = $questionsTable->find('all');
        $statistics = [];
        foreach ($questions as $question) {
            $answers = $answersTable->find()
                ->where(['questionID' => $question->id])
                ->toArray();
            $statistics[] = [
                'question' => $question->question,
                'total_responses' => count($answers),
                'responses' => array_map(function ($answer) {
                    return $answer->Answers;
                }, $answers),
            ];
        }
        $this->set(compact('statistics'));
    }
}

In the code above:

  • The invite() method is used to send survey invitations to users' WhatsApp numbers
  • The processResponse() method handles incoming WhatsApp messages containing user responses to survey questions and stores them in the database
  • The addQuestion() method adds new survey questions to the database
  • The viewStatistics() method retrieves all collected data, making it available to the admin for analysis

Create the application's templates

Let’s create templates for the application to handle the invite(), addQuestion(), and viewStatistics() methods. To do this, navigate to the templates folder and create a new folder named Survey. Then, inside the Survey folder, create a file named invite.php and add the following code:

<div class="contacts form">
    <h1>Invite Users to Participate in the Survey</h1>
    <?= $this->Form->create() ?>
    <fieldset>
        <?= $this->Form->control('phone_no', ['type' => 'textarea', 'label' => 'Whatsapp Numbers (one per line)']) ?>
    </fieldset>
    <?= $this->Form->button(__('Send Invitation')) ?>
    <?= $this->Form->end() ?>
</div>

Then, in the same directory, create a file named add_question.php and add the following code to it:

<h1>Add Survey Questions</h1>
<?= $this->Form->create() ?>
<?= $this->Form->control('questions', ['type' => 'textarea', 'label' => 'Survey Questions (one per line)','placeholder' => 'Enter each question on a new line']) ?>
<?= $this->Form->button(__('Save Questions')) ?>
<?= $this->Form->end() ?>

Now, create a file named view_statistics.php in the same directory as the previous two and add the following code to it:

<h1>Survey Statistics</h1>
<table>
    <thead>
        <tr>
            <th>Question</th>
            <th>Total Responses</th>
            <th>Responses</th>
        </tr>
    </thead>
    <tbody>
        <?php foreach ($statistics as $stat): ?>
        <tr>
            <td><?= h($stat['question']) ?></td>
            <td><?= $stat['total_responses'] ?></td>
            <td>
                <ul>
                    <?php foreach ($stat['responses'] as $response): ?>
                    <li><?= h($response) ?></li>
                    <?php endforeach; ?>
                </ul>
            </td>
        </tr>
        <?php endforeach; ?>
    </tbody>
</table>
<style>
table {
    width: 100%;
    border-collapse: collapse;
    margin-bottom: 20px;
}
th,
td {
    border: 1px solid #dddddd;
    text-align: left;
    padding: 8px;
}
th {
    background-color: #f2f2f2;
}
</style>

Configure the router

Now, let’s add the invite, processResponse, addQuestion, and viewStatistics routes to the application. From the application's root folder, navigate to the config folder and open the routes.php file. Inside the file, locate $routes->scope() and add the following code before $builder->fallbacks().

$builder->connect('/survey/invite', ['controller' => 'Survey', 'action' => 'invite']);
$builder->connect('/api/process-response', ['controller' => 'Survey', 'action' => 'processResponse']);
$builder->connect('/survey/add-question', ['controller' => 'Survey', 'action' => 'addQuestion']);
$builder->connect('/survey/statistics', ['controller' => 'Survey', 'action' => 'viewStatistics']);

Next, let’s disable CSRF protection for the /api/process-response API endpoint. To do this, open the Application.php file inside the src folder and update the middleware() function to match the following definition.

public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue
{
    $csrf = new CsrfProtectionMiddleware([
        'httponly' => true, // This option makes the CSRF cookie HttpOnly
    ]);
    $csrf->skipCheckCallback(function ($request) {
        return strpos($request->getUri()->getPath(), '/api') === 0;
    });
    $middlewareQueue
        ->add(new ErrorHandlerMiddleware(Configure::read('Error'), $this))
        ->add(new AssetMiddleware([
            'cacheTime' => Configure::read('Asset.cacheTime'),
        ]))
        ->add(new RoutingMiddleware($this))
        ->add(new BodyParserMiddleware())
        ->add($csrf);
    return $middlewareQueue;
}

Now, let’s start the development server by running the command below.

bin/cake server

Configure Twilio’s WhatsApp webhook

Next, let’s make the application accessible on the internet using ngrok, which will allow Twilio webhooks to send WhatsApp responses to your application for processing. To do that, open another terminal tab or session and run the command below.

ngrok http 8765

The command above will generate a Forwarding URL, as shown in the screenshot below.

Now, back in your Twilio Console, on the Try WhatsApp page, click on Sandbox Settings and configure the Sandbox as follows.

  • In the When a message comes in field, paste the ngrok Forwarding URL and append /api/process-response to the end of it
  • Set the Method field to POST

Next, click Save to apply the Sandbox configuration, as shown in the screenshot below.

Test that the application works as expected

To Test the application open the ngrok Forwarding URL in your browser. The CakePHP default page should appear, as shown in the screenshot below. 

Let's store the survey questions in the database. Open <ngrok Forwarding URL>/survey/add-question in your browser, replace the <ngrok Forwarding URL> placeholder with the ngrok Forwarding URL, and add your survey question as shown in the screenshot below.

After setting up all the survey questions, the next step is to invite users to participate. Open <Forwarding-URL>/survey/invite and enter the user's mobile/cell number in E.164 format to send an invitation, as shown in the screenshot below.

On WhatsApp, reply with 'YES' to start the survey, and then answer the questions as shown in the screenshot below.

To view the collected responses, open <Forwarding-URL>/survey/statistics in your browser. You will see the statistics for the collected survey responses, as shown in the screenshot below.

That’s how to create a WhatsApp survey using Twilio and CakePHP

Creating a WhatsApp survey application is an effective way to gather valuable feedback from your audience. In this tutorial, you learned how to build a WhatsApp survey application using Twilio and CakePHP to efficiently collect feedback from users.

Popoola Temitope is a mobile developer and a technical writer who loves writing about frontend technologies. He can be reached on LinkedIn.

The investigation icon in the tutorial's main image was created by Dewi Sari on Flaticon.