Automated Survey with PHP and Laravel

Download the Code

Have you ever wondered how to create an automated survey that can be answered over phone or SMS?

This tutorial will show how to do it using the Twilio PHP SDK.

Here's how it works at a high level

  1. The end user calls or sends an SMS to the survey's phone number.
  2. Twilio gets the call or text and makes an HTTP request to your application asking for instructions on how to respond.
  3. Your web application instructs Twilio (using TwiML) to Gather or Record the user's voice input, and prompts text input with Message if you are using SMS.
  4. After each question, Twilio makes another request to your server with the user's input, which your application stores in its database.
  5. After storing the answer, our server will instruct Twilio to Redirect the user to the next question or finish the survey.

Instacart uses Twilio to power their customer service surveys and integrate that feedback into their customer database. Read more here.

Loading Code Samples...
Language
<?php

use Illuminate\Http\RedirectResponse;

/*
|--------------------------------------------------------------------------
| Application Routes
|--------------------------------------------------------------------------
|
| Here is where you can register all of the routes for an application.
| It's a breeze. Simply tell Laravel the URIs it should respond to
| and give it the controller to call when that URI is requested.
|
*/

Route::get(
    '/survey/{survey}/results',
    ['as' => 'survey.results', 'uses' => 'SurveyController@showResults']
);
Route::get(
    '/',
    ['as' => 'root', 'uses' => 'SurveyController@showFirstSurveyResults']
);
Route::post(
    '/voice/connect',
    ['as' => 'voice.connect', 'uses' => 'SurveyController@connectVoice']
);
Route::post(
    '/sms/connect',
    ['as' => 'sms.connect', 'uses' => 'SurveyController@connectSms']
);
Route::get(
    '/survey/{id}/voice',
    ['as' => 'survey.show.voice', 'uses' => 'SurveyController@showVoice']
);
Route::get(
    '/survey/{id}/sms',
    ['as' => 'survey.show.sms', 'uses' => 'SurveyController@showSms']
);
Route::get(
    '/survey/{survey}/question/{question}/voice',
    ['as' => 'question.show.voice', 'uses' => 'QuestionController@showVoice']
);
Route::get(
    '/survey/{survey}/question/{question}/sms',
    ['as' => 'question.show.sms', 'uses' => 'QuestionController@showSms']
);
Route::post(
    '/survey/{survey}/question/{question}/response/voice',
    ['as' => 'response.store.voice', 'uses' => 'QuestionResponseController@storeVoice']
);
Route::post(
    '/survey/{survey}/question/{question}/response/sms',
    ['as' => 'response.store.sms', 'uses' => 'QuestionResponseController@storeSms']
);
Route::post(
    '/survey/{survey}/question/{question}/response/transcription',
    ['as' => 'response.transcription.store', 'uses' => 'QuestionResponseController@storeTranscription']
);
app/Http/routes.php
Application routes for an automated survey

app/Http/routes.php

Creating a Survey

In order to perform an automated survey we first need to have some questions we want to ask a user. For your convenience, this sample application's repository already includes one survey that can be loaded into your database. You can take a look at the Readme File for detailed instructions.

You can modify the questions from the survey by editing the bear_survey.json file located in the root of the repository and re-running the app.

Loading Code Samples...
Language
<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;

class LoadSurveys extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'surveys:load {fileName}';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Load surveys into the database';

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Execute the console command.
     *
     * @return void
     */
    public function handle()
    {
        $filename = $this->argument('fileName');
        $surveyJSON = file_get_contents($filename);

        $parser = new \App\Twilio\SurveyParser($surveyJSON);

        $survey = new \App\Survey();
        $survey->title = $parser->title();
        $survey->save();

        $parser->questions()->each(
            function ($question) use ($survey) {
                $questionToSave = new \App\Question($question);
                $questionToSave->survey()->associate($survey);
                $questionToSave->save();
            }
        );
    }
}
app/Console/Commands/LoadSurveys.php
Load a survey into your application

app/Console/Commands/LoadSurveys.php

Before we dive in, let's take a moment to understand the flow of a Twilio-powered survey as an interview loop.

The Interview Loop

 The user can answer a question for your survey over the phone using either their phone's keypad or by voice. After each interaction Twilio will make an HTTP request to your web application with either the string of keys the user pressed or a URL to a recording of their voice input.

For SMS surveys the user will answer questions by replying with another SMS to the Twilio number that sent the question.

It's up to the application to process, store and respond to the user's input.

Let's dive into this flow to see how it actually works.

Configuring a Twilio Number

To initiate the interview process, we need to configure one of our Twilio numbers to send our web application an HTTP request when we get an incoming call or text.

Click on one of your numbers and configure Voice and Message URLs that point to your server. In our code, the routes are /voice/connect and /sms/connect, respectively.

IVR Webhook Configuration

 

Loading Code Samples...
Language
<?php

use Illuminate\Http\RedirectResponse;

/*
|--------------------------------------------------------------------------
| Application Routes
|--------------------------------------------------------------------------
|
| Here is where you can register all of the routes for an application.
| It's a breeze. Simply tell Laravel the URIs it should respond to
| and give it the controller to call when that URI is requested.
|
*/

Route::get(
    '/survey/{survey}/results',
    ['as' => 'survey.results', 'uses' => 'SurveyController@showResults']
);
Route::get(
    '/',
    ['as' => 'root', 'uses' => 'SurveyController@showFirstSurveyResults']
);
Route::post(
    '/voice/connect',
    ['as' => 'voice.connect', 'uses' => 'SurveyController@connectVoice']
);
Route::post(
    '/sms/connect',
    ['as' => 'sms.connect', 'uses' => 'SurveyController@connectSms']
);
Route::get(
    '/survey/{id}/voice',
    ['as' => 'survey.show.voice', 'uses' => 'SurveyController@showVoice']
);
Route::get(
    '/survey/{id}/sms',
    ['as' => 'survey.show.sms', 'uses' => 'SurveyController@showSms']
);
Route::get(
    '/survey/{survey}/question/{question}/voice',
    ['as' => 'question.show.voice', 'uses' => 'QuestionController@showVoice']
);
Route::get(
    '/survey/{survey}/question/{question}/sms',
    ['as' => 'question.show.sms', 'uses' => 'QuestionController@showSms']
);
Route::post(
    '/survey/{survey}/question/{question}/response/voice',
    ['as' => 'response.store.voice', 'uses' => 'QuestionResponseController@storeVoice']
);
Route::post(
    '/survey/{survey}/question/{question}/response/sms',
    ['as' => 'response.store.sms', 'uses' => 'QuestionResponseController@storeSms']
);
Route::post(
    '/survey/{survey}/question/{question}/response/transcription',
    ['as' => 'response.transcription.store', 'uses' => 'QuestionResponseController@storeTranscription']
);
app/Http/routes.php
Automated survey endpoints for voice and SMS

