Skip to contentSkip to navigationSkip to topbar
Rate this page:
On this page

Dynamic Call Center with PHP and Laravel


In this tutorial we will show how to automate the routing of calls from customers to your support agents. Customers will be able to select a product and wait while TaskRouter tries to contact a product specialist for the best support experience. If no one is available, our application will save the customer's number and selected product so an agent can call them back later on.

This is what the application does at a high level:

  1. Configure a workspace using the Twilio TaskRouter REST API .
  2. Listen for incoming calls and let the user select a product with the dial pad.
  3. Create a Task with the selected product and let TaskRouter handle it.
  4. Store missed calls so agents can return the call to customers.

Configure the Workspace

configure-the-workspace page anchor

In order to instruct TaskRouter to handle the Tasks, we need to configure a Workspace. We can do this in the TaskRouter Console(link takes you to an external page) or programmatically using the TaskRouter REST API.

A Workspace is the container element for any TaskRouter application. The elements are:

  • Tasks - Represents a customer trying to contact an agent.
  • Workers - The agents responsible for handling Tasks.
  • Task Queues - Holds Tasks to be consumed by a set of Workers.
  • Workflows - Responsible for placing Tasks into Task Queues.
  • Activities - Possible states of a Worker, e.g. Idle, Offline, Busy.

Workspace Configuration

workspace-configuration page anchor

resources/workspace.json


_61
{
_61
"name": "Twilio Workspace",
_61
"event_callback": "%(host)s/events",
_61
"workers": [
_61
{
_61
"name": "Bob",
_61
"attributes": {
_61
"products": [
_61
"ProgrammableSMS"
_61
],
_61
"contact_uri": "%(bob_phone)s"
_61
}
_61
},
_61
{
_61
"name": "Alice",
_61
"attributes": {
_61
"products": [
_61
"ProgrammableVoice"
_61
],
_61
"contact_uri": "%(alice_phone)s"
_61
}
_61
}
_61
],
_61
"activities": [
_61
{
_61
"name": "Offline",
_61
"availability": "false"
_61
},
_61
{
_61
"name": "Idle",
_61
"availability": "true"
_61
},
_61
{
_61
"name": "Busy",
_61
"availability": "false"
_61
},
_61
{
_61
"name": "Reserved",
_61
"availability": "false"
_61
}
_61
],
_61
"task_queues": [
_61
{
_61
"name": "Default",
_61
"targetWorkers": "1==1"
_61
},
_61
{
_61
"name": "SMS",
_61
"targetWorkers": "products HAS \"ProgrammableSMS\""
_61
},
_61
{
_61
"name": "Voice",
_61
"targetWorkers": "products HAS \"ProgrammableVoice\""
_61
}
_61
],
_61
"workflow": {
_61
"name": "Sales",
_61
"callback": "%(host)s/assignment",
_61
"timeout": "15"
_61
}
_61
}

In order to build a client for this API, we need as system variables a TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKEN which you can find on Twilio Console. The artisan command workspace:create creates a Twilio\Rest\Client, which is provided by the Twilio PHP library(link takes you to an external page). This client is used by WorkspaceFacade(link takes you to an external page) which encapsulates all logic related to the creation of a Task Router workflow.

Let's take a look at that command next.


The CreateWorkspace Artisan Command

the-createworkspace-artisan-command page anchor

In this application the Artisan command(link takes you to an external page) workspace:create is used to orchestrate calls to our WorkspaceFacade class in order to build a Workspace from scratch. CreateWorkspace is the PHP class behind the Artisan command. It uses data provided by workspace.json and expects 3 arguments:

  1. host - A public URL to which Twilio can send requests. This can be either a cloud service or ngrok(link takes you to an external page) , which can expose a local application to the internet.
  2. bob_phone - The telephone number of Bob, the Programmable SMS specialist.
  3. alice_phone - Same for Alice, the Programmable Voice specialist.

The function createWorkspaceConfig is used to load the configuration of the workspace from workspace.json.

CreateWorkspace Artisan Command

createworkspace-artisan-command page anchor

app/Console/Commands/CreateWorkspace.php


