Hello, Laravel? Communicating with PHP through Phone Calls!

Share this article

Hello, Laravel? Communicating with PHP through Phone Calls!

Twilio is a SaaS application which enables developers to build telephone applications using web technologies. In this two-part series, we will leverage Twilio to build a weather forecast app that is accessed using the telephone system. The backend will be written with the Laravel framework (an exploratory video course is available for purchase here.

In this part, we will create a simple program that will allow a user to call a phone number that we buy from Twilio, enter a zipcode, and receive the current weather forecast. The user can also get the weather for any day of the week via the voice menu prompts. In the second part of this series, we will leverage what was built in this article to allow the user to interact with the app via SMS (text message).

Prerequisites

Development Environment

This article assumes Homestead Improved is installed. It is not necessary to use it, but the commands might differ slightly if you use a different environment. If you are not familiar with Homestead and want to produce similar results as this article aims to produce, please visit this SitePoint article that shows how to set up Homestead, and if you need a crash course in Vagrant, please see this post. Additionally, if this whets your appetite and you feel like exploring PHP development environments in depth, we have a book about that available for purchase.

Dependencies

We will create a new Laravel project and then add the Twilio PHP SDK and Guzzle HTTP client library to the project:

cd ~/Code
composer create-project --prefer-dist laravel/laravel Laravel 5.4.*
cd Laravel
composer require "twilio/sdk:^5.7"
composer require "guzzlehttp/guzzle:~6.0"

Development

Let’s go through all the steps, one by one.

Routes

Open up the routes/web.php file and add the following ones:

Route::group(['prefix' => 'voice', 'middleware' => 'twilio'], function () {
    Route::post('enterZipcode', 'VoiceController@showEnterZipcode')->name('enter-zip');

    Route::post('zipcodeWeather', 'VoiceController@showZipcodeWeather')->name('zip-weather');

    Route::post('dayWeather', 'VoiceController@showDayWeather')->name('day-weather');

    Route::post('credits', 'VoiceController@showCredits')->name('credits');
});

In this app, all requests will be under the /voice path. When Twilio first connects to the app, it will go to /voice/enterZipcode via HTTP POST. Depending on what happens in the telephone call, Twilio will make requests to other endpoints. This includes /voice/zipcodeWeather for providing today’s forecast, /voice/dayWeather, for providing a particular day’s forecast, and /voice/credits for providing information on where the data came from.

Service Layer

We are going to add a service class. This class will hold a lot of the business logic that will be shared between the voice telephone app and the SMS app.

Create a new sub-folder called Services inside the app folder. Then, create a file called WeatherService.php and put the following content into it:

<?php

namespace App\Services;

use Illuminate\Support\Facades\Cache;
use Twilio\Twiml;

class WeatherService
{
}

This is a large file in the project, so we will build it piece by piece. Put the following pieces of code in this section inside our new service class:

    public $daysOfWeek = [
        'Today',
        'Sunday',
        'Monday',
        'Tuesday',
        'Wednesday',
        'Thursday',
        'Friday',
        'Saturday'
    ];

We will use this array to map a day of the week to a number; Sunday = 1, Monday = 2, etc.

    public function getWeather($zip, $dayName)
    {

        $point = $this->getPoint($zip);
        $tz = $this->getTimeZone($point);
        $forecast = $this->retrieveNwsData($zip);
        $ts = $this->getTimestamp($dayName, $zip);

        $tzObj = new \DateTimeZone($tz->timezoneId);

        $tsObj = new \DateTime(null, $tzObj);
        $tsObj->setTimestamp($ts);

        foreach ($forecast->properties->periods as $k => $period) {
            $startTs = strtotime($period->startTime);
            $endTs = strtotime($period->endTime);

            if ($ts > $startTs and $ts < $endTs) {
                $day = $period;
                break;
            }
        }

        $response = new Twiml();

        $weather = $day->name;
        $weather .= ' the ' . $tsObj->format('jS') . ': ';
        $weather .= $day->detailedForecast;

        $gather = $response->gather(
            [
                'numDigits' => 1,
                'action' => route('day-weather', [], false)
            ]
        );

        $menuText = ' ';
        $menuText .= "Press 1 for Sunday, 2 for Monday, 3 for Tuesday, ";
        $menuText .= "4 for Wednesday, 5 for Thursday, 6 for Friday, ";
        $menuText .= "7 for Saturday. Press 8 for the credits. ";
        $menuText .= "Press 9 to enter in a new zipcode. ";
        $menuText .= "Press 0 to hang up.";

        $gather->say($weather . $menuText);

        return $response;
    }

The getWeather method takes a zipcode with the day of the week and crafts the text of a weather forecast. First, it figures out the reference time for the day requested, and then looks up the weather forecast by doing a foreach over the array of forecast data. After that, it returns a Voice TwiML response. Below is a sample of what is returned:

<?xml version="1.0" encoding="UTF-8"?>
<Response>
  <Gather numDigits="1" action="/voice/dayWeather">
    <Say>
      This Afternoon the 31st: Sunny, with a high near 72. South southwest wind around 8 mph. Press 1 for Sunday, 2 for Monday, 3 for Tuesday, 4 for Wednesday, 5 for Thursday, 6 for Friday, 7 for Saturday. Press 8 for the credits. Press 9 to enter in a new zipcode. Press 0 to hang up.
    </Say>
  </Gather>
</Response>

The <Gather> tag tells Twilio to expect input from the user’s keypad. The numDigits attribute says how many digits to expect. The action attribute says what endpoint to contact next.

    protected function retrieveNwsData($zip)
    {
        return Cache::remember('weather:' . $zip, 60, function () use ($zip) {
            $point = $this->getPoint($zip);

            $point = $point->lat . ',' . $point->lng;
            $url = 'https://api.weather.gov/points/' . $point . '/forecast';

            $client = new \GuzzleHttp\Client();

            $response = $client->request('GET', $url, [
                'headers' => [
                    'Accept' => 'application/geo+json',
                ]
            ]);

            return json_decode((string)$response->getBody());
        });
    }

The retrieveNwsData method gets the weather forecast data. First, the method checks to see if a copy of the zipcode’s weather forecast is in cache. If not, then the Guzzle HTTP client is used to make an HTTP GET request to the National Weather Service’s (NWS) API endpoint https://api.weather.gov/points/{point}/forecast. To get the geographic point of the zipcode, a call is made to the getPoint method before doing the request to the weather API. The response from the API endpoint is the weather forecast in GeoJSON format. The forecast is for every day and night for a week (with some exceptions we will discuss later); 14 entries in all. We cache the API response for an hour because making the request is slow, plus we do not want to hit the government servers too frequently and get banned.

    protected function getPoint($zip)
    {
        return Cache::remember('latLng:' . $zip, 1440, function () use ($zip) {
            $client = new \GuzzleHttp\Client();
            $url = 'http://api.geonames.org/postalCodeSearchJSON';

            $response = $client->request('GET', $url, [
                'query' => [
                    'postalcode' => $zip,
                    'countryBias' => 'US',
                    'username' => env('GEONAMES_USERNAME')
                ]
            ]);

            $json = json_decode((string)$response->getBody());

            return $json->postalCodes[0];
        });
    }

The getPoint method maps a zipcode to a geographic point. This is done by using the GeoNames API. The results are cached for a day because using the API is slow.

    protected function getTimeZone($point)
    {
        $key = 'timezone:' . $point->lat . ',' . $point->lng;

        return Cache::remember($key, 1440, function () use ($point) {
            $client = new \GuzzleHttp\Client();
            $url = 'http://api.geonames.org/timezoneJSON';

            $response = $client->request('GET', $url, [
                'query' => [
                    'lat' => $point->lat,
                    'lng' => $point->lng,
                    'username' => env('GEONAMES_USERNAME')
                ]
            ]);

            return json_decode((string) $response->getBody());
        });
    }

The getTimeZone method is used to get the timezone that a geographic point resides inside. The GeoNames API is also used and the results are cached for a day for the same reasons.

    protected function getTimestamp($day, $zip)
    {
        $point = $this->getPoint($zip);
        $tz = $this->getTimeZone($point);

        $tzObj = new \DateTimeZone($tz->timezoneId);

        $now = new \DateTime(null, $tzObj);

        $hourNow = $now->format('G');
        $dayNow = $now->format('l');

        if ($day == $dayNow and $hourNow >= 18) {
            $time = new \DateTime('next ' . $day . ' noon', $tzObj);
            $ts = $time->getTimestamp();
        } elseif (($day == 'Today' or $day == $dayNow) and $hourNow >= 6) {
            $ts = $now->getTimestamp();
        } else {
            $time = new \DateTime($day . ' noon', $tzObj);
            $ts = $time->getTimestamp();
        }

        return $ts;
    }

The getTimestamp method returns a reference time that is used to look up a forecast for a particular date. Most of the time, the forecast data has a day and night forecast, but sometimes it has an overnight (before 6 am), and an afternoon forecast (the afternoon, before 6 pm) for the current day. Because of this, we have to do some calculations to get a good reference timestamp. In most cases, it returns the zipcode’s noon time for the day requested.

    public function getCredits()
    {
        $credits = "Weather data provided by the National Weather Service. ";
        $credits .= "Zipcode data provided by GeoNames.";

        return $credits;
    }
}