app/Http/routes.php

If you don't already have a server configured to use as your webhook, ngrok is a great tool for testing webhooks locally.

Next, we'll see how to handle requests to our webhooks.

Responding to a Twilio Request

Right after receiving a call or SMS, Twilio will send a request to the URL specified in that phone number's configuration (/voice/connect for calls and /sms/connect for sms).

Each of these endpoints will receive the request and return a welcome message to the user. When a user calls the Twilio number, our application will use the Say verb. For users that respond via SMS, the application will use a Message. It also includes a Redirect to the question's endpoint in order to continue the survey flow.

Loading Code Samples...
Language
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Http\Response;

use App\Http\Requests;
use App\Http\Controllers\Controller;
use App\Survey;
use App\QuestionResponse;

use Twilio\Twiml;

class SurveyController extends Controller
{
    const START_SMS_SURVEY_COMMAND = 'start';

    public function showResults($surveyId)
    {
        $survey = Survey::find($surveyId);
        $responsesByCall = QuestionResponse::responsesForSurveyByCall($surveyId)
                         ->get()
                         ->groupBy('session_sid')
                         ->values();

        return response()->view(
            'surveys.results',
            ['survey' => $survey, 'responses' => $responsesByCall]
        );
    }

    public function showFirstSurveyResults()
    {
        $firstSurvey = $this->_getFirstSurvey();
        return redirect(route('survey.results', ['survey' => $firstSurvey->id]))
                ->setStatusCode(303);
    }

    public function connectVoice()
    {
        $response = new Twiml();
        $redirectResponse = $this->_redirectWithFirstSurvey('survey.show.voice', $response);
        return $this->_responseWithXmlType($redirectResponse);
    }

    /**
     * Display the specified resource.
     *
     * @param  int  $id
     * @return Response
     */
    public function showVoice($id)
    {
        $surveyToTake = Survey::find($id);
        $voiceResponse = new Twiml();

        if (is_null($surveyToTake)) {
            return $this->_responseWithXmlType($this->_noSuchVoiceSurvey($voiceResponse));
        }
        $surveyTitle = $surveyToTake->title;
        $voiceResponse->say("Hello and thank you for taking the $surveyTitle survey!");
        $voiceResponse->redirect($this->_urlForFirstQuestion($surveyToTake, 'voice'), ['method' => 'GET']);

        return $this->_responseWithXmlType(response($voiceResponse));
    }

    /**
     * Display the specified resource.
     *
     * @param  int  $id
     * @return Response
     */
    public function showSms($id)
    {
        $surveyToTake = Survey::find($id);
        $voiceResponse = new Twiml();

        if (is_null($surveyToTake)) {
            return $this->_responseWithXmlType($this->_noSuchSmsSurvey($voiceResponse));
        }

        $surveyTitle = $surveyToTake->title;
        $voiceResponse->message("Hello and thank you for taking the $surveyTitle survey!");
        $voiceResponse->redirect($this->_urlForFirstQuestion($surveyToTake, 'sms'), ['method' => 'GET']);

        return $this->_responseWithXmlType(response($voiceResponse));
    }

    public function connectSms(Request $request)
    {
        $response = $this->_getNextSmsStepFromCookies($request);
        return $this->_responseWithXmlType($response);
    }

    private function _getNextSmsStepFromCookies($request) {
        $response = new Twiml();
        if (strtolower(trim($request->input('Body'))) === self::START_SMS_SURVEY_COMMAND) {
            $messageSid = $request->input('MessageSid');

            return $this->_redirectWithFirstSurvey('survey.show.sms', $response)
                        ->withCookie('survey_session', $messageSid);
        }

        $currentQuestion = $request->cookie('current_question');
        $surveySession = $request->cookie('survey_session');

        if ($this->_noActiveSurvey($currentQuestion, $surveySession)) {
            return $this->_smsSuggestCommand($response);
        }

        return $this->_redirectToStoreSmsResponse($response, $currentQuestion);
    }

    private function _redirectWithFirstSurvey($routeName, $response)
    {
        $firstSurvey = $this->_getFirstSurvey();

        if (is_null($firstSurvey)) {
            if ($routeName === 'survey.show.voice') {
                return $this->_noSuchVoiceSurvey($response);
            }
            return $this->_noSuchSmsSurvey($response);
        }

        $response->redirect(
            route($routeName, ['id' => $firstSurvey->id]),
            ['method' => 'GET']
        );
        return response($response);
    }

    private function _noActiveSurvey($currentQuestion, $surveySession) {
        $noCurrentQuestion = is_null($currentQuestion) || $currentQuestion == 'deleted';
        $noSurveySession = is_null($surveySession) || $surveySession == 'deleted';

        return $noCurrentQuestion || $noSurveySession;
    }

    private function _redirectToStoreSmsResponse($response, $currentQuestion) {
        $firstSurvey = $this->_getFirstSurvey();
        $storeRoute = route('response.store.sms', ['survey' => $firstSurvey->id, 'question' => $currentQuestion]);
        $response->redirect($storeRoute, ['method' => 'POST']);

        return response($response);
    }

    private function _smsSuggestCommand($response) {
        $response->message('You have no active surveys. Reply with "Start" to begin.');
        return response($response);
    }

    private function _noSuchSmsSurvey($messageResponse)
    {
        $messageResponse->message('Sorry, we could not find the survey to take. Good-bye');
        return response($messageResponse);
    }

    private function _urlForFirstQuestion($survey, $routeType)
    {
        return route(
            'question.show.' . $routeType,
            ['survey' => $survey->id,
             'question' => $survey->questions()->orderBy('id')->first()->id]
        );
    }

    private function _noSuchVoiceSurvey($voiceResponse)
    {
        $voiceResponse->say('Sorry, we could not find the survey to take');
        $voiceResponse->say('Good-bye');
        $voiceResponse->hangup();

        return response($voiceResponse);
    }

    private function _getFirstSurvey() {
        return Survey::orderBy('id', 'DESC')->get()->first();
    }