_219
<?php
_219
_219
namespace App\Console\Commands;
_219
_219
use App\TaskRouter\WorkspaceFacade;
_219
use Illuminate\Console\Command;
_219
use Illuminate\Support\Facades\File;
_219
use TaskRouter_Services_Twilio;
_219
use Twilio\Rest\Client;
_219
use Twilio\Rest\Taskrouter;
_219
use WorkflowRuleTarget;
_219
_219
class CreateWorkspace extends Command
_219
{
_219
/**
_219
* The name and signature of the console command.
_219
*
_219
* @var string
_219
*/
_219
protected $signature = 'workspace:create
_219
{host : Server hostname in Internet}
_219
{bob_phone : Phone of the first agent (Bob)}
_219
{alice_phone : Phone of the secondary agent (Alice)}';
_219
_219
/**
_219
* The console command description.
_219
*
_219
* @var string
_219
*/
_219
protected $description = 'Creates a Twilio workspace for 2 call agents';
_219
_219
private $_twilioClient;
_219
_219
/**
_219
* Create a new command instance.
_219
*/
_219
public function __construct(Client $twilioClient)
_219
{
_219
parent::__construct();
_219
$this->_twilioClient = $twilioClient;
_219
}
_219
_219
/**
_219
* Execute the console command.
_219
*
_219
* @return mixed
_219
*/
_219
public function handle()
_219
{
_219
$this->info("Create workspace.");
_219
$this->line("- Server: \t{$this->argument('host')}");
_219
$this->line("- Bob phone: \t{$this->argument('bob_phone')}");
_219
$this->line("- Alice phone: \t{$this->argument('alice_phone')}");
_219
_219
//Get the configuration
_219
$workspaceConfig = $this->createWorkspaceConfig();
_219
_219
//Create the workspace
_219
$params = array();
_219
$params['friendlyName'] = $workspaceConfig->name;
_219
$params['eventCallbackUrl'] = $workspaceConfig->event_callback;
_219
$workspace = WorkspaceFacade::createNewWorkspace(
_219
$this->_twilioClient->taskrouter,
_219
$params
_219
);
_219
$this->addWorkersToWorkspace($workspace, $workspaceConfig);
_219
$this->addTaskQueuesToWorkspace($workspace, $workspaceConfig);
_219
$workflow = $this->addWorkflowToWorkspace($workspace, $workspaceConfig);
_219
_219
$this->printSuccessAndInstructions($workspace, $workflow);
_219
}
_219
_219
/**
_219
* Get the json configuration of the Workspace
_219
*
_219
* @return mixed
_219
*/
_219
function createWorkspaceConfig()
_219
{
_219
$fileContent = File::get("resources/workspace.json");
_219
$interpolatedContent = sprintfn($fileContent, $this->argument());
_219
return json_decode($interpolatedContent);
_219
}
_219
_219
/**
_219
* Add workers to workspace
_219
*
_219
* @param $workspace WorkspaceFacade
_219
* @param $workspaceConfig string with Json
_219
*/
_219
function addWorkersToWorkspace($workspace, $workspaceConfig)
_219
{
_219
$this->line("Add Workers.");
_219
$idleActivity = $workspace->findActivityByName("Idle")
_219
or die("The activity 'Idle' was not found. Workers cannot be added.");
_219
foreach ($workspaceConfig->workers as $workerJson) {
_219
$params = array();
_219
$params['friendlyName'] = $workerJson->name;
_219
$params['activitySid'] = $idleActivity->sid;
_219
$params['attributes'] = json_encode($workerJson->attributes);
_219
$workspace->addWorker($params);
_219
}
_219
}
_219
_219
/**
_219
* Add the Task Queues to the workspace
_219
*
_219
* @param $workspace WorkspaceFacade
_219
* @param $workspaceConfig string with Json
_219
*/
_219
function addTaskQueuesToWorkspace($workspace, $workspaceConfig)
_219
{
_219
$this->line("Add Task Queues.");
_219
$reservedActivity = $workspace->findActivityByName("Reserved");
_219
$assignmentActivity = $workspace->findActivityByName("Busy");
_219
foreach ($workspaceConfig->task_queues as $taskQueueJson) {
_219
$params = array();
_219
$params['friendlyName'] = $taskQueueJson->name;
_219
$params['targetWorkers'] = $taskQueueJson->targetWorkers;
_219
$params['reservationActivitySid'] = $reservedActivity->sid;
_219
$params['assignmentActivitySid'] = $assignmentActivity->sid;
_219
$workspace->addTaskQueue($params);
_219
}
_219
}
_219
_219
/**
_219
* Create and configure the workflow to use in the workspace
_219
*
_219
* @param $workspace WorkspaceFacade
_219
* @param $workspaceConfig string with Json
_219
*
_219
* @return object with added workflow
_219
*/
_219
function addWorkflowToWorkspace($workspace, $workspaceConfig)
_219
{
_219
$this->line("Add Worflow.");
_219
$workflowJson = $workspaceConfig->workflow;
_219
$params = array();
_219
$params['friendlyName'] = $workflowJson->name;
_219
$params['assignmentCallbackUrl'] = $workflowJson->callback;
_219
$params['taskReservationTimeout'] = $workflowJson->timeout;
_219
$params['configuration'] = $this->createWorkFlowJsonConfig(
_219
$workspace,
_219
$workflowJson
_219
);
_219
return $workspace->addWorkflow($params);
_219
}
_219
_219
/**
_219
* Create the workflow configuration in json format
_219
*
_219
* @param $workspace
_219
* @param $workspaceConfig
_219
*
_219
* @return string configuration of workflow in json format
_219
*/
_219
function createWorkFlowJsonConfig($workspace, $workspaceConfig)
_219
{
_219
$params = array();
_219
$defaultTaskQueue = $workspace->findTaskQueueByName("Default") or die(
_219
"The 'Default' task queue was not found. The Workflow cannot be created."
_219
);
_219
$smsTaskQueue = $workspace->findTaskQueueByName("SMS") or die(
_219
"The 'SMS' task queue was not found. The Workflow cannot be created."
_219
);
_219
$voiceTaskQueue = $workspace->findTaskQueueByName("Voice") or die(
_219
"The 'Voice' task queue was not found. The Workflow cannot be created."
_219
);
_219
_219
$params["default_task_queue_sid"] = $defaultTaskQueue->sid;
_219
$params["sms_task_queue_sid"] = $smsTaskQueue->sid;
_219
$params["voice_task_queue_sid"] = $voiceTaskQueue->sid;
_219
_219
$fileContent = File::get("resources/workflow.json");
_219
$interpolatedContent = sprintfn($fileContent, $params);
_219
return $interpolatedContent;
_219
}
_219
_219
/**
_219
* Prints the message indicating the workspace was successfully created and
_219
* shows the commands to export the workspace variables into the environment.
_219
*
_219
* @param $workspace
_219
* @param $workflow
_219
*/
_219
function printSuccessAndInstructions($workspace, $workflow)
_219
{
_219
$idleActivity = $workspace->findActivityByName("Idle")
_219
or die("Somehow the activity 'Idle' was not found.");
_219
$successMsg = "Workspace \"{$workspace->friendlyName}\"" .
_219
" was created successfully.";
_219
$this->printTitle($successMsg);
_219
$this->line(
_219
"The following variables will be set automatically."
_219
);
_219
$encondedWorkersPhone = http_build_query($workspace->getWorkerPhones());
_219
$envVars = [
_219
"WORKFLOW_SID" => $workflow->sid,
_219
"POST_WORK_ACTIVITY_SID" => $idleActivity->sid,
_219
"WORKSPACE_SID" => $workspace->sid,
_219
"PHONE_TO_WORKER" => $encondedWorkersPhone
_219
];
_219
updateEnv($envVars);
_219
foreach ($envVars as $key => $value) {
_219
$this->warn("export $key=$value");
_219
}
_219
}
_219
_219
/**
_219
* Prints a text separated up and doNwn by a token based line, usually "*"
_219
*/
_219
function printTitle($text)
_219
{
_219
$lineLength = strlen($text) + 2;
_219
$this->line(str_repeat("*", $lineLength));
_219
$this->line(" $text ");
_219
$this->line(str_repeat("*", $lineLength));
_219
}
_219
}

Now let's look in more detail at all the steps, starting with the creation of the workspace itself.


Before creating a workspace, we need to delete any others with the same friendlyName as identifier. In order to create a workspace we need to provide a friendlyName, and a eventCallbackUrl which contains an url to be called every time an event is triggered in the workspace.

app/TaskRouter/WorkspaceFacade.php