The getCredits method just returns some standard text about where the data came from.

Controller

Create the file VoiceController.php in the app/Http/Controllers folder and put the following code into it:

<?php

namespace App\Http\Controllers;

use App\Services\WeatherService;
use Illuminate\Http\Request;
use Twilio\Twiml;

class VoiceController extends Controller
{
    protected $weather;

    public function __construct(WeatherService $weatherService)
    {
        $this->weather = $weatherService;
    }

    public function showEnterZipcode()
    {
        $response = new Twiml();

        $gather = $response->gather(
            [
                'numDigits' => 5,
                'action' => route('zip-weather', [], false)
            ]
        );

        $gather->say('Enter the zipcode for the weather you want');

        return $response;
    }

    public function showZipcodeWeather(Request $request)
    {
        $zip = $request->input('Digits');

        $request->session()->put('zipcode', $zip);

        return $this->weather->getWeather($zip, 'Today');
    }

    public function showDayWeather(Request $request)
    {
        $digit = $request->input('Digits', '0');

        switch ($digit) {
            case '8':
                $response = new Twiml();
                $response->redirect(route('credits', [], false));
                break;
            case '9':
                $response = new Twiml();
                $response->redirect(route('enter-zip', [], false));
                break;
            case '0':
                $response = new Twiml();
                $response->hangup();
                break;
            default:
                $zip = $request->session()->get('zipcode');
                $day = $this->weather->daysOfWeek[$digit];
                $response = $this->weather->getWeather($zip, $day);
                break;
        }

        return $response;
    }