    private function _responseWithXmlType($response) {
        return $response->header('Content-Type', 'application/xml');
    }
}
app/Http/Controllers/SurveyController.php
Respond to a Twilio Request

app/Http/Controllers/SurveyController.php

Next, we will see how to build a TwiML response that will play back questions and gather input from the user.

Asking Questions (Voice)

After hearing the welcome message, the user will be redirected to the first question.

Each type of question will produce different instructions on how to proceed. If the question is "numeric" or "yes-no" in nature then we use the <Gather> verb. However, if we expect the user to record an answer we use the <Record> verb. Both verbs take an action attribute and a method attribute.

Twilio uses both attributes to define the response endpoint that will be used as callback. This endpoint will be responsible for receiving and storing the caller's answer.

During the Record verb creation, we also ask for a Transcription. Twilio will process the voice recording and extract all useful text. When the transcription is done it will make a request to our response's endpoint.

Loading Code Samples...
Language
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

use App\Http\Requests;
use App\Http\Controllers\Controller;

use App\Question;
use Twilio\Twiml;

class QuestionController extends Controller
{
    /**
     * Display the specified resource.
     *
     * @param  int  $id
     * @return Response
     */
    public function showVoice($surveyId, $questionId)
    {
        $questionToAsk = Question::find($questionId);
        return $this->_responseWithXmlType($this->_commandForVoice($questionToAsk));
    }

    /**
     * Display the specified resource.
     *
     * @param  int  $id
     * @return Response
     */
    public function showSms($surveyId, $questionId)
    {
        $questionToAsk = Question::find($questionId);
        return $this->_responseWithXmlType($this->_commandForSms($questionToAsk));
    }

    private function _messageForSmsQuestion($question) {
        $questionPhrases = collect(
            [
                'free-answer' => "\n\nReply to this message with your answer",
                'yes-no'      => "\n\nReply with \"YES\" or \"NO\" to this message",
                'numeric'     => "\n\nReply with a number from 1 to 10 to this message"
            ]
        );

        return $questionPhrases->get($question->kind, "\n\nReply to this message with your answer");
    }

    private function _messageForVoiceQuestion($question)
    {
        $questionPhrases = collect(
            [
                'free-answer' => "Please record your answer after the beep and then hit the pound sign",
                'yes-no'      => "Please press the one key for yes and the zero key for no and then hit the pound sign",
                'numeric'     => "Please press a number between 1 and 10 and then hit the pound sign"
            ]
        );

        return $questionPhrases->get($question->kind, "Please press a number and then the pound sign");
    }

    private function _commandForSms($question)
    {
        $smsResponse = new Twiml();

        $messageBody = $question->body . $this->_messageForSmsQuestion($question);
        $smsResponse->message($messageBody);

        return response($smsResponse)->withCookie('current_question', $question->id);
    }

    private function _commandForVoice($question)
    {
        $voiceResponse = new Twiml();

        $voiceResponse->say($question->body);
        $voiceResponse->say($this->_messageForVoiceQuestion($question));
        $voiceResponse = $this->_registerResponseCommand($voiceResponse, $question);

        return response($voiceResponse);
    }

    private function _registerResponseCommand($voiceResponse, $question)
    {
        $storeResponseURL = route(
            'response.store.voice',
            ['question' => $question->id,
             'survey' => $question->survey->id],
            false
        );

        if ($question->kind === 'free-answer') {
            $transcribeUrl = route(
                'response.transcription.store',
                ['question' => $question->id,
                 'survey' => $question->survey->id]
            );
            $voiceResponse->record(
                ['method' => 'POST',
                 'action' => $storeResponseURL,
                 'transcribe' => true,
                 'transcribeCallback' => $transcribeUrl]
            );
        } elseif ($question->kind === 'yes-no') {
            $voiceResponse->gather(['method' => 'POST', 'action' => $storeResponseURL]);
        } elseif ($question->kind === 'numeric') {
            $voiceResponse->gather(['method' => 'POST', 'action' => $storeResponseURL]);
        }
        return $voiceResponse;
    }

    private function _responseWithXmlType($response) {
        return $response->header('Content-Type', 'application/xml');
    }
}
app/Http/Controllers/QuestionController.php
Ask a survey question via voice call

app/Http/Controllers/QuestionController.php

Next, we will learn how to ask questions via SMS.

Asking Questions (SMS)

To ask questions over SMS we just need to use the <Message> TwiML verb. When users start a new survey, they will receive a welcome message as well as a message with the first survey question.

When the user is interacting over SMS we don't have something like an ongoing call session with a well defined state. It becomes harder to know if an SMS is answering question 2 or 20, since all requests will be sent to our main SMS endpoint at /sms/connect. To solve that, we can use Twilio Cookies to keep track of what question is being answered at the moment.

Loading Code Samples...
Language
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

use App\Http\Requests;
use App\Http\Controllers\Controller;

use App\Question;
use Twilio\Twiml;

class QuestionController extends Controller
{
    /**
     * Display the specified resource.
     *
     * @param  int  $id
     * @return Response
     */
    public function showVoice($surveyId, $questionId)
    {
        $questionToAsk = Question::find($questionId);
        return $this->_responseWithXmlType($this->_commandForVoice($questionToAsk));
    }

    /**
     * Display the specified resource.
     *
     * @param  int  $id
     * @return Response
     */
    public function showSms($surveyId, $questionId)
    {
        $questionToAsk = Question::find($questionId);
        return $this->_responseWithXmlType($this->_commandForSms($questionToAsk));
    }

    private function _messageForSmsQuestion($question) {
        $questionPhrases = collect(
            [
                'free-answer' => "\n\nReply to this message with your answer",
                'yes-no'      => "\n\nReply with \"YES\" or \"NO\" to this message",
                'numeric'     => "\n\nReply with a number from 1 to 10 to this message"
            ]
        );

        return $questionPhrases->get($question->kind, "\n\nReply to this message with your answer");
    }

    private function _messageForVoiceQuestion($question)
    {
        $questionPhrases = collect(
            [
                'free-answer' => "Please record your answer after the beep and then hit the pound sign",
                'yes-no'      => "Please press the one key for yes and the zero key for no and then hit the pound sign",
                'numeric'     => "Please press a number between 1 and 10 and then hit the pound sign"
            ]
        );

        return $questionPhrases->get($question->kind, "Please press a number and then the pound sign");
    }