_187
<?php
_187
_187
namespace App\TaskRouter;
_187
_187
use Twilio\Rest\Taskrouter\V1\Workspace;
_187
_187
_187
class WorkspaceFacade
_187
{
_187
private $_taskRouterClient;
_187
_187
private $_workspace;
_187
_187
private $_activities;
_187
_187
public function __construct($taskRouterClient, $workspace)
_187
{
_187
$this->_taskRouterClient = $taskRouterClient;
_187
$this->_workspace = $workspace;
_187
}
_187
_187
public static function createNewWorkspace($taskRouterClient, $params)
_187
{
_187
$workspaceName = $params["friendlyName"];
_187
$existingWorkspace = $taskRouterClient->workspaces->read(
_187
array(
_187
"friendlyName" => $workspaceName
_187
)
_187
);
_187
if ($existingWorkspace) {
_187
$existingWorkspace[0]->delete();
_187
}
_187
_187
$workspace = $taskRouterClient->workspaces
_187
->create($workspaceName, $params);
_187
return new WorkspaceFacade($taskRouterClient, $workspace);
_187
}
_187
_187
public static function createBySid($taskRouterClient, $workspaceSid)
_187
{
_187
$workspace = $taskRouterClient->workspaces($workspaceSid);
_187
return new WorkspaceFacade($taskRouterClient, $workspace);
_187
}
_187
_187
/**
_187
* Magic getter to lazy load subresources
_187
*
_187
* @param string $property Subresource to return
_187
*
_187
* @return \Twilio\ListResource The requested subresource
_187
*
_187
* @throws \Twilio\Exceptions\TwilioException For unknown subresources
_187
*/
_187
public function __get($property)
_187
{
_187
return $this->_workspace->$property;
_187
}
_187
_187
/**
_187
* Gets an activity instance by its friendly name
_187
*
_187
* @param $activityName Friendly name of the activity to search for
_187
*
_187
* @return ActivityInstance of the activity found or null
_187
*/
_187
function findActivityByName($activityName)
_187
{
_187
$this->cacheActivitiesByName();
_187
return $this->_activities[$activityName];
_187
}
_187
_187
/**
_187
* Caches the activities in an associative array which links friendlyName with
_187
* its ActivityInstance
_187
*/
_187
protected function cacheActivitiesByName()
_187
{
_187
if (!$this->_activities) {
_187
$this->_activities = array();
_187
foreach ($this->_workspace->activities->read() as $activity) {
_187
$this->_activities[$activity->friendlyName] = $activity;
_187
}
_187
}
_187
}
_187
_187
/**
_187
* Looks for a worker by its SID
_187
*
_187
* @param $sid string with the Worker SID
_187
*
_187
* @return mixed worker found or null
_187
*/
_187
function findWorkerBySid($sid)
_187
{
_187
return $this->_workspace->workers($sid);
_187
}
_187
_187
/**
_187
* Returns an associative array with
_187
*
_187
* @return mixed array with the relation phone -> workerSid
_187
*/
_187
function getWorkerPhones()
_187
{
_187
$worker_phones = array();
_187
foreach ($this->_workspace->workers->read() as $worker) {
_187
$workerAttribs = json_decode($worker->attributes);
_187
$worker_phones[$workerAttribs->contact_uri] = $worker->sid;
_187
}
_187
return $worker_phones;
_187
}
_187
_187
/**
_187
* Looks for a Task Queue by its friendly name
_187
*
_187
* @param $taskQueueName string with the friendly name of the task queue to
_187
* search for
_187
*
_187
* @return the activity found or null
_187
*/
_187
function findTaskQueueByName($taskQueueName)
_187
{
_187
$result = $this->_workspace->taskQueues->read(
_187
array(
_187
"friendlyName" => $taskQueueName
_187
)
_187
);
_187
if ($result) {
_187
return $result[0];
_187
}
_187
}
_187
_187
function updateWorkerActivity($worker, $activitySid)
_187
{
_187
$worker->update(['activitySid' => $activitySid]);
_187
}
_187
_187
/**
_187
* Adds workers to the workspace
_187
*
_187
* @param $params mixed with the attributes to define the new Worker in the
_187
* workspace
_187
*
_187
* @return Workspace\WorkerInstance|Null
_187
*/
_187
function addWorker($params)
_187
{
_187
return $this->_workspace->workers->create($params['friendlyName'], $params);
_187
}
_187
_187
/**
_187
* Adds a Task Queue to the workspace
_187
*
_187
* @param $params mixed with attributes to define the new Task Queue in the
_187
* workspace
_187
*
_187
* @return Workspace\TaskQueueInstance|Null
_187
*/
_187
function addTaskQueue($params)
_187
{
_187
return $this->_workspace->taskQueues->create(
_187
$params['friendlyName'],
_187
$params['reservationActivitySid'],
_187
$params['assignmentActivitySid'],
_187
$params
_187
);
_187
}
_187
_187
_187
/**
_187
* Adds a workflow to the workspace
_187
*
_187
* @param $params mixed with attributes to define the new Workflow in the
_187
* workspace
_187
*
_187
* @return Workspace\WorkflowInstance|Null
_187
*/
_187
function addWorkFlow($params)
_187
{
_187
return $this->_workspace->workflows->create(
_187
$params["friendlyName"],
_187
$params["configuration"],
_187
$params
_187
);
_187
}
_187
_187
}

We have a brand new workspace, now we need workers. Let's create them on the next step.


We'll create two workers, Bob and Alice. They each have two attributes: contact_uri, a phone number, and products, a list of products each worker is specialized in. We also need to specify an activity_sid and a name for each worker. The selected activity will define the status of the worker.

app/Console/Commands/CreateWorkspace.php


_219
<?php
_219
_219
namespace App\Console\Commands;
_219
_219
use App\TaskRouter\WorkspaceFacade;
_219
use Illuminate\Console\Command;
_219
use Illuminate\Support\Facades\File;
_219
use TaskRouter_Services_Twilio;
_219
use Twilio\Rest\Client;
_219
use Twilio\Rest\Taskrouter;
_219
use WorkflowRuleTarget;
_219
_219
class CreateWorkspace extends Command
_219
{
_219
/**
_219
* The name and signature of the console command.
_219
*
_219
* @var string
_219
*/
_219
protected $signature = 'workspace:create
_219
{host : Server hostname in Internet}
_219
{bob_phone : Phone of the first agent (Bob)}
_219
{alice_phone : Phone of the secondary agent (Alice)}';
_219
_219
/**
_219
* The console command description.
_219
*
_219
* @var string
_219
*/
_219
protected $description = 'Creates a Twilio workspace for 2 call agents';
_219
_219
private $_twilioClient;
_219
_219
/**
_219
* Create a new command instance.
_219
*/
_219
public function __construct(Client $twilioClient)
_219
{
_219
parent::__construct();
_219
$this->_twilioClient = $twilioClient;
_219
}
_219
_219
/**
_219
* Execute the console command.
_219
*
_219
* @return mixed
_219
*/
_219
public function handle()
_219
{
_219
$this->info("Create workspace.");
_219
$this->line("- Server: \t{$this->argument('host')}");
_219
$this->line("- Bob phone: \t{$this->argument('bob_phone')}");
_219
$this->line("- Alice phone: \t{$this->argument('alice_phone')}");
_219
_219
//Get the configuration
_219
$workspaceConfig = $this->createWorkspaceConfig();
_219
_219
//Create the workspace
_219
$params = array();
_219
$params['friendlyName'] = $workspaceConfig->name;
_219
$params['eventCallbackUrl'] = $workspaceConfig->event_callback;
_219
$workspace = WorkspaceFacade::createNewWorkspace(
_219
$this->_twilioClient->taskrouter,
_219
$params
_219
);
_219
$this->addWorkersToWorkspace($workspace, $workspaceConfig);
_219
$this->addTaskQueuesToWorkspace($workspace, $workspaceConfig);
_219
$workflow = $this->addWorkflowToWorkspace($workspace, $workspaceConfig);
_219
_219
$this->printSuccessAndInstructions($workspace, $workflow);
_219
}
_219
_219
/**
_219
* Get the json configuration of the Workspace
_219
*
_219
* @return mixed
_219
*/
_219
function createWorkspaceConfig()
_219
{
_219
$fileContent = File::get("resources/workspace.json");
_219
$interpolatedContent = sprintfn($fileContent, $this->argument());
_219
return json_decode($interpolatedContent);
_219
}
_219
_219
/**
_219
* Add workers to workspace
_219
*
_219
* @param $workspace WorkspaceFacade
_219
* @param $workspaceConfig string with Json
_219
*/
_219
function addWorkersToWorkspace($workspace, $workspaceConfig)
_219
{
_219
$this->line("Add Workers.");
_219
$idleActivity = $workspace->findActivityByName("Idle")
_219
or die("The activity 'Idle' was not found. Workers cannot be added.");
_219
foreach ($workspaceConfig->workers as $workerJson) {
_219
$params = array();
_219
$params['friendlyName'] = $workerJson->name;
_219
$params['activitySid'] = $idleActivity->sid;
_219
$params['attributes'] = json_encode($workerJson->attributes);
_219
$workspace->addWorker($params);
_219
}
_219
}
_219
_219
/**
_219
* Add the Task Queues to the workspace
_219
*
_219
* @param $workspace WorkspaceFacade
_219
* @param $workspaceConfig string with Json
_219
*/
_219
function addTaskQueuesToWorkspace($workspace, $workspaceConfig)
_219
{
_219
$this->line("Add Task Queues.");
_219
$reservedActivity = $workspace->findActivityByName("Reserved");
_219
$assignmentActivity = $workspace->findActivityByName("Busy");
_219
foreach ($workspaceConfig->task_queues as $taskQueueJson) {
_219
$params = array();
_219
$params['friendlyName'] = $taskQueueJson->name;
_219
$params['targetWorkers'] = $taskQueueJson->targetWorkers;
_219
$params['reservationActivitySid'] = $reservedActivity->sid;
_219
$params['assignmentActivitySid'] = $assignmentActivity->sid;
_219
$workspace->addTaskQueue($params);
_219
}
_219
}
_219
_219
/**
_219
* Create and configure the workflow to use in the workspace
_219
*
_219
* @param $workspace WorkspaceFacade
_219
* @param $workspaceConfig string with Json
_219
*
_219
* @return object with added workflow
_219
*/
_219
function addWorkflowToWorkspace($workspace, $workspaceConfig)
_219
{
_219
$this->line("Add Worflow.");
_219
$workflowJson = $workspaceConfig->workflow;
_219
$params = array();
_219
$params['friendlyName'] = $workflowJson->name;
_219
$params['assignmentCallbackUrl'] = $workflowJson->callback;
_219
$params['taskReservationTimeout'] = $workflowJson->timeout;
_219
$params['configuration'] = $this->createWorkFlowJsonConfig(
_219
$workspace,
_219
$workflowJson
_219
);
_219
return $workspace->addWorkflow($params);
_219
}
_219
_219
/**
_219
* Create the workflow configuration in json format
_219
*
_219
* @param $workspace
_219
* @param $workspaceConfig
_219
*
_219
* @return string configuration of workflow in json format
_219
*/
_219
function createWorkFlowJsonConfig($workspace, $workspaceConfig)
_219
{
_219
$params = array();
_219
$defaultTaskQueue = $workspace->findTaskQueueByName("Default") or die(
_219
"The 'Default' task queue was not found. The Workflow cannot be created."
_219
);
_219
$smsTaskQueue = $workspace->findTaskQueueByName("SMS") or die(
_219
"The 'SMS' task queue was not found. The Workflow cannot be created."
_219
);
_219
$voiceTaskQueue = $workspace->findTaskQueueByName("Voice") or die(
_219
"The 'Voice' task queue was not found. The Workflow cannot be created."
_219
);
_219
_219
$params["default_task_queue_sid"] = $defaultTaskQueue->sid;
_219
$params["sms_task_queue_sid"] = $smsTaskQueue->sid;
_219
$params["voice_task_queue_sid"] = $voiceTaskQueue->sid;
_219
_219
$fileContent = File::get("resources/workflow.json");
_219
$interpolatedContent = sprintfn($fileContent, $params);
_219
return $interpolatedContent;
_219
}
_219
_219
/**
_219
* Prints the message indicating the workspace was successfully created and
_219
* shows the commands to export the workspace variables into the environment.
_219
*
_219
* @param $workspace
_219
* @param $workflow
_219
*/
_219
function printSuccessAndInstructions($workspace, $workflow)
_219
{
_219
$idleActivity = $workspace->findActivityByName("Idle")
_219
or die("Somehow the activity 'Idle' was not found.");
_219
$successMsg = "Workspace \"{$workspace->friendlyName}\"" .
_219
" was created successfully.";
_219
$this->printTitle($successMsg);
_219
$this->line(
_219
"The following variables will be set automatically."
_219
);
_219
$encondedWorkersPhone = http_build_query($workspace->getWorkerPhones());
_219
$envVars = [
_219
"WORKFLOW_SID" => $workflow->sid,
_219
"POST_WORK_ACTIVITY_SID" => $idleActivity->sid,
_219
"WORKSPACE_SID" => $workspace->sid,
_219
"PHONE_TO_WORKER" => $encondedWorkersPhone
_219
];
_219
updateEnv($envVars);
_219
foreach ($envVars as $key => $value) {
_219
$this->warn("export $key=$value");
_219
}
_219
}
_219
_219
/**
_219
* Prints a text separated up and doNwn by a token based line, usually "*"
_219
*/
_219
function printTitle($text)
_219
{
_219
$lineLength = strlen($text) + 2;
_219
$this->line(str_repeat("*", $lineLength));
_219
$this->line(" $text ");
_219
$this->line(str_repeat("*", $lineLength));
_219
}
_219
}

