Building an Interactive Voice Response System with Laravel and Nexmo
Published on by Eric L. Barnes
](https://nexmo.com/?utm_source=laravel-news&utm_medium=site&utm_campaign=laravel-news)
It’s probably easier than you think, let’s get started!
The Setup
First, we’ll get setup with the standard Laravel installation using Composer. I’m putting this together using Laradock, so first I have to start a bash
process to work inside one of the containers:
$docker-compose exec --user=laradock workspace bash
That puts me in my project directory, so I’ll create a new Laravel project right there. The workspace
container has composer already installed. If you’re not using Laradock, you’ll need to have composer installed on your development system:
$composer create-project laravel/laravel laravel-hotline
It’s pretty vanilla, but you can take a peek at the code at this point. Not much to see there, so let’s add the Nexmo packages:
$composer require nexmo/laravel
And so our hotline can do something interesting, we’ll add two more dependencies. SimplePie so we can parse RSS feeds, and a Twitter client so we can pull in a Tweet.
$composer require simplepie/simplepie$composer require thujohn/twitter
Once all the libraries are installed, we can add the service providers to our configuration. Put these two lines in the providers
section of config/app.php
:
'providers' => [ //... Nexmo\Laravel\NexmoServiceProvider::class, Thujohn\Twitter\TwitterServiceProvider::class,],
And we can also add the two Facade aliases in the same file:
'aliases' => [ //... 'Nexmo' => Nexmo\Laravel\Facade\Nexmo::class, 'Twitter' => Thujohn\Twitter\Facades\Twitter::class,]
The final step is generating and editing the configuration files. To create configuration files for both Nexmo and Twitter, from the root of the project run the command:
$php artisan vendor:publish
Then edit config/nexmo.php
and add your Nexmo key and secret. Don’t have one? Signup for free at nexmo.com. You’ll have to add credentials to config/ttwitter.php
as well. If you don’t have those, head over to Twitter’s app management page and create a new application.
Instead of editing the configuration files, you can also just add the credentials to your .env
file. Take a look at the Nexmo and Twitter client readmes for specifics.
If you want, you can skip the setup, and grab the code at this point.
Creating a Nexmo Application
Just like routing web requests to your application requires a domain name pointing to our server’s IP address, routing phone calls requires some configuration external to our code. Nexmo manages that configuration with an API for ‘Applications’.
An Application is a container for configuration, allowing us to define a set of webhooks, attach a phone number, and store credentials. Webhooks are specific URLs Nexmo can request when something happens – like an incoming phone call. The phone numbers come from Nexmo, and once we have one in our account, we can attach it to the application.
Since creating an application, and attaching a phone number are not tasks that happen frequently, or that should be triggered by a web request (at least for our application), they’re perfect targets for an Artisan command.
The AppCreate Command
We’ll use an existing Artisan command to generate the structure of our new command:
php artisan make:command AppCreate
Once the basic structure is created (find it in /app/Console/Commands/AppCreate.php
), we can update the constructor to give it the Nexmo PHP Client we included with composer earlier.
<?phpnamespace App\Console\Commands;use Nexmo\Client; //... protected $client; public function __construct(Client $client){ $this->client = $client; parent::__construct();}
We also need to update the commands description, and it’s signature. That will make --help
useful, and define the arguments we expect the command to be given. The $description
is pretty straightforward:
protected $description = 'Create a new Nexmo Application';protected $signature = 'nexmo:app:create {name} {answer_url} {event_url} {--type=voice} {--answer_method} {--event_method} {--keyfile=nexmo.key}';
The $signature
is a little more complex. We start out by defining that nexmo:app:create
will be how we call the command. Then we’ll expect an application {name}
. After that, we need two webhooks. The webhook to use when there’s a new inbound call is aptly named the answer_url
, and the webhook to use for any call related event (like a call hanging up) is the event_url
.
Optionally we’ll allow an application --type
to be set (but at the moment, the API only supports voice
applications), and allow the HTTP methods of the webhooks to be set. By default, the method used for the answer_url
is a GET
, and the method used by the event_url
is a POST
.
Finally, when an application is created a private key is sent in the API response. This is only provided on the creation of an application, so it needs to be saved somewhere. --keyfile
will let us specify where; however, by default, it will be stored in nexmo.key
.
This argument set and order follows that of the same command in the Nexmo CLI tool, which does far more than just create an application. Obviously, that could be used instead of an Artisan command, but this gives us a chance to see how easy it is to create a new Application using the PHP Client.
With our command defined, let’s write the code to handle()
the command. The PHP Client allows us to build up an Application fluently, but for the sake of clarity, we’ll create it with a few lines of code. We’ll just create a new Application
and pass it a VoiceConfig
with the webhooks:
<?phpnamespace App\Console\Commands;use Nexmo\Client;use Nexmo\Application\Application;use Nexmo\Application\VoiceConfig; //... public function handle(){ $application = new Application(); $application->setName($this->argument('name')); $config = new VoiceConfig(); $config->setWebhook(VoiceConfig::ANSWER, $this->argument('answer_url'), $this->option('answer_method')); $config->setWebhook(VoiceConfig::EVENT, $this->argument('event_url'), $this->option('event_method')); $application->setVoiceConfig($config); //...
Once the Application is configured, we can finish up the handle()
method and make the API. Since the API call could fail (invalid credentials, a bad network connection, etc. ), we’ll wrap the call in a try
block. We’ll also inform the user of our progress, and on success provide the Application ID that can be used later to reference the Application.
//... try { $this->info('Making API Request'); $application = $this->client->applications()->post($application); file_put_contents($this->option('keyfile'), $application->getPrivateKey()); $this->info('Key Saved To: ' . realpath($this->option('keyfile'))); $this->info('Created Application: ' . $application->getId()); } catch (\Exception $e) { $this->error('Request Failed: ' . $e->getMessage()); $this->error($e->getTraceAsString(), \Symfony\Component\Console\Output\OutputInterface::VERBOSITY_DEBUG); }} //end of handle()
Should the API request fail, we notify the user and – should they request it – include the stack trace.
With the command done, we need to add it to the $commands
array in app/Console/Kernel.php
.
protected $commands = [ //... \App\Console\Commands\AppCreate::class];
Now all we need to do is run the command and create our application:
$php artisan nexmo:app:create Laravel-Hotline \ http://tjlytle.ngrok.io/nexmo/answer \ http://tjlytle.ngrok.io/nexmo/event Making API RequestKey Saved To: /var/www/nexmo.keyCreated Application: e2de8f00-0400-4475-89c9-a9c4fde5f40d
At this point you’ll notice we’re not referencing
localhost
in the webhooks, even though that’s likely where the application is running. For Nexmo to send the inbound call webhook to the application, the URL needs to be externally accessible. If you’ve not used it before, ngrok is the perfect solution for making a local development environment accessible on the public internet.
The Link App Command
We now have a Nexmo Application with webhooks pointing at our Laravel Hotline, but that Application can not be called without a phone number. Numbers can be acquired using the Nexmo dashboard, through the Nexmo CLI tool, or with an API call.
If you just created a new Nexmo account, there may be a test number in your dashboard. If not, login and grab one.
You can link one of your numbers to your new Application using the Application ID; however, you can also link it with the API, so let’s create another Artisan command to do that:
php artisan make:command LinkApp
Just like the CreateApp
command, we’ll update the command’s constructor to inject the Nexmo PHP Client. The $signature
is a lot simpler this time, and like CreateApp
it follows the same format as its sibling command in the Nexmo CLI tool:
protected $description = 'Link a Number to an Application';protected $signature = 'nexmo:link:app {number} {app}';
The command expects only two arguments, the phone number to link, and the application it should be linked to. We’ll add a few lines to the handle()
method to set up the relationship:
<?phpnamespace App\Console\Commands;use Nexmo\Client;use Nexmo\Application\Application;use Nexmo\Numbers\Number; //... public function handle(){ $application = new Application($this->argument('app')); $number = new Number($this->argument('number')); $number->setVoiceDestination($application); //...
As you can see, an Application
can be created with an application ID, and it’ll reference the matching Application in the API. The same is true for a Number
, if it is instantiated with the phone number as an ID, it will reference the same number in your Nexmo account.
Once we have the two resources, we can configure the number to use the application for any voice calls.
Like CreateApp
we’ll wrap the API call in a try
block so we can catch any errors and inform the user:
//... try{ $this->info('Making API Request'); $this->client->numbers()->update($number); $this->info('Linked Number to Application'); } catch (\Exception $e) { $this->error('Request Failed: ' . $e->getMessage()); $this->error($e->getTraceAsString(), \Symfony\Component\Console\Output\OutputInterface::VERBOSITY_DEBUG); }} //end of handle()
Register the command by adding it to the $commands
array alongside AppCreate
in app/Console/Kernel.php
.
protected $commands = [ //... \App\Console\Commands\AppCreate::class, \App\Console\Commands\LinkApp::class];
Then give it a spin (with your Nexmo number of course):
$php artisan -vvv nexmo:link:app 14045559404 e2de8f00-0400-4475-89c9-a9c4fde5f40d Making API RequestLinked Number to Application
Certianly all of this could have been done with a few commands in the Nexmo CLI tool; however, if you’re building a SaaS-like product automatically provisioning applications and linking numbers is something you’ll want to do inside your application. And it’s easy to do with a few lines of code.
Just reading along? You can see the code at this point as well.
Picking Up The Phone
Before you can unlock your phone and call your application, we need to put some code behind the webhooks we defined.
A Bit About Webhooks
When a call is made to our application’s number, Nexmo will make an HTTP request to /nexmo/answer
and expect the HTTP response to be the JSON structure of a valid NCCO (Nexmo call control object) stack. That stack defines what should happen in the call. For example, saying “Welcome to the Laravel Hotline” or capturing the user pressing a number on their phone.
An NCCO can also define what URL should get the next webhook. When we capture a user’s key presses using the input
NCCO, we also define the URL where that data is sent – just like an HTML <form>
.
The Nexmo Documentation has all you need to know about NCCOs.
The other webhook we defined was /nexmo/event
. Nexmo will make an HTTP request to that URL every time a call changes state. For example, an inbound call to our application’s number will result in a webhook when the call is ringing
, and another when the call is answered
by our application. These webhooks allow us to track call status.
Configuring the Routes
By default, Laravel attaches a CSRF middleware to the normal route (a good thing). By defaul, Nexmo sends event webhooks, as well as any user input to an NCCO, as a POST
(also a good thing).
We can remove the CSFR middleware by editing app/Http/Kernel.php
, but if we’re building an app that has both a web interface and a voice interface, we shouldn’t do that.
We could also add a URI exception to middleware itself, and since we prefixed all the urls with /nexmo
that would only take a line:
protected $except = [ 'nexmo/*',];
But instead, lets create a new middleware group for all Nexmo routes. In app/Providers/RouteServiceProvider.php
we’ll add method that looks similar to the existing API mapping method:
protected function mapNexmoRoutes(){ Route::prefix('nexmo') ->middleware('bindings') ->namespace($this->namespace) ->group(base_path('routes/nexmo.php'));}
And call that method from map()
:
public function map() { $this->mapApiRoutes(); $this->mapWebRoutes(); $this->mapNexmoRoutes(); }
Now any routes found in routes/nexmo.php
won’t have the CSRF middleware (as well as the other web related middleware). And any routes defined in routes/nexmo.php
are prefixed by /nexmo
, so to handle the /nexmo/answer
webhook, we just define a route for /answer
.
Let’s create routes/nexmo.php
and define a simple route:
<?phpuse Illuminate\Http\Request;use Illuminate\Support\Facades\Route; //simple hello worldRoute::get('/answer', function (Request $request) { return [ [ 'action' => 'talk', 'text' => 'Welcome to the Laravel Hotline' ] ];}); //just log the events so we can inspect the dataRoute::post('/event', function (Request $request) { error_log($request->getContent()); return;});
As simple as that! Laravel will see we’re returning an array, json_encode
the data, and set the correct content/type
. Go ahead, give it a call and see for yourself.
As mentioned when we created the application, for Nexmo to make the webhooks to
/nexmo/answer
and/nexmo/event
the application needs to be accessible on the public internet at the domain used when you created the application. If you need to update those web hooks, it’s as easy as an API call or a Nexmo CLI command.
Take this for a spin by checking out the code at this point.
Making A Controller
Putting the NCCO right in routes/nexmo.php
is a quick way to see everything in action, but we should build a proper IVR controller to handle our voice requests. Let’s update our route to point to a controller:
Route::get ('/answer', 'IvrController@answer')->name('ivr.answer');Route::post('/menu', 'IvrController@menu' )->name('ivr.menu');
We’ve added a new /menu
route to handle user input after our greeting. We’ve also named the routes so that we can generate URLs for them later. Now let’s use Artisan to create a new controller:
$php artisan make:controller IvrController
We’ll open up /app/Http/Controllers/IvrController.php
and add the answer
method that will answer inbound calls. This will take the simple NCCO we used before as a greeting, and add two more NCCOs to the stack:
public function answer(Request $request){ return [ [ 'action' => 'talk', 'text' => 'Welcome to the Laravel Hotline' ], [ 'action' => 'talk', 'text' => "Press 1 to hear Taylor's latest tweet. Press 2 to listen to the latest Laravel Podcast", 'bargeIn' => true ], [ 'action' => 'input', 'eventUrl' => [route('ivr.menu')], 'maxDigits' => 1 ] ];}
The first NCCO is exactly what we used before. The second is also a talk
, but we’ve added the bargeIn
flag which allows the called to press a digit at any time. Since our first talk
doesn’t have bargeIn
, the text will continue playing even if the user pressed a key.
The last NCCO captures the user input and sends the results to the ivr.menu
route (/nexmo/menu
) as a POST
. Let’s build that simple method (menu()
now:
public function menu(Request $request){ switch ($request->json('dtmf')){ case '1'; return $this->tweet($request); case '2': return $this->podcast($request); default: return $this->answer($request); }}
This simple switch
looks for valid options, and if none are found replays the menu. When the user dials a 1
we return whatever NCCO is created by tweet()
, and for 2
we use the NCCO generated by podcast()
.
Our tweet()
method fetches the most recent tweet from @taylorotwell (note the array dereferencing used to get the individual tweet) :
public function tweet(){ $tweet = \Twitter::getUserTimeline(['screen_name' => 'taylorotwell', 'count' => 1, 'format' => 'array'])[0]; $text = $tweet['text']; //...
Since there’s a reasonable chance he’s tweeted a link, and having the Nexmo TTS read a t.co
shortened URL isn’t all that informative, we can replace the URL with a reference to the domain name (at least that’ll add some context for those that call in):
//...foreach($tweet['entities']['urls'] as $link){ $domain = parse_url($link['expanded_url'], PHP_URL_HOST); $text = substr($text, 0, $link['indices'][0]) . 'and a link to ' . $domain . substr($text, $link['indices'][1]);}//....
With the tweet made a little more readable, we’ll return a simple talk
NCCO:
//... return [ [ 'action' => 'talk', 'text' => \Twitter::ago($tweet['created_at']) . ' he tweeted ' . $text ] ];} // end of tweet()
Go ahead, try it out – dial your hotline number and press 1
.
Now let’s finish up the second option. For that, we’ll grab the RSS feed for the Laravel Podcast and find the latest audio file. To keep configuration simple, we’ll disable the cache:
public function podcast(){ $rss = new \SimplePie(); $rss->enable_cache(false); $rss->set_feed_url('https://rss.simplecast.com/podcasts/351/rss'); $rss->init(); $item = $rss->get_item(0); //...
With the RSS feed parsed, and the first item found, we’ll return an NCCO stack with a talk
that reads the episode description, and a stream
that plays the audio file:
//... return [ [ 'action' => 'talk', 'text' => $item->get_description() ], [ 'action' => 'stream', 'streamUrl' => [$item->get_enclosure(0)->get_link()] ] ];} //end of podcast()
Now if you dial the hotline, you can listen to the latest Laravel Podcast with just a press of a button.
What else can you do with Nexmo and Laravel? Next time we’ll add a soundboard to our Laravel Hotline app. You’ll be able to call your friends, and during the conversation inject your favorite Laravel audio bits into the conversation.
And of course, you can grab the source of the Laravel Hotline app in its completed form.
Eric is the creator of Laravel News and has been covering Laravel since 2012.