    private function _commandForSms($question)
    {
        $smsResponse = new Twiml();

        $messageBody = $question->body . $this->_messageForSmsQuestion($question);
        $smsResponse->message($messageBody);

        return response($smsResponse)->withCookie('current_question', $question->id);
    }

    private function _commandForVoice($question)
    {
        $voiceResponse = new Twiml();

        $voiceResponse->say($question->body);
        $voiceResponse->say($this->_messageForVoiceQuestion($question));
        $voiceResponse = $this->_registerResponseCommand($voiceResponse, $question);

        return response($voiceResponse);
    }

    private function _registerResponseCommand($voiceResponse, $question)
    {
        $storeResponseURL = route(
            'response.store.voice',
            ['question' => $question->id,
             'survey' => $question->survey->id],
            false
        );

        if ($question->kind === 'free-answer') {
            $transcribeUrl = route(
                'response.transcription.store',
                ['question' => $question->id,
                 'survey' => $question->survey->id]
            );
            $voiceResponse->record(
                ['method' => 'POST',
                 'action' => $storeResponseURL,
                 'transcribe' => true,
                 'transcribeCallback' => $transcribeUrl]
            );
        } elseif ($question->kind === 'yes-no') {
            $voiceResponse->gather(['method' => 'POST', 'action' => $storeResponseURL]);
        } elseif ($question->kind === 'numeric') {
            $voiceResponse->gather(['method' => 'POST', 'action' => $storeResponseURL]);
        }
        return $voiceResponse;
    }

    private function _responseWithXmlType($response) {
        return $response->header('Content-Type', 'application/xml');
    }
}
app/Http/Controllers/QuestionController.php
Ask a survey question via SMS

app/Http/Controllers/QuestionController.php

Up next, we will see how to store the state of the survey.

Storing Answers

After the user has finished inputting voice commands or pressing keys, Twilio sends us a request that tells us what happened and asks for further instructions.

At this point, we need to recover data from Twilio's request parameters and store them.

Recovered parameters vary according to the questions:

  • Body contains the text message from an answer sent with SMS.
  • Digits contains the keys pressed for a numeric question.
  • RecodingUrl contains the URL for listening to a recorded message.
  • TranscriptionText contains the result of a recording transcription.

Finally we redirect to our question controller, which will ask the next question in the loop. This is done in the _redirectToQuestion() method.

Loading Code Samples...
Language
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

use App\Http\Requests;
use App\Http\Controllers\Controller;
use App\Question;
use App\Survey;
use App\QuestionResponse;
use App\ResponseTranscription;
use Twilio\Twiml;
use Cookie;

class QuestionResponseController extends Controller
{
    /**
     * Store a newly created resource in storage.
     *
     * @param  Request  $request
     * @return Response
     */
    public function storeVoice($surveyId, $questionId, Request $request)
    {
        $question = Question::find($questionId);
        $newResponse = $question->responses()->create(
            ['response' => $this->_responseFromVoiceRequest($question, $request),
             'type' => 'voice',
             'session_sid' => $request->input('CallSid')]
        );

        $nextQuestion = $this->_questionAfter($question);

        if (is_null($nextQuestion)) {
            return $this->_responseWithXmlType($this->_voiceMessageAfterLastQuestion());
        } else {
            return $this->_responseWithXmlType(
                $this->_redirectToQuestion($nextQuestion, 'question.show.voice')
            );
        }
    }

    /**
     * Store a newly created resource in storage.
     *
     * @param  Request  $request
     * @return Response
     */
    public function storeSms($surveyId, $questionId, Request $request)
    {
        $answer = trim($request->input('Body'));
        $question = Question::find($questionId);
        if ($question->kind === 'yes-no') {
            $answer = strtolower($answer) === 'yes' ? 1 : 0;
        }
        $newResponse = $question->responses()->create(
            ['response' => $answer,
             'type' => 'sms',
             'session_sid' => $request->cookie('survey_session')]
        );

        $nextQuestion = $this->_questionAfter($question);

        if (is_null($nextQuestion)) {
            return $this->_responseWithXmlType($this->_smsMessageAfterLastQuestion());
        } else {
            return $this->_responseWithXmlType(
                $this->_redirectToQuestion($nextQuestion, 'question.show.sms')
            );
        }
    }

    public function storeTranscription($surveyId, $questionId, Request $request)
    {
        $callSid = $request->input('CallSid');
        $question = Question::find($questionId);
        $questionResponse = $question->responses()->where('session_sid', $callSid)->firstOrFail();
        $questionResponse->responseTranscription()->create(
            ['transcription' => $this->_transcriptionMessageIfCompleted($request)]
        );
    }

    private function _responseFromVoiceRequest($question, $request)
    {
        if ($question->kind === 'free-answer') {
            return $request->input('RecordingUrl');
        } else {
            return $request->input('Digits');
        }
    }

    private function _questionAfter($question)
    {
        $survey = Survey::find($question->survey_id);
        $allQuestions = $survey->questions()->orderBy('id', 'asc')->get();
        $position = $allQuestions->search($question);
        $nextQuestion = $allQuestions->get($position + 1);
        return $nextQuestion;
    }

    private function _redirectToQuestion($question, $route)
    {
        $questionUrl = route(
            $route,
            ['question' => $question->id, 'survey' => $question->survey->id]
        );
        $redirectResponse = new Twiml();
        $redirectResponse->redirect($questionUrl, ['method' => 'GET']);

        return response($redirectResponse);
    }

    private function _voiceMessageAfterLastQuestion()
    {
        $voiceResponse = new Twiml();
        $voiceResponse->say('That was the last question');
        $voiceResponse->say('Thank you for participating in this survey');
        $voiceResponse->say('Good-bye');
        $voiceResponse->hangup();

        return response($voiceResponse);
    }

    private function _smsMessageAfterLastQuestion() {
        $messageResponse = new Twiml();
        $messageResponse->message(
            "That was the last question.\n" .
            "Thank you for participating in this survey.\n" .
            'Good bye.'
        );
        return response($messageResponse)
                   ->withCookie(Cookie::forget('survey_session'))
                   ->withCookie(Cookie::forget('current_question'));
    }

    private function _transcriptionMessageIfCompleted($request)
    {
        if ($request->input('TranscriptionStatus') === 'completed') {
            return $request->input('TranscriptionText');
        }
        return 'An error occurred while transcribing the answer';
    }

    private function _responseWithXmlType($response)
    {
        return $response->header('Content-Type', 'application/xml');
    }
}
app/Http/Controllers/QuestionResponseController.php
Store voice, SMS, or a transcription of survey answers