After creating our workers, let's set up the Task Queues.


Next, we set up the Task Queues. Each with a name and a targetWorkers property, which is an expression to match Workers. Our Task Queues are:

  1. SMS - Will target Workers specialized in Programmable SMS, such as Bob, using the expression "products HAS \"ProgrammableSMS\"" .
  2. Voice - Will do the same for Programmable Voice Workers, such as Alice, using the expression "products HAS \"ProgrammableVoice\"" .
  3. All - This queue targets all users and can be used when there are no specialist around for the chosen product. We can use the "1==1" expression here.

app/Console/Commands/CreateWorkspace.php


_219
<?php
_219
_219
namespace App\Console\Commands;
_219
_219
use App\TaskRouter\WorkspaceFacade;
_219
use Illuminate\Console\Command;
_219
use Illuminate\Support\Facades\File;
_219
use TaskRouter_Services_Twilio;
_219
use Twilio\Rest\Client;
_219
use Twilio\Rest\Taskrouter;
_219
use WorkflowRuleTarget;
_219
_219
class CreateWorkspace extends Command
_219
{
_219
/**
_219
* The name and signature of the console command.
_219
*
_219
* @var string
_219
*/
_219
protected $signature = 'workspace:create
_219
{host : Server hostname in Internet}
_219
{bob_phone : Phone of the first agent (Bob)}
_219
{alice_phone : Phone of the secondary agent (Alice)}';
_219
_219
/**
_219
* The console command description.
_219
*
_219
* @var string
_219
*/
_219
protected $description = 'Creates a Twilio workspace for 2 call agents';
_219
_219
private $_twilioClient;
_219
_219
/**
_219
* Create a new command instance.
_219
*/
_219
public function __construct(Client $twilioClient)
_219
{
_219
parent::__construct();
_219
$this->_twilioClient = $twilioClient;
_219
}
_219
_219
/**
_219
* Execute the console command.
_219
*
_219
* @return mixed
_219
*/
_219
public function handle()
_219
{
_219
$this->info("Create workspace.");
_219
$this->line("- Server: \t{$this->argument('host')}");
_219
$this->line("- Bob phone: \t{$this->argument('bob_phone')}");
_219
$this->line("- Alice phone: \t{$this->argument('alice_phone')}");
_219
_219
//Get the configuration
_219
$workspaceConfig = $this->createWorkspaceConfig();
_219
_219
//Create the workspace
_219
$params = array();
_219
$params['friendlyName'] = $workspaceConfig->name;
_219
$params['eventCallbackUrl'] = $workspaceConfig->event_callback;
_219
$workspace = WorkspaceFacade::createNewWorkspace(
_219
$this->_twilioClient->taskrouter,
_219
$params
_219
);
_219
$this->addWorkersToWorkspace($workspace, $workspaceConfig);
_219
$this->addTaskQueuesToWorkspace($workspace, $workspaceConfig);
_219
$workflow = $this->addWorkflowToWorkspace($workspace, $workspaceConfig);
_219
_219
$this->printSuccessAndInstructions($workspace, $workflow);
_219
}
_219
_219
/**
_219
* Get the json configuration of the Workspace
_219
*
_219
* @return mixed
_219
*/
_219
function createWorkspaceConfig()
_219
{
_219
$fileContent = File::get("resources/workspace.json");
_219
$interpolatedContent = sprintfn($fileContent, $this->argument());
_219
return json_decode($interpolatedContent);
_219
}
_219
_219
/**
_219
* Add workers to workspace
_219
*
_219
* @param $workspace WorkspaceFacade
_219
* @param $workspaceConfig string with Json
_219
*/
_219
function addWorkersToWorkspace($workspace, $workspaceConfig)
_219
{
_219
$this->line("Add Workers.");
_219
$idleActivity = $workspace->findActivityByName("Idle")
_219
or die("The activity 'Idle' was not found. Workers cannot be added.");
_219
foreach ($workspaceConfig->workers as $workerJson) {
_219
$params = array();
_219
$params['friendlyName'] = $workerJson->name;
_219
$params['activitySid'] = $idleActivity->sid;
_219
$params['attributes'] = json_encode($workerJson->attributes);
_219
$workspace->addWorker($params);
_219
}
_219
}
_219
_219
/**
_219
* Add the Task Queues to the workspace
_219
*
_219
* @param $workspace WorkspaceFacade
_219
* @param $workspaceConfig string with Json
_219
*/
_219
function addTaskQueuesToWorkspace($workspace, $workspaceConfig)
_219
{
_219
$this->line("Add Task Queues.");
_219
$reservedActivity = $workspace->findActivityByName("Reserved");
_219
$assignmentActivity = $workspace->findActivityByName("Busy");
_219
foreach ($workspaceConfig->task_queues as $taskQueueJson) {
_219
$params = array();
_219
$params['friendlyName'] = $taskQueueJson->name;
_219
$params['targetWorkers'] = $taskQueueJson->targetWorkers;
_219
$params['reservationActivitySid'] = $reservedActivity->sid;
_219
$params['assignmentActivitySid'] = $assignmentActivity->sid;
_219
$workspace->addTaskQueue($params);
_219
}
_219
}
_219
_219
/**
_219
* Create and configure the workflow to use in the workspace
_219
*
_219
* @param $workspace WorkspaceFacade
_219
* @param $workspaceConfig string with Json
_219
*
_219
* @return object with added workflow
_219
*/
_219
function addWorkflowToWorkspace($workspace, $workspaceConfig)
_219
{
_219
$this->line("Add Worflow.");
_219
$workflowJson = $workspaceConfig->workflow;
_219
$params = array();
_219
$params['friendlyName'] = $workflowJson->name;
_219
$params['assignmentCallbackUrl'] = $workflowJson->callback;
_219
$params['taskReservationTimeout'] = $workflowJson->timeout;
_219
$params['configuration'] = $this->createWorkFlowJsonConfig(
_219
$workspace,
_219
$workflowJson
_219
);
_219
return $workspace->addWorkflow($params);
_219
}
_219
_219
/**
_219
* Create the workflow configuration in json format
_219
*
_219
* @param $workspace
_219
* @param $workspaceConfig
_219
*
_219
* @return string configuration of workflow in json format
_219
*/
_219
function createWorkFlowJsonConfig($workspace, $workspaceConfig)
_219
{
_219
$params = array();
_219
$defaultTaskQueue = $workspace->findTaskQueueByName("Default") or die(
_219
"The 'Default' task queue was not found. The Workflow cannot be created."
_219
);
_219
$smsTaskQueue = $workspace->findTaskQueueByName("SMS") or die(
_219
"The 'SMS' task queue was not found. The Workflow cannot be created."
_219
);
_219
$voiceTaskQueue = $workspace->findTaskQueueByName("Voice") or die(
_219
"The 'Voice' task queue was not found. The Workflow cannot be created."
_219
);
_219
_219
$params["default_task_queue_sid"] = $defaultTaskQueue->sid;
_219
$params["sms_task_queue_sid"] = $smsTaskQueue->sid;
_219
$params["voice_task_queue_sid"] = $voiceTaskQueue->sid;
_219
_219
$fileContent = File::get("resources/workflow.json");
_219
$interpolatedContent = sprintfn($fileContent, $params);
_219
return $interpolatedContent;
_219
}
_219
_219
/**
_219
* Prints the message indicating the workspace was successfully created and
_219
* shows the commands to export the workspace variables into the environment.
_219
*
_219
* @param $workspace
_219
* @param $workflow
_219
*/
_219
function printSuccessAndInstructions($workspace, $workflow)
_219
{
_219
$idleActivity = $workspace->findActivityByName("Idle")
_219
or die("Somehow the activity 'Idle' was not found.");
_219
$successMsg = "Workspace \"{$workspace->friendlyName}\"" .
_219
" was created successfully.";
_219
$this->printTitle($successMsg);
_219
$this->line(
_219
"The following variables will be set automatically."
_219
);
_219
$encondedWorkersPhone = http_build_query($workspace->getWorkerPhones());
_219
$envVars = [
_219
"WORKFLOW_SID" => $workflow->sid,
_219
"POST_WORK_ACTIVITY_SID" => $idleActivity->sid,
_219
"WORKSPACE_SID" => $workspace->sid,
_219
"PHONE_TO_WORKER" => $encondedWorkersPhone
_219
];
_219
updateEnv($envVars);
_219
foreach ($envVars as $key => $value) {
_219
$this->warn("export $key=$value");
_219
}
_219
}
_219
_219
/**
_219
* Prints a text separated up and doNwn by a token based line, usually "*"
_219
*/
_219
function printTitle($text)
_219
{
_219
$lineLength = strlen($text) + 2;
_219
$this->line(str_repeat("*", $lineLength));
_219
$this->line(" $text ");
_219
$this->line(str_repeat("*", $lineLength));
_219
}
_219
}