    public function showCredits()
    {
        $response = new Twiml();
        $credits = $this->weather->getCredits();

        $response->say($credits);
        $response->hangup();

        return $response;
    }
}

The showEnterZipcode method is executed when a request is made to the /voice/enterZipcode endpoint. This method returns TwiML that asks the caller to enter a zipcode. The TwiML also says a request to /voice/zipcodeWeather should be made when the caller has entered 5 digits. Here is a sample response:

<?xml version="1.0" encoding="UTF-8"?>
<Response>
  <Gather numDigits="5" action="/voice/zipcodeWeather">
    <Say>
      Enter the zipcode for the weather you want
    </Say>
  </Gather>
</Response>

The showZipcodeWeather method is executed when a request is made to the /voice/zipcodeWeather endpoint. This method returns the text of today’s forecast and a voice menu to navigate the app in TwiML format. Below is what a response looks like:

<?xml version="1.0" encoding="UTF-8"?>
<Response>
  <Gather numDigits="1" action="/voice/dayWeather">
    <Say>
      This Afternoon the 31st: Sunny, with a high near 72. South southwest wind around 8 mph. Press 1 for Sunday, 2 for Monday, 3 for Tuesday, 4 for Wednesday, 5 for Thursday, 6 for Friday, 7 for Saturday. Press 8 for the credits. Press 9 to enter in a new zipcode. Press 0 to hang up.
    </Say>
  </Gather>
</Response>

When the /voice/dayWeather endpoint is requested, the showDayWeather method is executed. This returns the forecast for the day requested and a voice menu to navigate the app in TwiML format. The response for a Monday might look like this:

<?xml version="1.0" encoding="UTF-8"?>
<Response>
  <Gather numDigits="1" action="/voice/dayWeather">
    <Say>
      Monday the 3rd: Sunny, with a high near 70. Press 1 for Sunday, 2 for Monday, 3 for Tuesday, 4 for Wednesday, 5 for Thursday, 6 for Friday, 7 for Saturday. Press 8 for the credits. Press 9 to enter in a new zipcode. Press 0 to hang up.
    </Say>
  </Gather>
</Response>

The last method, showCredits, is executed when the /voice/credits endpoint is requested. The TwiML response has the credits and an instruction to hang up. A response would look like this:

<?xml version="1.0" encoding="UTF-8"?>
<Response>
  <Say>
    Weather data provided by the National Weather Service. Zipcode data provided by GeoNames.
  </Say>
  <Hangup/>
</Response>

Middleware

By default, Twilio makes requests to webhooks using HTTP POST. Because of this, Laravel requires that the POST submission have a CSRF token. In our case, we will not be using the CSRF token, so we must disable the middleware that checks for one. In the app/Http/Kernel.php file, remove or comment the line \App\Http\Middleware\VerifyCsrfToken::class,.

In another section, we will setup ngrok – an application that will allow the internet to connect to our local environment. Because the app does not have CSRF protection any more, anyone on the internet will able to hit our endpoints. To make sure requests are coming from either Twilio, or from the unit tests, we have to create a custom middleware. We will use a middleware suggested in the Twilio docs, but modified to work with our setup.

In the app/Http/Kernel.php file, add the following line to the end of the $routeMiddleware array:

'twilio' => \App\Http\Middleware\TwilioRequestValidator::class,

Create a file called TwilioRequestValidator.php in the app/Http/Middleware folder and paste the following code into it:

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Response;
use Twilio\Security\RequestValidator;

class TwilioRequestValidator
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        if (env('APP_ENV') === 'test') {
            return $next($request);
        }

        $host = $request->header('host');
        $originalHost = $request->header('X-Original-Host', $host);

        $fullUrl = $request->fullUrl();

        $fullUrl = str_replace($host, $originalHost, $fullUrl);

        $requestValidator = new RequestValidator(env('TWILIO_APP_TOKEN'));

        $isValid = $requestValidator->validate(
            $request->header('X-Twilio-Signature'),
            $fullUrl,
            $request->toArray()
        );

        if ($isValid) {
            return $next($request);
        } else {
            return new Response('You are not Twilio :(', 403);
        }
    }
}

With each request that Twilio makes, it sends an HMAC computed with the URL and request variables as the data and the Twilio Auth Token as the secret key. This HMAC is sent in the X-Twilio-Signature request header. We can compare the X-Twilio-Signature sent in the request with an HMAC that we generate on the web server. This is done using the validate() method on the $requestValidator object.

Go to the Twilio Console Dashboard at https://www.twilio.com/console. Once there, unhide the AUTH TOKEN and take note of it.

Photo of Twilio console dashboard page

Then, open up the .env file and then append the following code to the end of the file.

TWILIO_APP_TOKEN=YOUR_AUTH_TOKEN_HERE

Don’t forget to replace YOUR_AUTH_TOKEN_HERE with the value you took note of on the dashboard.

Because of how Ngrok works, it changes the host request header from what the original client defined (something like abc123.ngrok.io) to the host which we specified. In this case it will be homestead.app. We need to change the URL to take this into consideration to compute the correct HMAC. The middleware is skipped if it is determined that there are unit tests being run.

GeoNames

To be able to use the GeoNames API to do the zipcode and timezone queries, we must create a free account. After that, we must enable the ability to use the API by visiting the manage account page.

Once registered, open your .env file and add the variable GEONAMES_USERNAME with the value being your GeoNames username.

Ngrok

Twilio requires your app to be accessible from the internet because Twilio calls the webhooks you implement. With the default design of Homestead, the web server is only available to your local machine. To get around this limitation, we use Ngrok. This program allows you to get an FQDN that works on the internet and forwards the traffic for that address to your Homestead instance, using a tunnel.