app/Http/Controllers/QuestionResponseController.php

Now, let's see how to visualize the results.

Displaying the Survey Results

You can access the survey results from the application's root route.

The results will be shown grouped by survey. To hear recorded answers, you can click on a playback icon to listen to the user's response. Any transcribed voice answers will be shown when available.

Loading Code Samples...
Language
SDK Version:
  • blade
@extends('layouts.master')

@section('content')
<h1>Results for survey: {{ $survey-&gt;title }}</h1>
<div class="col-md-12">
    <ul class="list-unstyled">
        @foreach ($responses as $response)
        <li>
            <div class="panel panel-default">
                <div class="panel-heading">
                    Response from: {{ $response-&gt;first()-&gt;session_sid }}
                    </br>
                    Survey type:
                    @if($response->first()->type == 'voice')
                    <span class="label label-primary">
                    @else
                    <span class="label label-success">
                    @endif
                        {{ $response-&gt;first()-&gt;type }}
                    </span>
                </div>
                <div class="panel-body">
                    @foreach ($response as $questionResponse)
                    <ol class="list-group">
                        <li class="list-group-item">Question: {{ $questionResponse-&gt;question-&gt;body }}</li>
                        <li class="list-group-item">Answer type: {{ $questionResponse-&gt;question-&gt;kind }}</li>
                        <li class="list-group-item">
                            @if($questionResponse->question->kind === 'free-answer' && $questionResponse->type === 'voice')
                            <div class="voice-response">
                                <span class="voice-response-text">Response:</span>
                                <i class="fa fa-play-circle fa-2x play-icon"></i>
                                <audio class="voice-response" src="{{ $questionResponse-&gt;response }}"></audio>
                            </div>
                            @elseif($questionResponse->question->kind === 'yes-no')
                                @if($questionResponse->response == 1)
                                YES
                                @else
                                NO
                                @endif
                            @else
                            {{ $questionResponse-&gt;response }}
                            @endif
                        </li>
                        @if(!is_null($questionResponse->transcription))
                        <li class="list-group-item">Transcribed Answer: {{ $questionResponse-&gt;transcription }}</li>
                        @endif
                    </ol>
                    @endforeach
                </div>
            </div>
        </li>
        @endforeach
    </ul>
</div>
@stop
resources/views/surveys/results.blade.php
Display the Survey Results

resources/views/surveys/results.blade.php

That's it!

If you have configured one of your Twilio numbers for the application built in this tutorial you should be able to take the survey and see the results under the root route of the application. We hope you found this sample application useful.

Where to next?

If you're a PHP developer working with Twilio, you might enjoy these other tutorials:

Click to Call

Put a button on your web page that connects visitors to live support or salespeople via telephone.

Two-Factor Authentication

Improve the security of Laravel's built-in login functionality by adding two-factor authentication via text message.

Did this help?

Thanks for checking this tutorial out! If you have any feedback to share with us, we'd love to hear it. Reach out to us on Twitter and let us know what you build!

Orlando Hidalgo
Jose Oliveros
Kat King
Andrew Baker
Hector Ortega

Need some help?

We all do sometimes; code is hard. Get help now from our support team, or lean on the wisdom of the crowd browsing the Twilio tag on Stack Overflow.

1 / 1
Loading Code Samples...
<?php

use Illuminate\Http\RedirectResponse;

/*
|--------------------------------------------------------------------------
| Application Routes
|--------------------------------------------------------------------------
|
| Here is where you can register all of the routes for an application.
| It's a breeze. Simply tell Laravel the URIs it should respond to
| and give it the controller to call when that URI is requested.
|
*/

Route::get(
    '/survey/{survey}/results',
    ['as' => 'survey.results', 'uses' => 'SurveyController@showResults']
);
Route::get(
    '/',
    ['as' => 'root', 'uses' => 'SurveyController@showFirstSurveyResults']
);
Route::post(
    '/voice/connect',
    ['as' => 'voice.connect', 'uses' => 'SurveyController@connectVoice']
);
Route::post(
    '/sms/connect',
    ['as' => 'sms.connect', 'uses' => 'SurveyController@connectSms']
);
Route::get(
    '/survey/{id}/voice',
    ['as' => 'survey.show.voice', 'uses' => 'SurveyController@showVoice']
);
Route::get(
    '/survey/{id}/sms',
    ['as' => 'survey.show.sms', 'uses' => 'SurveyController@showSms']
);
Route::get(
    '/survey/{survey}/question/{question}/voice',
    ['as' => 'question.show.voice', 'uses' => 'QuestionController@showVoice']
);
Route::get(
    '/survey/{survey}/question/{question}/sms',
    ['as' => 'question.show.sms', 'uses' => 'QuestionController@showSms']
);
Route::post(
    '/survey/{survey}/question/{question}/response/voice',
    ['as' => 'response.store.voice', 'uses' => 'QuestionResponseController@storeVoice']
);
Route::post(
    '/survey/{survey}/question/{question}/response/sms',
    ['as' => 'response.store.sms', 'uses' => 'QuestionResponseController@storeSms']
);
Route::post(
    '/survey/{survey}/question/{question}/response/transcription',
    ['as' => 'response.transcription.store', 'uses' => 'QuestionResponseController@storeTranscription']
);
<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;

class LoadSurveys extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'surveys:load {fileName}';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Load surveys into the database';

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Execute the console command.
     *
     * @return void
     */
    public function handle()
    {
        $filename = $this->argument('fileName');
        $surveyJSON = file_get_contents($filename);

        $parser = new \App\Twilio\SurveyParser($surveyJSON);

        $survey = new \App\Survey();
        $survey->title = $parser->title();
        $survey->save();

        $parser->questions()->each(
            function ($question) use ($survey) {
                $questionToSave = new \App\Question($question);
                $questionToSave->survey()->associate($survey);
                $questionToSave->save();
            }
        );
    }
}
<?php

use Illuminate\Http\RedirectResponse;

/*
|--------------------------------------------------------------------------
| Application Routes
|--------------------------------------------------------------------------
|
| Here is where you can register all of the routes for an application.
| It's a breeze. Simply tell Laravel the URIs it should respond to
| and give it the controller to call when that URI is requested.
|
*/