We have a Workspace, Workers and Task Queues... what's left? A Workflow. Let's see how to create one next!


Finally, we set up the Workflow using the following parameters:

  1. friendlyName as the name of a Workflow.
  2. assignmentCallbackUrl as the public URL where a request will be made when this Workflow assigns a Task to a Worker. We will learn how to implement it on the next steps.
  3. taskReservationTimeout as the maximum time we want to wait until a Worker handles a Task.
  4. configuration which is a set of rules for placing Task into Task Queues. The routing configuration will take a Task's attribute and match this with Task Queues. This application's Workflow rules are defined as:

    • selected_product=="ProgrammableSMS" expression for SMS Task Queue. This expression will match any Task with ProgrammableSMS as the selected_product attribute.
    • selected_product=="ProgrammableVoice expression for Voice Task Queue.

app/Console/Commands/CreateWorkspace.php


_219
<?php
_219
_219
namespace App\Console\Commands;
_219
_219
use App\TaskRouter\WorkspaceFacade;
_219
use Illuminate\Console\Command;
_219
use Illuminate\Support\Facades\File;
_219
use TaskRouter_Services_Twilio;
_219
use Twilio\Rest\Client;
_219
use Twilio\Rest\Taskrouter;
_219
use WorkflowRuleTarget;
_219
_219
class CreateWorkspace extends Command
_219
{
_219
/**
_219
* The name and signature of the console command.
_219
*
_219
* @var string
_219
*/
_219
protected $signature = 'workspace:create
_219
{host : Server hostname in Internet}
_219
{bob_phone : Phone of the first agent (Bob)}
_219
{alice_phone : Phone of the secondary agent (Alice)}';
_219
_219
/**
_219
* The console command description.
_219
*
_219
* @var string
_219
*/
_219
protected $description = 'Creates a Twilio workspace for 2 call agents';
_219
_219
private $_twilioClient;
_219
_219
/**
_219
* Create a new command instance.
_219
*/
_219
public function __construct(Client $twilioClient)
_219
{
_219
parent::__construct();
_219
$this->_twilioClient = $twilioClient;
_219
}
_219
_219
/**
_219
* Execute the console command.
_219
*
_219
* @return mixed
_219
*/
_219
public function handle()
_219
{
_219
$this->info("Create workspace.");
_219
$this->line("- Server: \t{$this->argument('host')}");
_219
$this->line("- Bob phone: \t{$this->argument('bob_phone')}");
_219
$this->line("- Alice phone: \t{$this->argument('alice_phone')}");
_219
_219
//Get the configuration
_219
$workspaceConfig = $this->createWorkspaceConfig();
_219
_219
//Create the workspace
_219
$params = array();
_219
$params['friendlyName'] = $workspaceConfig->name;
_219
$params['eventCallbackUrl'] = $workspaceConfig->event_callback;
_219
$workspace = WorkspaceFacade::createNewWorkspace(
_219
$this->_twilioClient->taskrouter,
_219
$params
_219
);
_219
$this->addWorkersToWorkspace($workspace, $workspaceConfig);
_219
$this->addTaskQueuesToWorkspace($workspace, $workspaceConfig);
_219
$workflow = $this->addWorkflowToWorkspace($workspace, $workspaceConfig);
_219
_219
$this->printSuccessAndInstructions($workspace, $workflow);
_219
}
_219
_219
/**
_219
* Get the json configuration of the Workspace
_219
*
_219
* @return mixed
_219
*/
_219
function createWorkspaceConfig()
_219
{
_219
$fileContent = File::get("resources/workspace.json");
_219
$interpolatedContent = sprintfn($fileContent, $this->argument());
_219
return json_decode($interpolatedContent);
_219
}
_219
_219
/**
_219
* Add workers to workspace
_219
*
_219
* @param $workspace WorkspaceFacade
_219
* @param $workspaceConfig string with Json
_219
*/
_219
function addWorkersToWorkspace($workspace, $workspaceConfig)
_219
{
_219
$this->line("Add Workers.");
_219
$idleActivity = $workspace->findActivityByName("Idle")
_219
or die("The activity 'Idle' was not found. Workers cannot be added.");
_219
foreach ($workspaceConfig->workers as $workerJson) {
_219
$params = array();
_219
$params['friendlyName'] = $workerJson->name;
_219
$params['activitySid'] = $idleActivity->sid;
_219
$params['attributes'] = json_encode($workerJson->attributes);
_219
$workspace->addWorker($params);
_219
}
_219
}
_219
_219
/**
_219
* Add the Task Queues to the workspace
_219
*
_219
* @param $workspace WorkspaceFacade
_219
* @param $workspaceConfig string with Json
_219
*/
_219
function addTaskQueuesToWorkspace($workspace, $workspaceConfig)
_219
{
_219
$this->line("Add Task Queues.");
_219
$reservedActivity = $workspace->findActivityByName("Reserved");
_219
$assignmentActivity = $workspace->findActivityByName("Busy");
_219
foreach ($workspaceConfig->task_queues as $taskQueueJson) {
_219
$params = array();
_219
$params['friendlyName'] = $taskQueueJson->name;
_219
$params['targetWorkers'] = $taskQueueJson->targetWorkers;
_219
$params['reservationActivitySid'] = $reservedActivity->sid;
_219
$params['assignmentActivitySid'] = $assignmentActivity->sid;
_219
$workspace->addTaskQueue($params);
_219
}
_219
}
_219
_219
/**
_219
* Create and configure the workflow to use in the workspace
_219
*
_219
* @param $workspace WorkspaceFacade
_219
* @param $workspaceConfig string with Json
_219
*
_219
* @return object with added workflow
_219
*/
_219
function addWorkflowToWorkspace($workspace, $workspaceConfig)
_219
{
_219
$this->line("Add Worflow.");
_219
$workflowJson = $workspaceConfig->workflow;
_219
$params = array();
_219
$params['friendlyName'] = $workflowJson->name;
_219
$params['assignmentCallbackUrl'] = $workflowJson->callback;
_219
$params['taskReservationTimeout'] = $workflowJson->timeout;
_219
$params['configuration'] = $this->createWorkFlowJsonConfig(
_219
$workspace,
_219
$workflowJson
_219
);
_219
return $workspace->addWorkflow($params);
_219
}
_219
_219
/**
_219
* Create the workflow configuration in json format
_219
*
_219
* @param $workspace
_219
* @param $workspaceConfig
_219
*
_219
* @return string configuration of workflow in json format
_219
*/
_219
function createWorkFlowJsonConfig($workspace, $workspaceConfig)
_219
{
_219
$params = array();
_219
$defaultTaskQueue = $workspace->findTaskQueueByName("Default") or die(
_219
"The 'Default' task queue was not found. The Workflow cannot be created."
_219
);
_219
$smsTaskQueue = $workspace->findTaskQueueByName("SMS") or die(
_219
"The 'SMS' task queue was not found. The Workflow cannot be created."
_219
);
_219
$voiceTaskQueue = $workspace->findTaskQueueByName("Voice") or die(
_219
"The 'Voice' task queue was not found. The Workflow cannot be created."
_219
);
_219
_219
$params["default_task_queue_sid"] = $defaultTaskQueue->sid;
_219
$params["sms_task_queue_sid"] = $smsTaskQueue->sid;
_219
$params["voice_task_queue_sid"] = $voiceTaskQueue->sid;
_219
_219
$fileContent = File::get("resources/workflow.json");
_219
$interpolatedContent = sprintfn($fileContent, $params);
_219
return $interpolatedContent;
_219
}
_219
_219
/**
_219
* Prints the message indicating the workspace was successfully created and
_219
* shows the commands to export the workspace variables into the environment.
_219
*
_219
* @param $workspace
_219
* @param $workflow
_219
*/
_219
function printSuccessAndInstructions($workspace, $workflow)
_219
{
_219
$idleActivity = $workspace->findActivityByName("Idle")
_219
or die("Somehow the activity 'Idle' was not found.");
_219
$successMsg = "Workspace \"{$workspace->friendlyName}\"" .
_219
" was created successfully.";
_219
$this->printTitle($successMsg);
_219
$this->line(
_219
"The following variables will be set automatically."
_219
);
_219
$encondedWorkersPhone = http_build_query($workspace->getWorkerPhones());
_219
$envVars = [
_219
"WORKFLOW_SID" => $workflow->sid,
_219
"POST_WORK_ACTIVITY_SID" => $idleActivity->sid,
_219
"WORKSPACE_SID" => $workspace->sid,
_219
"PHONE_TO_WORKER" => $encondedWorkersPhone
_219
];
_219
updateEnv($envVars);
_219
foreach ($envVars as $key => $value) {
_219
$this->warn("export $key=$value");
_219
}
_219
}
_219
_219
/**
_219
* Prints a text separated up and doNwn by a token based line, usually "*"
_219
*/
_219
function printTitle($text)
_219
{
_219
$lineLength = strlen($text) + 2;
_219
$this->line(str_repeat("*", $lineLength));
_219
$this->line(" $text ");
_219
$this->line(str_repeat("*", $lineLength));
_219
}
_219
}

