PAGI: Quick telephony applications using AGI and PHP
Introduction
PAGI (not to be confused with phpagi) is a PHP 5.3 client for AGI: Asterisk Gateway Interface. AGI is a very simple protocol, and it has been implemented in a wide variety of languages, allowing to quickly create telephony applications, like IVR's, voicemails, prepaid telephony systems, etc. If you want or need to create this kind of telephony applications using PHP, you will find PAGI very useful.
By the way, it turns out this is quite a long article, but just because I wanted to enumerate and get an overview of all the features (or almost) that pagi offers, you can really skip the parts you are not interested in, it's kind of a "modular" reading.
NOTE: Since version 1.11.0 PAGI comes with call flow nodes, which are *very* useful when creating ivr applications without having to manually use the PAGI client. You can find out more here.
Getting it
First, a couple of useful links:
- Example application: Complete IVR application skeleton, including unit tests.
- http://marcelog.github.com/PAGI/: The homepage.
- http://pear.marcelog.name/: The PEAR channel
- http://packagist.org/packages/marcelog/pagi: Packagist home
- API documentation
- https://github.com/marcelog/PAGI/: The GitHub repository
- Full PAGI examples
- http://ci.marcelog.name:8080/job/PAGI/lastSuccessfulBuild/artifact/build/: The CI server, containing the latest successful built artifacts, phar and pear packages.
- http://pear.apache.org/log4php/: The log4php pear channel.
- AGI Protocol article
A brief parenthesis: Installing log4php (only needed if installing manually or using the phar)
Unless you are installing PAGI via the pear channel, you need to install a dependency first. PAGI uses log4php as its logging system, so let's install log4php via the pear channel:
Please also see the official installation guide of log4php.
$ pear channel-discover pear.apache.org/log4php $ pear install pear.apache.org/log4php/Apache_log4php-2.1.0
Make sure the directory "log4php" inside the log4php distribution is in your include path when using PAGI. If you get this error:
PHP Fatal error: Class 'Logger' not found
You need to include the logphp subdirectory correctly in your include_path.
You can download and install PAGI in a number of ways, let's first examine them and then see how to bootstrap the pagi client:
Option A: Installing and using the PHAR distribution
You can get the latest phar and pear package from the CI server. At this time, the latest version is 1.11.0, so the available artifact is named "PAGI-1.11.0.phar". Let's fetch it:
$ wget http://ci.marcelog.name:8080/job/PAGI/lastSuccessfulBuild/artifact/build/PAGI-1.11.0.phar
Let's see how to use it:
// Include the phar file.
require_once 'PAGI-1.11.0.phar';
// Set the include path to have pagi's phar first
// (just to avoid having another installation in
// the include_path loading first).
ini_set('include_path', implode(PATH_SEPARATOR, array(
'phar://pagi.phar', ini_get('include_path')
)));
That's it. You can now bootstrap the pagi client and do your thing (see below).
Option B: Installing the PEAR distribution manually
As stated before, you can get the pear package at the CI server. You can download it and install it manually (again, 1.11.0 is the latest version at the time of this writing):
$ wget http://ci.marcelog.name:8080/job/PAGI/lastSuccessfulBuild/artifact/build/PAGI-1.11.0.tgz $ pear install PAGI-1.11.0.tgz
Option C: Installing via the PEAR Channel
A pear channel is available here. If you want to use it, just:
$ pear channel-discover pear.marcelog.name $ pear install marcelog/PAGI
Either way, to use it, make sure log4php is in your include path. PAGI will already be, since it's installed via pear:
/* * If you have a PSR-0 compatible autoloader, you wont * need this, just make sure your autoloader is * bootstrapped. If you don't, PAGI comes with its own * PSR-0 autoloader, this can also be used for your own * benefit since you wont need any other autoloader if * your tree honors PSR-0. */ require_once 'PAGI/Autoloader/Autoloader.php'; PAGI\Autoloader\Autoloader::register();
That's it!
Option D: Getting the source, installing from github
You can download the ZIP file from github, and also, you can clone the repository:
$ git clone git://github.com/marcelog/PAGI.git
When installed this way, you need to make sure you set the include path into the /src/mg subdirectory inside the root tree, so the PSR-0 autoloader would load from the src/mg/PAGI subdirectory.
Option E: Use composer
Just add the package "marcelog/pagi":
{
"require": {
"marcelog/pagi": "dev-master"
},
"repositories": [
{
"type": "pear",
"url": "http://pear.apache.org/log4php/"
}]
}
Packagist URL: http://packagist.org/packages/marcelog/pagi
Setup asterisk dialplan
First of all, you've got to setup asterisk so it will run your agi application in a given extension. For example, we can do this in the extensions.conf file:
exten => _X.,1,AGI(/tmp/app.php) exten => _X.,n,Hangup
The _X. is a dialplan pattern, it will match any number dialed as long as it is 1 digit long. You can of course go ahead and write your own pattern or use an exact number, like "111" or "5555555".
So the first line uses the dialplan AGI command to invoke your application. This tells asterisk to fork() a new process and exec() the given path (in this case /tmp/app.php, but it can be anything that is executable, we'll see that in a bit).
The second line is just a safe bet, a Hangup command, just in case your app goes a little funny on the call and does not terminate it in an appropiate way.
Create the agi application file
Of course this file is just an entry point, and in this particular case, also the application. But as any other entry point, it can be used to just bootstrap your real application, in the case you have a more complex scenario.
So, assuming your php binary is located in "/usr/bin/php" create the file /tmp/app.php with this content (if using the phar file):
#!/usr/bin/php
<?php
require_once 'PAGI-1.11.0.phar';
ini_set('include_path', implode(PATH_SEPARATOR, array(
'phar://pagi.phar', ini_get('include_path')
)));
Or this one, if you're using the source or pear distributions:
#!/usr/bin/php <?php require_once 'PAGI/Autoloader/Autoloader.php'; PAGI\Autoloader\Autoloader::register();
The first line is the shebang line that will make /usr/bin/php the interpreter for the file, effectively using php to run our application.
In order for the operating system to execute file, make it executable:
$ chmod a+x /tmp/app.php
NOTE: You should only give execute permissions to the asterisk user instead of just making it executable for all users, this was just an example.
If you want to have a little better structure for the project (with configurable php installation and php.ini files), or just dont like to hardcode the php interpreter there (good for you!), you might want to take a look at this article.
The PAGI Client
The pagi client is the central player here, it will handle all the communication with asterisk during the call. Actually, as you can see, this is just an interface. An abstract implementation exists (the AbstractClient) that implements the AGI commands declared in the IClient interface, *but* it leaves the implementation details for managing the I/O to its subclasses.
Specifically, this lets you use the standard client implementation, or something like pami's async agi client, suitable for async agi applications. You can read more about pami here.
In this case, we're going to use the standard pagi client implementation.
Getting an instance of the client
This is as easy as:
$pagiClientOptions = array(
'log4php.properties' => __DIR__ . '/log4php.properties',
);
use PAGI\Client\Impl\ClientImpl as PagiClient;
$pagiClient = PagiClient::getInstance($pagiClientOptions);
NOTE: The client accepts an array with options. The available options are:
- log4php.properties: Optional. If set, should contain the absolute path to the log4php.properties file
- stdin: Optional. If set, should contain an already open stream from where the client will read data (useful to make it interact with fastagi servers or even text files to mock stuff when testing). If not set, stdin will be used by the client
- stdout: Same as stdin but for the output of the client
NOTE: Since AGI establishes the use of stdin and stdout for communication, you cant write anything that outputs to console directly (like echo's, or var_dump's). By default log4php will log to console, so let's first configure it to log to a file (instead of the default, which is log to console). This will allow us to have a logger inside the application and avoid messing up the AGI communication.
You can configure log4php on your own (see this) or have pagi do it on its own, by passing an option to the client with the log4php configuration (just as we did above, in this case, an .ini file, but you can use xml). A sample log4php.properties ini file:
log4php.appender.default = LoggerAppenderDailyFile
log4php.appender.default.layout = LoggerLayoutPattern
log4php.appender.default.layout.ConversionPattern = "%d{ISO8601} [%p] %c: %m%n"
log4php.appender.default.file = /tmp/log.log
log4php.rootLogger = DEBUG, default
Checkpoint #1: A basic application
$pagiClientOptions = array(
'log4php.properties' => __DIR__ . '/log4php.properties',
);
use PAGI\Client\Impl\ClientImpl as PagiClient;
$pagiClient = PagiClient::getInstance($pagiClientOptions);
$pagiClient->answer();
$pagiClient->sayDigits(123);
$pagiClient->hangup();
Now dial into your asterisk, you should listen to the numbers "one", "two", and "three". From now on, I'll mention some if the many things you can do through the pagi client, you should refer to the IClient interface to see all the available stuff!
The decorated Results
Sometimes you want the user to input 1 digit, or many digits, or let him/her interrupt the messages played, or do something if no input is received, or any kind of logic associated with playing and reading simultaneously. Every result from an agi command that pagi issues, is returned as an IResult. This is then implemented in ResultDecorator, effectively implementing the decorator pattern to return the result of the operation. This is so, because sometimes you only want to play a file, sometimes you just want to read digits, and sometimes you want to play AND read input from the user, and you might need to check the result of one or all the operations. See the API documentation for all the results available, in the namespace PAGI\Client\Result. You should check the api documentation for the method you are using to see what kind of result is available (ExecResult, RecordResult, FaxResult, PlayResult, etc). Do not be freighten! It's quite easy to use them. See below.
Playing a sound file
$result = $pagiClient->streamFile($aSoundFile, $escapeDigits);
Where:
- $aSoundFile: A sound file to play, like 'welcome', or 'silence/1', etc
- $escapeDigits: The digits that can be used to skip the sound file, like "#" or "01234567890*#".
$result will be a PlayResult that you can use to get information about what happened while the file was playing.
if ($result->isTimeout()) {
// The user did not interrupt the play
} else {
// The user interrupted the play
$digits = $result->getDigits(); // Get what the user pressed
}
Reading input from the user
To read input from the user, you have a couple of options:
// Read a single digit, play a sound file as a prompt.
$result = $pagiClient->getOption(
$aSoundFile, $escapeDigits, $maxInputTime
);
// Read a single digit, no sound file played.
$result = $pagiClient->waitDigit($maxInputTime);
// Get at least 1 digit, playing a sound file as a prompt.
$result = $pagiClient->getData(
$aSoundFile, $maxInputTime, $maxDigitsToRead
);
Where:
- $maxInputTime: The maximum amount of time to wait for the user input in milliseconds.
- $maxDigitsToRead: The maximum number of digits a user can input for this reading.
In the case of waitDigit(), the result is a DigitReadResult, and the others return a PlayResult.
Playing indication tones
You can also play some standard tones, these might need some tweaking in your indications.conf file:
$pagiClient->playDialTone(); $pagiClient->playCongestionTone($seconds); $pagiClient->playBusyTone($seconds);
And you can also send indications at the signaling level, without playing any audio:
$pagiClient->indicateProgress(); $pagiClient->indicateBusy(); $pagiClient->indicateCongestion();
Playing Music On Hold
Putting music on hold is also quite easy. You might need to configure your moh.conf file:
$pagiClient->setMusic(true, 'myMOHClass'); $pagiClient->setMusic(false);
Logging through asterisk
In case you want to log stuff through the asterisk logger (so your messages get to the asterisk console or general asterisk log files), you can use the AsteriskLogger interface, remember that when logging through asterisk, you might need to tweak your logger.conf file:
$asteriskLogger = $pagiClient->getAsteriskLogger();
$asteriskLogger->dtmf("A DTMF priority message");
$asteriskLogger->verbose("A VERBOSE priority message");
$asteriskLogger->debug("A DEBUG priority message");
$asteriskLogger->notice("A NOTICE priority message");
$asteriskLogger->error("An ERROR priority message");
$asteriskLogger->warn("A WARNING priority message");
Manipulating the channel variables
When AGI handshake starts, the server (asterisk) sends a couple of channel variables. The pagi client offers the IChannelVariables interface to access them:
$channelVariables = $pagiClient->getChannelVariables(); $asteriskLogger->debug($channelVariables->getCallerId()); $asteriskLogger->debug($channelVariables->getDNIS());
To set a variable, you would do:
$pagiClient->setVariable("myVariable", "myValue");
$asteriskLogger->debug($pagiClient->getVariable("myVariable"));
The IChannelVariables also provides access to some important environment variables. For example, to get the directory where spooled call files should go:
$spoolDir = $channelVariables->getDirectorySpool();
Manipulating the CDR (Call Detail Record)
Pagi offers the ICDR interface to interact with the cdr's, by setting custom values and retrieving the ones set by asterisk itself:
$cdr = $pagiClient->getCDR();
$cdr->setUserfield("my own content here");
$cdr->setAccountCode("blah");
$cdr->setCustom("myOwnField", "withMyOwnValue");
$asteriskLogger->debug($cdr->getAnswerLength());
Working with the Caller ID's
In order to get and set caller id values, you can access the ICallerId interface:
$clid = $pagiClient->getCallerId();
$asteriskLogger->debug($clid->getNumber());
$clid->setNumber('123123');
In this case, the next dial command will carry the caller id set.
To set the caller id presentation mode:
$clid = $pagiClient->getCallerId();
$clid->setCallerPres("allowed_not_screened");
For the complete list of caller id presentation modes, see: http://www.voip-info.org/wiki/view/Asterisk+func+CALLERPRES
Manipulating SIP headers
Before issuing a dial(), you may need to set some sip headers (to set privacy settings, or whatever). Here's how you can do it with the pagi client:
$pagiClient->sipHeaderAdd('Privacy', 'Id');
$pagiClient->sipHeaderAdd('P-Asserted-Identity', 'Anonymous');
To delete a header:
$pagiClient->sipHeaderRemove('Privacy');
$pagiClient->sipHeaderRemove('P-Asserted-Identity');
Spooling calls through call files
Asterisk lets you use call files to generate calls automatically, you can access this feature via the ICallSpool interface:
use PAGI\DialDescriptor\SIPDialDescriptor;
use PAGI\CallSpool\CallFile;
use PAGI\CallSpool\Impl\CallSpoolImpl;
$dialDescriptor = new SIPDialDescriptor("myDevice", "myProvider");
$callFile = new CallFile($dialDescriptor);
$callFile->setCallerId('123');
$callFile->setContext('campaignContext');
$callFile->setExtension('555');
$callFile->setPriority(1);
$spool = CallSpoolImpl::getInstance(array(
"tmpDir" => "/tmp/temporaryDirForSpool",
"spoolDir" => "/var/lib/asterisk/spool/outgoing"
));
The dial descriptor is an abstraction over dial strings, so you dont have to code them yourself. Currently, PAGI supports SIPDialDescriptor and DAHDIDialDescriptor.
This example will then issue a dial to SIP/myDevice@myProvider with the caller id "123". When the call is answered, it will go to context "campaignContext", in the priority 1 of the extension 555. There are many options you can use in the CallFile, like custom variables, timeouts, etc.
The array passed to the CallSpoolImpl::getInstance() method is necessary so the call file can be created in a temporary directory and then moved to the real asterisk spool directory, so asterisk will pick it up when it's completely written to disk.
Dialing
If you want to Dial from an agi application:
$result = dial("DAHDI/g1/5555555", array(60, 'rh'));
if ($result->isAnswer()) {
$asteriskLogger->debug($result->getAnsweredTime());
}
The result is a DialResult
Last but not least: The PAGI Application
The pagi client can also be used inside a PAGI Application. That is, your own ivr application can be itself a PAGIApplication, by extending it. This will provide a number of features:
- An error handler is automatically registered, and will invoke PAGIApplication::errorHandler() method.
- A signal handler is automatically registered for the signals SIGINT, SIGQUIT, SIGTERM, SIGHUP, SIGUSR1, SIGUSR2, SIGCHLD, SIGALRM, and will invoke PAGIApplication::signalHandler() method.
- A shutdown handler is automatically registered, and will invoke PAGIApplication::shutdown()
- An init method, PAGIApplication::init()
- A log4php logger instance in PAGIApplication::$logger (protected).
Checkpoint #2: A complete PAGI Application Skeleton
#!/usr/bin/php
<?php
// Include the phar file.
require_once 'PAGI-1.11.0.phar';
// Set the include path to have pagi's phar first
// (just to avoid having another installation in
// the include_path loading first).
ini_set('include_path', implode(PATH_SEPARATOR, array(
'phar://pagi.phar', ini_get('include_path')
)));
use PAGI\Application\PAGIApplication;
use PAGI\Client\Impl\ClientImpl as PagiClient;
class MyPagiApplication extends PAGIApplication
{
protected $agi;
protected $asteriskLogger;
protected $channelVariables;
public function run()
{
$this->asteriskLogger->notice("Run");
$this->logger->info("Run");
}
public function init()
{
$this->logger->info('Init');
$this->agi = $this->getAgi();
$this->asteriskLogger = $this->agi->getAsteriskLogger();
$this->channelVariables = $this->agi->getChannelVariables();
$this->asteriskLogger->notice('Init');
}
public function signalHandler($signo)
{
$this->asteriskLogger->notice("Got signal: $signo");
$this->logger->info("Got signal: $signo");
}
public function errorHandler($type, $message, $file, $line)
{
$this->asteriskLogger->error("$message at $file:$line");
$this->logger->error("$message at $file:$line");
}
public function shutdown()
{
$this->asteriskLogger->notice('Shutdown');
}
}
// Go, go, gooo!
$pagiClientOptions = array(
'log4php.properties' => __DIR__ . '/log4php.properties',
);
$pagiClient = PagiClient::getInstance($pagiClientOptions);
$pagiAppOptions = array(
'pagiClient' => $pagiClient,
);
$pagiApp = new MyPagiApplication($pagiAppOptions);
$pagiApp->init();
$pagiApp->run();
See this for a complete example of an ivr application including unit tests.
Conclusion
Enough writing already, right? Go make your own ivr applications now :)