Route::get(
    '/survey/{survey}/results',
    ['as' => 'survey.results', 'uses' => 'SurveyController@showResults']
);
Route::get(
    '/',
    ['as' => 'root', 'uses' => 'SurveyController@showFirstSurveyResults']
);
Route::post(
    '/voice/connect',
    ['as' => 'voice.connect', 'uses' => 'SurveyController@connectVoice']
);
Route::post(
    '/sms/connect',
    ['as' => 'sms.connect', 'uses' => 'SurveyController@connectSms']
);
Route::get(
    '/survey/{id}/voice',
    ['as' => 'survey.show.voice', 'uses' => 'SurveyController@showVoice']
);
Route::get(
    '/survey/{id}/sms',
    ['as' => 'survey.show.sms', 'uses' => 'SurveyController@showSms']
);
Route::get(
    '/survey/{survey}/question/{question}/voice',
    ['as' => 'question.show.voice', 'uses' => 'QuestionController@showVoice']
);
Route::get(
    '/survey/{survey}/question/{question}/sms',
    ['as' => 'question.show.sms', 'uses' => 'QuestionController@showSms']
);
Route::post(
    '/survey/{survey}/question/{question}/response/voice',
    ['as' => 'response.store.voice', 'uses' => 'QuestionResponseController@storeVoice']
);
Route::post(
    '/survey/{survey}/question/{question}/response/sms',
    ['as' => 'response.store.sms', 'uses' => 'QuestionResponseController@storeSms']
);
Route::post(
    '/survey/{survey}/question/{question}/response/transcription',
    ['as' => 'response.transcription.store', 'uses' => 'QuestionResponseController@storeTranscription']
);
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Http\Response;

use App\Http\Requests;
use App\Http\Controllers\Controller;
use App\Survey;
use App\QuestionResponse;

use Twilio\Twiml;

class SurveyController extends Controller
{
    const START_SMS_SURVEY_COMMAND = 'start';

    public function showResults($surveyId)
    {
        $survey = Survey::find($surveyId);
        $responsesByCall = QuestionResponse::responsesForSurveyByCall($surveyId)
                         ->get()
                         ->groupBy('session_sid')
                         ->values();

        return response()->view(
            'surveys.results',
            ['survey' => $survey, 'responses' => $responsesByCall]
        );
    }

    public function showFirstSurveyResults()
    {
        $firstSurvey = $this->_getFirstSurvey();
        return redirect(route('survey.results', ['survey' => $firstSurvey->id]))
                ->setStatusCode(303);
    }

    public function connectVoice()
    {
        $response = new Twiml();
        $redirectResponse = $this->_redirectWithFirstSurvey('survey.show.voice', $response);
        return $this->_responseWithXmlType($redirectResponse);
    }

    /**
     * Display the specified resource.
     *
     * @param  int  $id
     * @return Response
     */
    public function showVoice($id)
    {
        $surveyToTake = Survey::find($id);
        $voiceResponse = new Twiml();

        if (is_null($surveyToTake)) {
            return $this->_responseWithXmlType($this->_noSuchVoiceSurvey($voiceResponse));
        }
        $surveyTitle = $surveyToTake->title;
        $voiceResponse->say("Hello and thank you for taking the $surveyTitle survey!");
        $voiceResponse->redirect($this->_urlForFirstQuestion($surveyToTake, 'voice'), ['method' => 'GET']);

        return $this->_responseWithXmlType(response($voiceResponse));
    }

    /**
     * Display the specified resource.
     *
     * @param  int  $id
     * @return Response
     */
    public function showSms($id)
    {
        $surveyToTake = Survey::find($id);
        $voiceResponse = new Twiml();

        if (is_null($surveyToTake)) {
            return $this->_responseWithXmlType($this->_noSuchSmsSurvey($voiceResponse));
        }

        $surveyTitle = $surveyToTake->title;
        $voiceResponse->message("Hello and thank you for taking the $surveyTitle survey!");
        $voiceResponse->redirect($this->_urlForFirstQuestion($surveyToTake, 'sms'), ['method' => 'GET']);

        return $this->_responseWithXmlType(response($voiceResponse));
    }

    public function connectSms(Request $request)
    {
        $response = $this->_getNextSmsStepFromCookies($request);
        return $this->_responseWithXmlType($response);
    }

    private function _getNextSmsStepFromCookies($request) {
        $response = new Twiml();
        if (strtolower(trim($request->input('Body'))) === self::START_SMS_SURVEY_COMMAND) {
            $messageSid = $request->input('MessageSid');

            return $this->_redirectWithFirstSurvey('survey.show.sms', $response)
                        ->withCookie('survey_session', $messageSid);
        }

        $currentQuestion = $request->cookie('current_question');
        $surveySession = $request->cookie('survey_session');

        if ($this->_noActiveSurvey($currentQuestion, $surveySession)) {
            return $this->_smsSuggestCommand($response);
        }

        return $this->_redirectToStoreSmsResponse($response, $currentQuestion);
    }

    private function _redirectWithFirstSurvey($routeName, $response)
    {
        $firstSurvey = $this->_getFirstSurvey();

        if (is_null($firstSurvey)) {
            if ($routeName === 'survey.show.voice') {
                return $this->_noSuchVoiceSurvey($response);
            }
            return $this->_noSuchSmsSurvey($response);
        }

        $response->redirect(
            route($routeName, ['id' => $firstSurvey->id]),
            ['method' => 'GET']
        );
        return response($response);
    }

    private function _noActiveSurvey($currentQuestion, $surveySession) {
        $noCurrentQuestion = is_null($currentQuestion) || $currentQuestion == 'deleted';
        $noSurveySession = is_null($surveySession) || $surveySession == 'deleted';

        return $noCurrentQuestion || $noSurveySession;
    }

    private function _redirectToStoreSmsResponse($response, $currentQuestion) {
        $firstSurvey = $this->_getFirstSurvey();
        $storeRoute = route('response.store.sms', ['survey' => $firstSurvey->id, 'question' => $currentQuestion]);
        $response->redirect($storeRoute, ['method' => 'POST']);

        return response($response);
    }

    private function _smsSuggestCommand($response) {
        $response->message('You have no active surveys. Reply with "Start" to begin.');
        return response($response);
    }

    private function _noSuchSmsSurvey($messageResponse)
    {
        $messageResponse->message('Sorry, we could not find the survey to take. Good-bye');
        return response($messageResponse);
    }

    private function _urlForFirstQuestion($survey, $routeType)
    {
        return route(
            'question.show.' . $routeType,
            ['survey' => $survey->id,
             'question' => $survey->questions()->orderBy('id')->first()->id]
        );
    }