To use Ngrok, you must first sign up for a free account. After that, download and install Ngrok. Once the app is installed, be sure to install the authtoken as per the Get Started documentation. Next, we will start up the tunnel:

./ngrok http 192.168.10.10:80 -host-header=homestead.app

This tunnel will allow incoming requests from the internet to be received by our local server. Homestead expects the host HTTP header to be homestead.app. The Ngrok command we used will copy the HTTP request’s original host header to the X-Original-Host header and then overwrites host with the value homestead.app.

Please make note of the HTTP URL given by the program as we will need the hostname in it later on when setting up the Twilio phone number to call.

Photo of Ngrok running in a terminal

Twilio

Create an account with Twilio and add money to your account. Adding ten dollars is more than enough to implement and test the app. The money is needed to procure a telephone number and to pay for the incoming calls. Go to the Find a Number page and purchase a number that has voice and SMS capability. I picked a toll-free number.

Photo of buy a number advanced search page

In the search page, click the “Advanced Search” link and make sure the “Voice” and “SMS” capabilities checkboxes are marked. Then click the “Search” button. After the list of results appears, pick a phone number.

After you have procured your number, go to the settings page for the phone number and replace the voice “a call comes in” webhook, https://demo.twilio.com/welcome/voice/ with a webhook that will go to your app, i.e. http://YOUR_NGROK_HOSTNAME/voice/enterZipcode, and save the change.

Photo of the Twilio voice webhook setup page

Using the App

With your app and the Ngrok program running, call your toll-free number. You should be asked to enter a zipcode. After entering a zipcode, you should get the current forecast and a voice menu to navigate the app.

Production Considerations

One thing not covered in this article but which can be done to improve this app is to make Twilio use HTTPS endpoints instead of HTTP. If you’d like to see how this is done, please request it in the comments below!

Conclusion

In this article, we built a web app that a user could call and interact with using Twilio. This app allowed a user to get the weather forecast for a particular zipcode. Please remember that there might be a date discrepancy between where a user calls from and the zipcode they enter, if the two are in different timezones.

You can find the code for the app in this article series on Github.

In the next part of this two-part series, we will leverage what we built here to make this app work over SMS.

Frequently Asked Questions (FAQs) about Laravel and PHP Phone Calls

How can I integrate Laravel with Twilio for making phone calls?

Integrating Laravel with Twilio for making phone calls involves a few steps. First, you need to install the Twilio SDK using Composer. You can do this by running the command composer require twilio/sdk. Next, you need to set up your Twilio account SID and Auth Token in your Laravel application. You can do this by adding them to your .env file. Once you’ve done that, you can use the Twilio SDK to make phone calls. Here’s a basic example of how you can do this:

$twilio = new Client($sid, $token);
$twilio->calls
->create("+1234567890", // to
"+0987654321", // from
["url" => "http://demo.twilio.com/docs/voice.xml"]
);

In this example, “+1234567890” is the number you’re calling to, “+0987654321” is your Twilio number, and the URL is the TwiML document that controls your call.

How can I handle incoming calls in Laravel using Twilio?

Handling incoming calls in Laravel using Twilio involves setting up a webhook that Twilio can send requests to when you receive a call. You can do this by creating a new route in your Laravel application that Twilio can send POST requests to. Here’s an example of how you can do this:

Route::post('/calls', function (Request $request) {
$response = new Twiml;
$response->say('Hello, thanks for calling!', ['voice' => 'alice']);
return $response;
});

In this example, when you receive a call, Twilio will send a POST request to your “/calls” route. Your application will then respond with a TwiML document that instructs Twilio to say “Hello, thanks for calling!”.

How can I make outbound phone calls in Laravel using Plivo?

Making outbound phone calls in Laravel using Plivo involves a few steps. First, you need to install the Plivo PHP SDK using Composer. You can do this by running the command composer require plivo/plivo-php. Next, you need to set up your Plivo Auth ID and Auth Token in your Laravel application. You can do this by adding them to your .env file. Once you’ve done that, you can use the Plivo SDK to make phone calls. Here’s a basic example of how you can do this:

$plivo = new \Plivo\RestClient($auth_id, $auth_token);
$message_created = $plivo->calls->create(
'+14156667777', // From
['+14156667778'], // To
'http://foo.com/speak.xml' // Answer URL
);

In this example, “+14156667777” is your Plivo number, “+14156667778” is the number you’re calling to, and the URL is the XML document that controls your call.

How can I handle incoming calls in Laravel using Plivo?

Handling incoming calls in Laravel using Plivo involves setting up a webhook that Plivo can send requests to when you receive a call. You can do this by creating a new route in your Laravel application that Plivo can send POST requests to. Here’s an example of how you can do this:

Route::post('/calls', function (Request $request) {
$response = new \Plivo\XML\Response();
$response->addSpeak('Hello, thanks for calling!');
return Response::make($response->toXML(), '200')->header('Content-Type', 'text/xml');
});

In this example, when you receive a call, Plivo will send a POST request to your “/calls” route. Your application will then respond with an XML document that instructs Plivo to say “Hello, thanks for calling!”.

How can I integrate Laravel with Sinch for making phone calls?

Integrating Laravel with Sinch for making phone calls involves a few steps. First, you need to install the Sinch PHP SDK using Composer. You can do this by running the command composer require sinch/php-rest-api. Next, you need to set up your Sinch Application Key and Application Secret in your Laravel application. You can do this by adding them to your .env file. Once you’ve done that, you can use the Sinch SDK to make phone calls. Here’s a basic example of how you can do this:

$sinch = new \Sinch\Client($applicationKey, $applicationSecret);
$call = $sinch->callout('+1234567890', '+0987654321', 'Hello, this is a test call from Sinch!');

In this example, “+1234567890” is the number you’re calling to, “+0987654321” is your Sinch number, and the message is what you want Sinch to say when the call is answered.

How can I handle incoming calls in Laravel using Sinch?

Handling incoming calls in Laravel using Sinch involves setting up a webhook that Sinch can send requests to when you receive a call. You can do this by creating a new route in your Laravel application that Sinch can send POST requests to. Here’s an example of how you can do this:

Route::post('/calls', function (Request $request) {
$response = new \Sinch\XML\Response();
$response->addSay('Hello, thanks for calling!');
return Response::make($response->toXML(), '200')->header('Content-Type', 'text/xml');
});

In this example, when you receive a call, Sinch will send a POST request to your “/calls” route. Your application will then respond with an XML document that instructs Sinch to say “Hello, thanks for calling!”.

How can I integrate video calls in Laravel?

Integrating video calls in Laravel can be done using various services like Twilio, Sinch, or Plivo. These services provide SDKs that you can use to make and receive video calls. The process is similar to integrating voice calls, but instead of using the voice call methods provided by the SDKs, you would use the video call methods. You would also need to handle video streams in your application, which can be done using WebRTC.

How can I test my Laravel application’s phone call functionality?

Testing your Laravel application’s phone call functionality can be done using various methods. One way is to use the services’ built-in testing tools. For example, Twilio provides a Test Console that you can use to simulate incoming and outgoing calls. Another way is to write automated tests using tools like PHPUnit. You can mock the services’ SDKs and assert that the correct methods are being called with the correct parameters.

How can I secure my Laravel application’s phone call functionality?

Securing your Laravel application’s phone call functionality involves a few steps. First, you should always use HTTPS to protect your application from man-in-the-middle attacks. Second, you should validate all incoming requests to ensure they’re coming from the service you’re using. Most services provide a way to validate incoming requests. For example, Twilio provides a RequestValidator class that you can use to validate incoming Twilio requests.

How can I monitor my Laravel application’s phone call functionality?

Monitoring your Laravel application’s phone call functionality can be done using various methods. One way is to use the services’ built-in monitoring tools. For example, Twilio provides a Monitor API that you can use to retrieve information about your application’s usage. Another way is to use application performance monitoring tools like New Relic or Datadog. These tools can provide insights into your application’s performance and help you identify any issues.

Christopher ThomasChristopher Thomas
View Author

Chris is an App Development Manager for one of the top medical schools in Southern California. He has contributed to a few open source PHP projects.

apiBrunoSframeworkFrameworkslaravelOOPHPPHPphp frameworkphp frameworkstwilio
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week