Our workspace is completely setup. Now it's time to see how we use it to route calls.


Handle Twilio's Request

handle-twilios-request page anchor

Right after receiving a call, Twilio will send a request to the URL specified on the number's configuration.

The endpoint will then process the request and generate a TwiML response. We'll use the Say verb to give the user product alternatives, and a key they can press in order to select one. The Gather verb allows us to capture the user's key press.

app/Http/Controllers/IncomingCallController.php


_33
<?php
_33
_33
namespace App\Http\Controllers;
_33
_33
use App\Http\Requests;
_33
use Twilio\Twiml;
_33
_33
/**
_33
* Class IncomingCallController
_33
*
_33
* @package App\Http\Controllers
_33
*/
_33
class IncomingCallController extends Controller
_33
{
_33
_33
public function respondToUser()
_33
{
_33
$response = new Twiml();
_33
_33
$params = array();
_33
$params['action'] = '/call/enqueue';
_33
$params['numDigits'] = 1;
_33
$params['timeout'] = 10;
_33
$params['method'] = "POST";
_33
_33
$params = $response->gather($params);
_33
$params->say(
_33
'For Programmable SMS, press one. For Voice, press any other key.'
_33
);
_33
_33
return response($response)->header('Content-Type', 'text/xml');
_33
}
_33
}