    private function _noSuchVoiceSurvey($voiceResponse)
    {
        $voiceResponse->say('Sorry, we could not find the survey to take');
        $voiceResponse->say('Good-bye');
        $voiceResponse->hangup();

        return response($voiceResponse);
    }

    private function _getFirstSurvey() {
        return Survey::orderBy('id', 'DESC')->get()->first();
    }

    private function _responseWithXmlType($response) {
        return $response->header('Content-Type', 'application/xml');
    }
}
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

use App\Http\Requests;
use App\Http\Controllers\Controller;

use App\Question;
use Twilio\Twiml;

class QuestionController extends Controller
{
    /**
     * Display the specified resource.
     *
     * @param  int  $id
     * @return Response
     */
    public function showVoice($surveyId, $questionId)
    {
        $questionToAsk = Question::find($questionId);
        return $this->_responseWithXmlType($this->_commandForVoice($questionToAsk));
    }

    /**
     * Display the specified resource.
     *
     * @param  int  $id
     * @return Response
     */
    public function showSms($surveyId, $questionId)
    {
        $questionToAsk = Question::find($questionId);
        return $this->_responseWithXmlType($this->_commandForSms($questionToAsk));
    }

    private function _messageForSmsQuestion($question) {
        $questionPhrases = collect(
            [
                'free-answer' => "\n\nReply to this message with your answer",
                'yes-no'      => "\n\nReply with \"YES\" or \"NO\" to this message",
                'numeric'     => "\n\nReply with a number from 1 to 10 to this message"
            ]
        );

        return $questionPhrases->get($question->kind, "\n\nReply to this message with your answer");
    }

    private function _messageForVoiceQuestion($question)
    {
        $questionPhrases = collect(
            [
                'free-answer' => "Please record your answer after the beep and then hit the pound sign",
                'yes-no'      => "Please press the one key for yes and the zero key for no and then hit the pound sign",
                'numeric'     => "Please press a number between 1 and 10 and then hit the pound sign"
            ]
        );

        return $questionPhrases->get($question->kind, "Please press a number and then the pound sign");
    }

    private function _commandForSms($question)
    {
        $smsResponse = new Twiml();

        $messageBody = $question->body . $this->_messageForSmsQuestion($question);
        $smsResponse->message($messageBody);

        return response($smsResponse)->withCookie('current_question', $question->id);
    }

    private function _commandForVoice($question)
    {
        $voiceResponse = new Twiml();

        $voiceResponse->say($question->body);
        $voiceResponse->say($this->_messageForVoiceQuestion($question));
        $voiceResponse = $this->_registerResponseCommand($voiceResponse, $question);

        return response($voiceResponse);
    }

    private function _registerResponseCommand($voiceResponse, $question)
    {
        $storeResponseURL = route(
            'response.store.voice',
            ['question' => $question->id,
             'survey' => $question->survey->id],
            false
        );

        if ($question->kind === 'free-answer') {
            $transcribeUrl = route(
                'response.transcription.store',
                ['question' => $question->id,
                 'survey' => $question->survey->id]
            );
            $voiceResponse->record(
                ['method' => 'POST',
                 'action' => $storeResponseURL,
                 'transcribe' => true,
                 'transcribeCallback' => $transcribeUrl]
            );
        } elseif ($question->kind === 'yes-no') {
            $voiceResponse->gather(['method' => 'POST', 'action' => $storeResponseURL]);
        } elseif ($question->kind === 'numeric') {
            $voiceResponse->gather(['method' => 'POST', 'action' => $storeResponseURL]);
        }
        return $voiceResponse;
    }

    private function _responseWithXmlType($response) {
        return $response->header('Content-Type', 'application/xml');
    }
}
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

use App\Http\Requests;
use App\Http\Controllers\Controller;

use App\Question;
use Twilio\Twiml;

class QuestionController extends Controller
{
    /**
     * Display the specified resource.
     *
     * @param  int  $id
     * @return Response
     */
    public function showVoice($surveyId, $questionId)
    {
        $questionToAsk = Question::find($questionId);
        return $this->_responseWithXmlType($this->_commandForVoice($questionToAsk));
    }

    /**
     * Display the specified resource.
     *
     * @param  int  $id
     * @return Response
     */
    public function showSms($surveyId, $questionId)
    {
        $questionToAsk = Question::find($questionId);
        return $this->_responseWithXmlType($this->_commandForSms($questionToAsk));
    }

    private function _messageForSmsQuestion($question) {
        $questionPhrases = collect(
            [
                'free-answer' => "\n\nReply to this message with your answer",
                'yes-no'      => "\n\nReply with \"YES\" or \"NO\" to this message",
                'numeric'     => "\n\nReply with a number from 1 to 10 to this message"
            ]
        );

        return $questionPhrases->get($question->kind, "\n\nReply to this message with your answer");
    }

    private function _messageForVoiceQuestion($question)
    {
        $questionPhrases = collect(
            [
                'free-answer' => "Please record your answer after the beep and then hit the pound sign",
                'yes-no'      => "Please press the one key for yes and the zero key for no and then hit the pound sign",
                'numeric'     => "Please press a number between 1 and 10 and then hit the pound sign"
            ]
        );

        return $questionPhrases->get($question->kind, "Please press a number and then the pound sign");
    }

    private function _commandForSms($question)
    {
        $smsResponse = new Twiml();

        $messageBody = $question->body . $this->_messageForSmsQuestion($question);
        $smsResponse->message($messageBody);

        return response($smsResponse)->withCookie('current_question', $question->id);
    }

    private function _commandForVoice($question)
    {
        $voiceResponse = new Twiml();

        $voiceResponse->say($question->body);
        $voiceResponse->say($this->_messageForVoiceQuestion($question));
        $voiceResponse = $this->_registerResponseCommand($voiceResponse, $question);

        return response($voiceResponse);
    }

    private function _registerResponseCommand($voiceResponse, $question)
    {
        $storeResponseURL = route(
            'response.store.voice',
            ['question' => $question->id,
             'survey' => $question->survey->id],
            false
        );

        if ($question->kind === 'free-answer') {
            $transcribeUrl = route(
                'response.transcription.store',
                ['question' => $question->id,
                 'survey' => $question->survey->id]
            );
            $voiceResponse->record(
                ['method' => 'POST',
                 'action' => $storeResponseURL,
                 'transcribe' => true,
                 'transcribeCallback' => $transcribeUrl]
            );
        } elseif ($question->kind === 'yes-no') {
            $voiceResponse->gather(['method' => 'POST', 'action' => $storeResponseURL]);
        } elseif ($question->kind === 'numeric') {
            $voiceResponse->gather(['method' => 'POST', 'action' => $storeResponseURL]);
        }
        return $voiceResponse;
    }

