Create VoIP applications for Asterisk using PHP, PAMI, and DingTweet
Writing VoIP applications in PHP for Asterisk using PAMI
NOTE: If you are just looking for an introduction to ami and PAMI, take a look at: "Getting Started with the PAMI: PHP Asterisk Manager Interface = Easy Asterisk Monitoring".
Here, I'll write about AsterTrace (https://github.com/marcelog/AsterTrace), a
simple project that will help you get started using the asterisk manager interface in extremely few lines of code :)
The goal for AsterTrace is to log into ami and capture every event sent by asterisk, process some (or not) and maybe log every one of them to some database.
AsterTrace uses Ding and PAMI. The first one as the container, and latter to communicate with AMI, so I'll talk about how AsterTrace uses them both, and how this will help you here and in other applications to get a more decoupled code, very easy to mantain and scale.
A similar article with Ding and Doctrine2 as ORM is available in "Writing PHP applications with Doctrine2 as ORM and Ding as DI container".
Installing AsterTrace, the horsepower of your telephony application
You can get AsterTrace directly from github:
- SVN: http://svn.github.com/marcelog/AsterTrace.git
- GIT: git://github.com/marcelog/AsterTrace.git
That's it :) Now read on to see how it works.
How AsterTrace makes possible to rapidly write VoIP and Telephony Applications
To sum up, this is the general idea:
- Use Ding as Inversion of Control and Dependency Injection container. All we're going to do are just beans, plain old php objects.
- Use PAMI as the framework to handle the connection to the Asterisk Manager Interface.
- Use Ding's PamiHelper so we dont need to deal with pami use details. Our main program will be just a bean.
- Use Ding's PropertiesHolder so we can configure mysql, ami, php, etc, from properties file (aka php INI files).
- Use Ding's TCPServerHelper to open a tcp server where you can connect via telnet or via the language of your choice, to listen for events serialized using json. You can also send commands and get the responses in json format.
- Our main program (called from PamiHelper) will use Ding's events to dispatch events coming in from AMI, to our "event listeners".
- A REST interface is also provided, so this software can be used from a web environment.
- Every event listener will be just a bean, that will get called by the container whenever an event is dispatched from our main program.
- We are going to log events to a database. I chose to use mysql, via pdo. This PDO object is a bean.
- To actually write to the database, we'll use prepared statements.
- Each prepared statement will be a "bean", and will get injected to our event listeners. These beans are instantiated by ding using the factory-method and factory-bean options.
- We need to log to files the normal application stuff, like debug info, errors, etc. We'll use ding's ILoggerAware interface in our beans, giving them direct access to the logger of the container (this needs log4php, so may want to install it before proceeding).
- Handle errors through Ding error handler helper
- Handle signals through Ding signal handler helper
- Handle shutdown through Ding shutdown handler helper
Application tree layout overview. What's where.
The key here, are the xml files, which are the container configuration. This is where all the beans are defined and related to each other. Because of this configuration, our beans will be injected with everything they need to work (even a logger :))
Main entry point
The main entry point for AsterTrace is astertrace.php. So you can start it by running it:
This script will call the bootstrap.php file that resides in the same directory. Boostrap.php will:
- Setup the include path needed
- Check the invoking arguments (remember, if this is a command line application, you need one argument which is the config directory where all configuration files reside, typically, ./conf)
- If on a web environment, check the environment variable "CONFIG_DIR" to get the application's config directory.
- Setup the ding's container configuration
After this, astertrace.php will continue execution:
- The ding container will be instantiated using the configuration that bootstrap.php prepared.
- The container will use the cli.xml file as its configuration (if cli environment), or the rest.xml (if on web environment).
- Either beans file uses the PropertiesHolder to make ding load properties from php.properties, mysql.properties, astertrace.properties. So the correct values are injected in any beans that need them.
- Either beans file includes other bean configurations, for PDO, event listeners, error handler, signal handler, and shutdown handler, so all of them are available in the application.
- The bean pamiHelper is requested to the container. This bean is actually the PamiHelper that comes from ding. The container will also create the bean pami which is our own event handler.
- An infinite loop is started, calling the process() method of the PamiHelper. This method will read messages incoming from AMI.
And that is pretty much all we have to do :) the container takes care of the rest:
- Whenever an event is received by PAMI, it will call the PamiHelper.
- The PamiHelper will call our own handler.
- Our handler will dispatch an event through the container, named "anyEvent", so any beans listening for this event will execute.
- Based on the name of the event, the handler dispatches another event through the container. For example, if the event was Dial, then the "dial" event is dispatched, and the method onDial() of every listening bean (for that event) is executed.
- Make a telnet connection to the address:port configured in server.properties. You should see the events coming in from ami in json format :)
- Setup your web server so you can access rest.php. Remember to setup the environment variable CONFIG_DIR to point to the application environment. When requesting rest.php, you should start seeing events in json format coming in from ami.
Currently available listeners
- Code: src/mg/AsterTrace/EventHandlers/EventListener.php
- Bean: eventListener
- Listens-on: anyEvent
- Bean definition: conf/support/event-listeners/event.xml
- Statements: conf/support/event-listeners/event-pdo-mysql-statements.xml
- Behaviour: Log all events to the mysql. The events are saved by serializing the event object that pami delivers, which is a subclass of EventMessage. This will serialize everything about the incoming event.
- Event handler method: onAnyEvent
- Code: src/mg/AsterTrace/EventHandlers/VarSetListener.php
- Bean: varSetListener
- Listens-on: varSet
- Bean definition: conf/support/event-listeners/varset.xml
- Statements: conf/support/varset-listeners/varset-pdo-mysql-statements.xml
- Behaviour: Log all variables that are set to database. Each row will contain the uniqueid, the variable name, and the value set.
- Event handler method: onVarSet
- Code: src/mg/AsterTrace/EventHandlers/DialListener.php
- Bean: dialListener
- Listens-on: dial
- Bean definition: conf/support/event-listeners/dial.xml
- Statements: conf/support/event-listeners/dial-pdo-mysql-statements.xml
- Behaviour: Will capture Dial (SubEvent Begin), Dial (SubEvent End), Hangup, and VarSet (for the variables ANSWEREDTIME and DIALEDTIME). This will save the new call on DialBegin, and save the status of the call on Hangup. The VarSets then will update the answered and dialed times. This will effectively give you a working CDR :)
- Event handler method: onDial, onHangup, onVarSet
- Code: src/mg/AsterTrace/EventHandlers/DtmfListener.php
- Bean: dtmfListener
- Listens-on: dTMF
- Bean definition: conf/support/event-listeners/dtmf.xml
- Statements: conf/support/event-listeners/dtmf-pdo-mysql-statements.xml
- Behaviour: Will capture all dtmf events and execute an insert or update operation in its table in the following way: If the uniqueid does not exist, the uniqueid and the dtmf digit will be saved. Otherwise, the uniqueid row is updated, concatenating the new dtmf digit after the last one. This will effectively give you all the dtmf's digits pressed by every uniqueid channel.
- Event handler method: onDTMF
- Code: src/mg/AsterTrace/EventHandlers/NewChannelListener.php
- Bean: newChannelListener
- Listens-on: newchannel
- Bean definition: conf/support/event-listeners/newchannel.xml
- Statements: conf/support/event-listeners/newchannel-pdo-mysql-statements.xml
- Behaviour: Will capture all newchannel events. You can see when a channel is ringing, or reserved, etc.
- Event handler method: onNewchannel
- Code: src/mg/AsterTrace/EventHandlers/NewStateListener.php
- Bean: newStateListener
- Listens-on: newstate
- Bean definition: conf/support/event-listeners/newstate.xml
- Statements: conf/support/event-listeners/newstate-pdo-mysql-statements.xml
- Behaviour: Will capture all newstate events. You can see when a channel is ringing, up, down, etc.
- Event handler method: onNewstate
- Code: src/mg/AsterTrace/EventHandlers/NewExtenListener.php
- Bean: newExtenListener
- Listens-on: newexten
- Bean definition: conf/support/event-listeners/newexten.xml
- Statements: conf/support/event-listeners/newexten-pdo-mysql-statements.xml
- Behaviour: Will capture all newexten events, so you can see how channels go through the dialplan, step by step.
- Event handler method: onNewexten
Extending your VoIP application to listen for more PBX Events
We can add more beans to extend this functionality. Adding beans is trivial, we could use annotations, yaml, or xml configurations.
Whenever we want to get a specific event, we just need to define a listens-on directive in the bean configuration, and the container will automagically call the method onEventName() with the event received :) So we can propagate events via Radius, or ActiveMQ, or RabbitMQ, etc.
Start creating your own VoIP applications (like Operator Panels) for Asterisk
As you see, having Ding and PAMI work together is a pretty smooth task, and will benefit your code by making it cleaner and less coupled. You will be able to focus on the task you really want to do. Ding will take care of the inversion of control and dependency injection, while PAMI will take care of the managing the AMI protocol. In this way, your application is truly event driven, and everything will get called without you having to worry about how or when. Even configuring the application is trivial due to the use of the PropertiesHolder.
Everything is a bean. The PDO object, the PDO statements, and error/shutdown/signal handlers, our own event handlers, etc. This allows the application to be easily extended (just write new beans that "listens-on" different events, or event the same). Everything can be done within extremely few lines of code :)