We just asked the caller to choose a product, next we will use their choice to create the appropiate Task.


This is the endpoint set as the action URL on the Gather verb on the previous step. A request is made to this endpoint when the user presses a key during the call. This request has a Digits parameter that holds the pressed keys. A Task will be created based on the pressed digit with the selected_product as an attribute. The Workflow will take this Task's attributes and match them with the configured expressions in order to find a corresponding Task Queue, so an appropriate available Worker can be assigned to handle it.

We use the Enqueue verb with a workflowSid attribute to integrate with TaskRouter. Then the voice call will be put on hold while TaskRouter tries to find an available Worker to handle this Task.

app/Http/Controllers/EnqueueCallController.php


_46
<?php
_46
_46
namespace App\Http\Controllers;
_46
_46
use Illuminate\Http\Request;
_46
use Twilio\Twiml;
_46
_46
_46
/**
_46
* Class EnqueueCallController
_46
*
_46
* @package App\Http\Controllers
_46
*/
_46
class EnqueueCallController extends Controller
_46
{
_46
_46
public function enqueueCall(Request $request)
_46
{
_46
$workflowSid = config('services.twilio')['workflowSid']
_46
or die("WORKFLOW_SID is not set in the system environment");
_46
_46
$selectProductInstruction = new \StdClass();
_46
$selectProductInstruction->selected_product
_46
= $this->_getSelectedProduct($request);
_46
_46
$response = new Twiml();
_46
$enqueue = $response->enqueue(['workflowSid' => $workflowSid]);
_46
$enqueue->task(json_encode($selectProductInstruction));
_46
_46
return response($response)->header('Content-Type', 'text/xml');
_46
}
_46
_46
/**
_46
* Gets the wanted product upon the user's input
_46
*
_46
* @param $request Request of the user
_46
*
_46
* @return string selected product: "ProgrammableSMS" or "ProgrammableVoice"
_46
*/
_46
private function _getSelectedProduct($request)
_46
{
_46
return $request->input("Digits") == 1
_46
? "ProgrammableSMS"
_46
: "ProgrammableVoice";
_46
}
_46
}

After sending a Task to Twilio, let's see how we tell TaskRouter which Worker to use to execute that task.


When TaskRouter selects a Worker, it does the following:

  1. The Task's Assignment Status is set to reserved .
  2. A Reservation instance is generated, linking the Task to the selected Worker.
  3. At the same time the Reservation is created, a POST request is made to the Workflow's AssignmentCallbackURL, which was configured using the Artisan command workspace:create(link takes you to an external page) . This request includes the full details of the Task, the selected Worker, and the Reservation.

Handling this Assignment Callback is a key component of building a TaskRouter application as we can instruct how the Worker will handle a Task. We could send a text, email, push notifications or make a call.

Since we created this Task during a voice call with an Enqueue verb, let's instruct TaskRouter to dequeue the call and dial a Worker. If we do not specify a to parameter with a phone number, TaskRouter will pick the Worker's contact_uri attribute.

We also send a post_work_activity_sid which will tell TaskRouter which Activity to assign this worker after the call ends.

app/Http/Controllers/CallbackController.php


_117
<?php
_117
_117
namespace App\Http\Controllers;
_117
_117
use App\Exceptions\TaskRouterException;
_117
use App\MissedCall;
_117
use Illuminate\Http\Request;
_117
use Illuminate\Support\Facades\Log;
_117
use Twilio\Rest\Client;
_117
_117
/**
_117
* Class CallbackController Handles callbacks
_117
*
_117
* @package App\Http\Controllers
_117
*/
_117
class CallbackController extends Controller
_117
{
_117
/**
_117
* Callback endpoint for Task assignments
_117
*/
_117
public function assignTask()
_117
{
_117
$dequeueInstructionModel = new \stdClass;
_117
$dequeueInstructionModel->instruction = "dequeue";
_117
$dequeueInstructionModel->post_work_activity_sid
_117
= config('services.twilio')['postWorkActivitySid'];
_117
_117
$dequeueInstructionJson = json_encode($dequeueInstructionModel);
_117
_117
return response($dequeueInstructionJson)
_117
->header('Content-Type', 'application/json');
_117
}
_117
_117
/**
_117
* Events callback for missed calls
_117
*
_117
* @param $request Request with the input data
_117
* @param $twilioClient Client of the Twilio Rest Api
_117
*/
_117
public function handleEvent(Request $request, Client $twilioClient)
_117
{
_117
$missedCallEvents = config('services.twilio')['missedCallEvents'];
_117
_117
$eventTypeName = $request->input("EventType");
_117
_117
if (in_array($eventTypeName, $missedCallEvents)) {
_117
$taskAttr = $this->parseAttributes("TaskAttributes", $request);
_117
if (!empty($taskAttr)) {
_117
$this->addMissingCall($taskAttr);
_117
_117
$message = config('services.twilio')["leaveMessage"];
_117
return $this->redirectToVoiceMail(
_117
$twilioClient, $taskAttr->call_sid, $message
_117
);
_117
}
_117
} else if ('worker.activity.update' === $eventTypeName) {
_117
$workerActivityName = $request->input("WorkerActivityName");
_117
if ($workerActivityName === "Offline") {
_117
$workerAttr = $this->parseAttributes("WorkerAttributes", $request);
_117
$this->notifyOfflineStatusToWorker(
_117
$workerAttr->contact_uri, $twilioClient
_117
);
_117
}
_117
}
_117
}
_117
_117
protected function parseAttributes($name, $request)
_117
{
_117
$attrJson = $request->input($name);
_117
return json_decode($attrJson);
_117
}
_117
_117
protected function addMissingCall($task)
_117
{
_117
$missedCall = new MissedCall(
_117
[
_117
"selected_product" => $task->selected_product,
_117
"phone_number" => $task->from
_117
]
_117
);
_117
$missedCall->save();
_117
Log::info("New missed call added: $missedCall");
_117
}
_117
_117
protected function redirectToVoiceMail($twilioClient, $callSid, $message)
_117
{
_117
$missedCallsEmail = config('services.twilio')['missedCallsEmail']
_117
or die("MISSED_CALLS_EMAIL_ADDRESS is not set in the environment");
_117
_117
$call = $twilioClient->calls->getContext($callSid);
_117
if (!$call) {
_117
throw new TaskRouterException("The specified call does not exist");
_117
}
_117
_117
$encodedMsg = urlencode($message);
_117
$twimletUrl = "http://twimlets.com/voicemail?Email=$missedCallsEmail" .
_117
"&Message=$encodedMsg";
_117
$call->update(["url" => $twimletUrl, "method" => "POST"]);
_117
}
_117
_117
protected function notifyOfflineStatusToWorker($workerPhone, $twilioClient)
_117
{
_117
$twilioNumber = config('services.twilio')['number']
_117
or die("TWILIO_NUMBER is not set in the system environment");
_117
_117
$params = [
_117
"from" => $twilioNumber,
_117
"body" => config('services.twilio')["offlineMessage"]
_117
];
_117
_117
$twilioClient->account->messages->create(
_117
$workerPhone,
_117
$params
_117
);
_117
}
_117
_117
}

Now that our Tasks are routed properly, let's deal with missed calls in the next step.