    private function _responseWithXmlType($response) {
        return $response->header('Content-Type', 'application/xml');
    }
}
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

use App\Http\Requests;
use App\Http\Controllers\Controller;
use App\Question;
use App\Survey;
use App\QuestionResponse;
use App\ResponseTranscription;
use Twilio\Twiml;
use Cookie;

class QuestionResponseController extends Controller
{
    /**
     * Store a newly created resource in storage.
     *
     * @param  Request  $request
     * @return Response
     */
    public function storeVoice($surveyId, $questionId, Request $request)
    {
        $question = Question::find($questionId);
        $newResponse = $question->responses()->create(
            ['response' => $this->_responseFromVoiceRequest($question, $request),
             'type' => 'voice',
             'session_sid' => $request->input('CallSid')]
        );

        $nextQuestion = $this->_questionAfter($question);

        if (is_null($nextQuestion)) {
            return $this->_responseWithXmlType($this->_voiceMessageAfterLastQuestion());
        } else {
            return $this->_responseWithXmlType(
                $this->_redirectToQuestion($nextQuestion, 'question.show.voice')
            );
        }
    }

    /**
     * Store a newly created resource in storage.
     *
     * @param  Request  $request
     * @return Response
     */
    public function storeSms($surveyId, $questionId, Request $request)
    {
        $answer = trim($request->input('Body'));
        $question = Question::find($questionId);
        if ($question->kind === 'yes-no') {
            $answer = strtolower($answer) === 'yes' ? 1 : 0;
        }
        $newResponse = $question->responses()->create(
            ['response' => $answer,
             'type' => 'sms',
             'session_sid' => $request->cookie('survey_session')]
        );

        $nextQuestion = $this->_questionAfter($question);

        if (is_null($nextQuestion)) {
            return $this->_responseWithXmlType($this->_smsMessageAfterLastQuestion());
        } else {
            return $this->_responseWithXmlType(
                $this->_redirectToQuestion($nextQuestion, 'question.show.sms')
            );
        }
    }

    public function storeTranscription($surveyId, $questionId, Request $request)
    {
        $callSid = $request->input('CallSid');
        $question = Question::find($questionId);
        $questionResponse = $question->responses()->where('session_sid', $callSid)->firstOrFail();
        $questionResponse->responseTranscription()->create(
            ['transcription' => $this->_transcriptionMessageIfCompleted($request)]
        );
    }

    private function _responseFromVoiceRequest($question, $request)
    {
        if ($question->kind === 'free-answer') {
            return $request->input('RecordingUrl');
        } else {
            return $request->input('Digits');
        }
    }

    private function _questionAfter($question)
    {
        $survey = Survey::find($question->survey_id);
        $allQuestions = $survey->questions()->orderBy('id', 'asc')->get();
        $position = $allQuestions->search($question);
        $nextQuestion = $allQuestions->get($position + 1);
        return $nextQuestion;
    }

    private function _redirectToQuestion($question, $route)
    {
        $questionUrl = route(
            $route,
            ['question' => $question->id, 'survey' => $question->survey->id]
        );
        $redirectResponse = new Twiml();
        $redirectResponse->redirect($questionUrl, ['method' => 'GET']);

        return response($redirectResponse);
    }

    private function _voiceMessageAfterLastQuestion()
    {
        $voiceResponse = new Twiml();
        $voiceResponse->say('That was the last question');
        $voiceResponse->say('Thank you for participating in this survey');
        $voiceResponse->say('Good-bye');
        $voiceResponse->hangup();

        return response($voiceResponse);
    }

    private function _smsMessageAfterLastQuestion() {
        $messageResponse = new Twiml();
        $messageResponse->message(
            "That was the last question.\n" .
            "Thank you for participating in this survey.\n" .
            'Good bye.'
        );
        return response($messageResponse)
                   ->withCookie(Cookie::forget('survey_session'))
                   ->withCookie(Cookie::forget('current_question'));
    }

    private function _transcriptionMessageIfCompleted($request)
    {
        if ($request->input('TranscriptionStatus') === 'completed') {
            return $request->input('TranscriptionText');
        }
        return 'An error occurred while transcribing the answer';
    }

    private function _responseWithXmlType($response)
    {
        return $response->header('Content-Type', 'application/xml');
    }
}
SDK Version:
  • blade
@extends('layouts.master')

@section('content')
<h1>Results for survey: {{ $survey-&gt;title }}</h1>
<div class="col-md-12">
    <ul class="list-unstyled">
        @foreach ($responses as $response)
        <li>
            <div class="panel panel-default">
                <div class="panel-heading">
                    Response from: {{ $response-&gt;first()-&gt;session_sid }}
                    </br>
                    Survey type:
                    @if($response->first()->type == 'voice')
                    <span class="label label-primary">
                    @else
                    <span class="label label-success">
                    @endif
                        {{ $response-&gt;first()-&gt;type }}
                    </span>
                </div>
                <div class="panel-body">
                    @foreach ($response as $questionResponse)
                    <ol class="list-group">
                        <li class="list-group-item">Question: {{ $questionResponse-&gt;question-&gt;body }}</li>
                        <li class="list-group-item">Answer type: {{ $questionResponse-&gt;question-&gt;kind }}</li>
                        <li class="list-group-item">
                            @if($questionResponse->question->kind === 'free-answer' && $questionResponse->type === 'voice')
                            <div class="voice-response">
                                <span class="voice-response-text">Response:</span>
                                <i class="fa fa-play-circle fa-2x play-icon"></i>
                                <audio class="voice-response" src="{{ $questionResponse-&gt;response }}"></audio>
                            </div>
                            @elseif($questionResponse->question->kind === 'yes-no')
                                @if($questionResponse->response == 1)
                                YES
                                @else
                                NO
                                @endif
                            @else
                            {{ $questionResponse-&gt;response }}
                            @endif
                        </li>
                        @if(!is_null($questionResponse->transcription))
                        <li class="list-group-item">Transcribed Answer: {{ $questionResponse-&gt;transcription }}</li>
                        @endif
                    </ol>
                    @endforeach
                </div>
            </div>
        </li>
        @endforeach
    </ul>
</div>
@stop