Advanced telephony applications with PHP and PAGI using call flow nodesTweet
Write PHP nodes for your IVR or Telephony Applications for Asterisk PBX
In "PAGI: Quick telephony applications using AGI and PHP" I've talked about how to create telephony applications using nothing else than the standard pagi client. Also, in "Unit test your PHP IVR applications with PAGI" I've shown how to unit test them, by using the mocked client pagi. Now, since version 1.10.0, PAGI comes with a neat feature, which is a small abstraction layer over the pagi client, called "Nodes". Also, the "NodeController" will orchestrate how those nodes interact with each other. Nodes are essentially call flow nodes.
These new features will allow you to implement complete call flows in no time, and maybe even without using the pagi client by yourself. In this article, I'll introduce the nodes by themselves (and how to unit test them), and will talk about the node controller in "Making your ivr nodes (call) flow with PAGI".
First, a couple of useful links:
- PAGI introduction
- Unit testing with PAGI
- http://marcelog.github.com/PAGI/: The homepage.
- PAGI API documentation
- Node API
- MockedNode API
What can you in your VoIP application from your PHP nodes
- Fluent interface.
- A node has a resulting state after being run: COMPLETE, CANCEL, TIMEOUT, MAX_INPUT_REACHED
- Can play prompt messages (sound files, saying digits/numbers/datetimes).
- Can expect input from users.
- Can play a message on no input from the user (except in the last attempt).
- Expected input can be of exactly K digits, or of a length between M and N (0 -zero- by default).
- Input may (or may not) interrupt prompt messages (interruptable by default).
- Can configure a DTMF digit to behave as a "cancel digit", allowing the node to be "cancelled" (usually *, none by default).
- Can configure a DTMF digit to behave as the "end of input" digit, (usually #, none by default).
- Cancel digit can be configured to cancel and retry the input or cancel the node completely (will cancel the node by default).
- Can use a maximum timeout between expected digits (infinite by default).
- Can use a maximum timeout to enter the complete input (infinite by default).
- Once the user entered input, it can be validated.
- Messages can be played for each failed validation or when timeout has expired.
- Error messages from failed validations may (or may not) be interrupted by user input (interruptable by default).
- Can configure a maximum attempt number to use to enter a valid input (1 by default).
- Can play a message when maximum attempts has been reached (none by default).
- Can carry state: each node has an internal registry where temporal data can be stored/retrieved, useful to pass information between nodes.
- Can execute callbacks: before/after running the node, on validated/failed input.
Creating a PHP Node for your Application
First, get a pagi client instance:
Then, just create a node:
To run the node:
To get what the user entered as input after running the node:
To access the pagi client used for that node:
A Basic IVR (Interactive Voice Response) Menu written in a few PHP nodes
Let's see a semi-complex example of an ivr menu. Of course this is just an example and it's possible you may not need everything shown:
The above code will setup a node that will prompt the user with the sound "main-menu-prompt", will expect exactly 1 digit as input, and will give the user 3 opportunities to enter a valid input. A timeout of 3 seconds is set to enter the input. The input will be validated with just 1 validation, named "standardOptionValidation", and if this validation fails, the sound file "invalid-option" will be played. If the user does not enter any input (i.e: the timeout expires) the sound file "please-choose-an-option" will be played (except in the last attempt). When the maximum allowed input attempts has been reached without a valid input, the sound file "max-attempts-reached" will be played. Also, after each invalid input, a callback will add the sound file "please-try-again" to be played before the prompt and after the failed validation message, except in the last attempt.
A note about validators: Validators can have an optional sound file to be played, but that's not mandatory at all. However, it *is* mandatory to return true to indicate success and false to indicate a validation failure. You can configure as many validators as needed (by calling validateInputWith() several times) and they will be executed in order. The validator's execution will stop when any of them returns false (that is, only 1 failed validation is allowed).
If you want to play the "no input" message in the last attempt too, you can use:
To check how this node ended up, we could use the following code (note that this is pretty much the job of the NodeController, which we are not using *yet*):
You can chain as many message prompts as desired. Message prompts can play a sound file, say some digits, a number, or a datetime. For example, this plays multiple messages to actually build the prompt:
Manipulating pre prompt messages and audios in your PHP node
Messages can be played before playing the prompt. These messages are called "Pre Prompt" messages. These messages are for example the sound files played when a validation fails, or the "no input" message. These messages are interruptable by default, and if a digit is pressed while playing them, it will also interrupt the prompt messages themselves and the digit counts as input to be handled by the node. This behaviour can be altered. For example, if you would like to make the pre prompt messages interruptable but continue to play the prompt messages:
If you would like to make the pre prompt messages not interruptable:
To programatically add a pre prompt message:
Remember that pre prompt messages are dynamic. They are cleared (reset) after playing them, so if you want to replay a pre prompt message, you have to add it yourself (probably in one of the callbacks allowed for a node, see below).
Playing digits, numbers, and datetimes
Let's now create a node that will play back to the user his/her telephone number and the amount of money available, with a given due date. This node wont accept any input and cant be interrupted:
To know more about the datetime formats in asterisk, see this.
A sample PHP node: Entering a calling card PIN in your IVR
Let's see how we could ask a user for their pin number:
In this case, we are expecting between 10 and 12 digits for a valid PIN number. If the user enters 12 digits, no more digits will be expected. However, a valid PIN number may have 10 digits instead of 12. When the node is waiting for the 11th digit, the user may press the hash character (#), configured as the end of input digit, so he/she does not have to wait for the timeout to complete. Validators will only be run if the input length is above or equals to the minimum expected length. Otherwise, it is considered a failed input attempt.
Also, a cancel digit is defined (the star digit, *). So the user can cancel the node. An additional behaviour is specified for the cancel digit (cancelWithInputRetriesInput()) that means that the node will be cancelled if the user presses the cancel digit without any previous input, but if there is any previous input, only the input attempt is cancelled.
For example, if after the prompt, the user immediatly presses the cancel digit, then the node is cancelled and ends. On the other hand, if the user presses something like "123*", then the node will cancel that particular input attempt and then retry it (this is useful when the user enters a wrong digit and wants to continue using that particular node, retrying the input). Each cancelled input attempt counts as a failed input attempt.
Validating input in your Telephony Application
If you have many validators to use for an input, it can be annoying to call validateInputWith() for every one of them. In this case, you can use the loadValidatorsFrom() method, like so:
Executing custom callbacks during the lifecycle of your nodes in your Telephony Application
Here's a brief example of all the callbacks available in a node:
Carrying state: Using the node internal registry to move data from one node to another
A Node has what is called a registry, where data can be stored, queried, and deleted. This is a convenient way to store data in the node so outsiders can access it, specially, a chained of validations. Let's go back to our example of calling card pin input, adding some validations:
How to unit test the IVR nodes with pure PHP Code
To unit test a node (for example, from a phpunit testcase or event without it), first, get a pagi mocked client:
Then, mock the node:
In the above test, we are configuring the mocked client to expect a node to be created by someone else, called "mainMenu". When this node is created, the mocked client will return a mocked node. When run() is called on the node ( from your application, or wherever you need/want to), the mocked node will be run with the input "123*4#".
After the node has finished, some assertions will be done (in no particular order):
- The file pp/30 has to be played exactly 1 time.
- The digits "123" will be read to the user, exactly 2 times.
- The number "321" will be read to the user, exactly 3 times.
- A datetime will be read to the user (the unix timestamp 11223344) with the format dmY, exactly 4 times.
NOTE: Be careful when asserting numbers. A strict check is used (===) to distinguish between false and 0 and null, so when asserting numbers, remember that '123' IS NOT THE SAME AS 123.
Also, there are some other assertions available:
Simulating the exact input from the user
When simulating input from the user, use the method "runWithInput()" in the mocked node, like so:
You can specify every dtmf digit available (1234567890#*). Also, if you want to simulate a timeout when entering a digit, you can use a space, for example:
This simulates the user pressing "1", then nothing, then "2", then nothing (twice), and then "3". This is useful to avoid skipping one or more prompt/pre prompt messages. For example, you can use a space to emulate the user listening to the complete error message, and then pressing an option in the prompt message. Or even 2 spaces to simulate the user listening to the pre prompt message and the prompt message without interrupting them, and then pressing a dtmf digit.
This works this way because the simulated input string is only consumed whenever an "interruptable" call happens in the pagi client. Interruptable calls are the ones that let the user press a dtmf digit to interrupt the operation, for example:
- Playing a sound (STREAM FILE).
- Playing digits (SAY DIGITS).
- Playing a number (SAY NUMBER).
- Playing a datetime (SAY DATETIME).
- Waiting for a digit (WAIT FOR DIGIT).
Interruptable operations can be made non interruptable (thus modifying the behaviour described above). When an operation is not interruptable, the input string will not be consumed by that particular operation.
Special callbacks for the mocked node
There also some specific callbacks available for the mocked node:
These callbacks are useful (for example) to add some assertions in the pagi client, when you expect something to be done from outside the node. Suppose you issue a dial() when the node has valid input, you could then do:
So when the node reaches a valid input and the valid input callback is run (and the dial is executed), everything works as expected.
So, having this node:
This can be a test:
In this way, we're using one space to let the "main-menu-prompt" finish completely, then another space to make the call to "WAIT FOR DIGIT" return timeout (no input from the user), the message "please-choose-an-option" is played completely, the next input is an invalid input, and will trigger the failed validation, playing "invalid-option" and "please-try-again" (note the 2 spaces). Then the prompt is interrupted with a valid input and the node finishes with status complete.
Writing and testing PHP Telephony Applications and IVRs for the Asterisk PBX has never been easier
Phew. First of all, thanks for keep on reading! :P I think the conclusion is somewhat clear: It's now much more easy to create ivr applications by just configuring the call flow nodes to do what you need. Lots of code should now disappear from your applications (like reading loops, error checking, manually manipulating the pagi client). Also, you can now focus on doing exactly what your telephony application requires, and nothing else.
This leads to reduced development times and less bugs introduced. The node behaviour aims to be generic, and to cover the vast majority of general use cases for standard call flow nodes. Still, it will let you "fill in the blanks", by using callbacks appropiately. This feature was very fun to write, and I hope it will be fun for you to use :)