This endpoint will be called after each TaskRouter Event is triggered. In our application, we are trying to collect missed calls, so we would like to handle the workflow.timeout event. This event is triggered when the Task waits more than the limit set on Workflow Configuration-- or rather when no worker is available.

Here we use TwilioRestClient to route this call to a Voicemail Twimlet(link takes you to an external page). Twimlets are tiny web applications for voice. This one will generate a TwiML response using Say verb and record a message using Record verb. The recorded message will then be transcribed and sent to the email address configured.

We are also listening for task.canceled. This is triggered when the customer hangs up before being assigned to an agent, therefore canceling the task. Capturing this event allows us to collect the information from the customers that hang up before the Workflow times out.

app/Http/Controllers/CallbackController.php


_117
<?php
_117
_117
namespace App\Http\Controllers;
_117
_117
use App\Exceptions\TaskRouterException;
_117
use App\MissedCall;
_117
use Illuminate\Http\Request;
_117
use Illuminate\Support\Facades\Log;
_117
use Twilio\Rest\Client;
_117
_117
/**
_117
* Class CallbackController Handles callbacks
_117
*
_117
* @package App\Http\Controllers
_117
*/
_117
class CallbackController extends Controller
_117
{
_117
/**
_117
* Callback endpoint for Task assignments
_117
*/
_117
public function assignTask()
_117
{
_117
$dequeueInstructionModel = new \stdClass;
_117
$dequeueInstructionModel->instruction = "dequeue";
_117
$dequeueInstructionModel->post_work_activity_sid
_117
= config('services.twilio')['postWorkActivitySid'];
_117
_117
$dequeueInstructionJson = json_encode($dequeueInstructionModel);
_117
_117
return response($dequeueInstructionJson)
_117
->header('Content-Type', 'application/json');
_117
}
_117
_117
/**
_117
* Events callback for missed calls
_117
*
_117
* @param $request Request with the input data
_117
* @param $twilioClient Client of the Twilio Rest Api
_117
*/
_117
public function handleEvent(Request $request, Client $twilioClient)
_117
{
_117
$missedCallEvents = config('services.twilio')['missedCallEvents'];
_117
_117
$eventTypeName = $request->input("EventType");
_117
_117
if (in_array($eventTypeName, $missedCallEvents)) {
_117
$taskAttr = $this->parseAttributes("TaskAttributes", $request);
_117
if (!empty($taskAttr)) {
_117
$this->addMissingCall($taskAttr);
_117
_117
$message = config('services.twilio')["leaveMessage"];
_117
return $this->redirectToVoiceMail(
_117
$twilioClient, $taskAttr->call_sid, $message
_117
);
_117
}
_117
} else if ('worker.activity.update' === $eventTypeName) {
_117
$workerActivityName = $request->input("WorkerActivityName");
_117
if ($workerActivityName === "Offline") {
_117
$workerAttr = $this->parseAttributes("WorkerAttributes", $request);
_117
$this->notifyOfflineStatusToWorker(
_117
$workerAttr->contact_uri, $twilioClient
_117
);
_117
}
_117
}
_117
}
_117
_117
protected function parseAttributes($name, $request)
_117
{
_117
$attrJson = $request->input($name);
_117
return json_decode($attrJson);
_117
}
_117
_117
protected function addMissingCall($task)
_117
{
_117
$missedCall = new MissedCall(
_117
[
_117
"selected_product" => $task->selected_product,
_117
"phone_number" => $task->from
_117
]
_117
);
_117
$missedCall->save();
_117
Log::info("New missed call added: $missedCall");
_117
}
_117
_117
protected function redirectToVoiceMail($twilioClient, $callSid, $message)
_117
{
_117
$missedCallsEmail = config('services.twilio')['missedCallsEmail']
_117
or die("MISSED_CALLS_EMAIL_ADDRESS is not set in the environment");
_117
_117
$call = $twilioClient->calls->getContext($callSid);
_117
if (!$call) {
_117
throw new TaskRouterException("The specified call does not exist");
_117
}
_117
_117
$encodedMsg = urlencode($message);
_117
$twimletUrl = "http://twimlets.com/voicemail?Email=$missedCallsEmail" .
_117
"&Message=$encodedMsg";
_117
$call->update(["url" => $twimletUrl, "method" => "POST"]);
_117
}
_117
_117
protected function notifyOfflineStatusToWorker($workerPhone, $twilioClient)
_117
{
_117
$twilioNumber = config('services.twilio')['number']
_117
or die("TWILIO_NUMBER is not set in the system environment");
_117
_117
$params = [
_117
"from" => $twilioNumber,
_117
"body" => config('services.twilio')["offlineMessage"]
_117
];
_117
_117
$twilioClient->account->messages->create(
_117
$workerPhone,
_117
$params
_117
);
_117
}
_117
_117
}

Most of the features of our application are implemented. The last piece is allowing the Workers to change their availability status. Let's see how to do that next.


Change a Worker's Activity

change-a-workers-activity page anchor

We have created this endpoint, so a worker can send an SMS message to the support line with the command "On" or "Off" to change their availability status.

This is important as a worker's activity will change to Offline when they miss a call. When this happens, they receive an SMS letting them know that their activity has changed, and that they can reply with the On command to make themselves available for incoming calls again.

app/Http/Controllers/MessageController.php


_50
<?php
_50
_50
namespace App\Http\Controllers;
_50
_50
use App\Exceptions\TaskRouterException;
_50
use Illuminate\Http\Request;
_50
use App\TaskRouter\WorkspaceFacade;
_50
use Twilio\Twiml;
_50
_50
class MessageController extends Controller
_50
{
_50
_50
public function handleIncomingMessage(
_50
Request $request, WorkspaceFacade $workspace
_50
) {
_50
$cmd = strtolower($request->input("Body"));
_50
$fromNumber = $request->input("From");
_50
$newWorkerStatus = ($cmd === "off") ? "Offline" : "Idle";
_50
_50
$response = new Twiml();
_50
_50
try {
_50
$worker = $this->getWorkerByPhone($fromNumber, $workspace);
_50
$this->updateWorkerStatus($worker, $newWorkerStatus, $workspace);
_50
_50
$response->sms("Your status has changed to {$newWorkerStatus}");
_50
_50
} catch (TaskRouterException $e) {
_50
$response->sms($e->getMessage());
_50
}
_50
_50
return response($response)->header('Content-Type', 'text/xml');
_50
}
_50
_50
function updateWorkerStatus($worker, $status, $workspace)
_50
{
_50
$wantedActivity = $workspace->findActivityByName($status);
_50
$workspace->updateWorkerActivity($worker, $wantedActivity->sid);
_50
}
_50
_50
protected function getWorkerByPhone($phone, $workspace)
_50
{
_50
$phoneToWorkerStr = config('services.twilio')['phoneToWorker'];
_50
parse_str($phoneToWorkerStr, $phoneToWorkerArray);
_50
if (empty($phoneToWorkerArray[$phone])) {
_50
throw new TaskRouterException("You are not a valid worker");
_50
}
_50
return $workspace->findWorkerBySid($phoneToWorkerArray[$phone]);
_50
}
_50
}

Congratulations! You finished this tutorial. As you can see, using Twilio's TaskRouter is quite simple.


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

Automated-Survey(link takes you to an external page)

Instantly collect structured data from your users with a survey conducted over a call or SMS text messages. Let's get started!

ETA-Notifications(link takes you to an external page)

Learn how to implement ETA Notifications using Laravel and Twilio.

Did this help?

did-this-help page anchor

Thanks for checking out this tutorial! If you have any feedback to share with us, we'd love to hear it. Tweet @twilio(link takes you to an external page) to let us know what you think!


Rate this page: