clue/ami-react
Async Asterisk Manager Interface (AMI) client built on ReactPHP. Connect over TCP, send actions like Originate or CoreShowChannels, and handle responses and events asynchronously via promises, with a simple event-driven API for monitoring calls and channels in real time.
Install with Composer and make sure your Asterisk manager.conf has a dedicated AMI user before writing any code:
composer require clue/ami-react
; /etc/asterisk/manager.conf
[general]
enabled = yes
port = 5038
bindaddr = 127.0.0.1
[myapp]
secret = change-me
read = all
write = all
permit = 127.0.0.1/255.255.255.255
Reload with asterisk -rx "manager reload", then start with the smallest possible listener:
<?php
require __DIR__ . '/vendor/autoload.php';
use Clue\React\Ami\Factory;
use Clue\React\Ami\Protocol\Action;
use Clue\React\Ami\Protocol\Event;
$factory = new Factory();
$factory->createClient('myapp:[email protected]:5038')->then(
function ($client) {
$client->on('event', function (Event $event) {
printf(
"%s channel=%s\n",
$event->getName(),
$event->getFieldValue('Channel') ?? '-'
);
});
// Events are off by default on a new AMI session.
$client->getEvents();
$client->action(new Action('Ping'))->then(function ($response) {
echo 'Ping response: ' . $response->getFieldValue('Response') . PHP_EOL;
});
},
function (Throwable $e) {
fwrite(STDERR, 'AMI connection failed: ' . $e->getMessage() . PHP_EOL);
}
);
Where to look first:
README.md and examples/ for the supported connection and action patterns.src/Factory.php for URL parsing, connector/TLS options, and login behavior.src/Client.php for action(), getEvents(), event emission, and close semantics.src/Protocol/Message.php for getFieldValue() / getFields(); Action, Response, and Event build on it.The first useful production use case is usually a long-running event tail: watch Newchannel, Dial, Bridge, Hangup, and Cdr to build a live call view, or subscribe to queue/agent events for wallboard data. Remember that the process stays alive while the connection and loop are active; use Ctrl-C or call $client->close() to stop it.
Wrap AMI actions behind small helper methods. The package exposes raw AMI actions, so keep protocol details out of your domain code:
use Clue\React\Ami\Client;
use Clue\React\Ami\Protocol\Action;
use React\Promise\PromiseInterface;
function amiAction(Client $client, string $name, array $fields = []): PromiseInterface
{
return $client->action(new Action($name, $fields));
}
amiAction($client, 'CoreStatus')->then(function ($response) {
echo $response->getFieldValue('CoreCurrentCalls') . PHP_EOL;
});
Treat the connection as an event bus, not a request/response database. Keep an in-memory index keyed by Uniqueid or Linkedid, update it from events, and prune it on Hangup / Cdr:
$calls = [];
$client->on('event', function (Event $event) use (&$calls) {
$name = $event->getName();
$uniqueid = $event->getFieldValue('Uniqueid');
if ($name === 'Newchannel' && $uniqueid) {
$calls[$uniqueid] = [
'channel' => $event->getFieldValue('Channel'),
'state' => $event->getFieldValue('ChannelStateDesc'),
];
}
if ($name === 'Hangup' && $uniqueid) {
unset($calls[$uniqueid]);
}
});
Filter events at the source. AMI can be extremely chatty. Use EventMask rather than receiving everything and discarding it in PHP:
$client->action(new Action('Events', [
'EventMask' => 'call,agent,queue',
]));
If you need everything, expect high event volume on busy systems and design handlers accordingly.
Reconnect explicitly. There is no built-in reconnect loop. Treat close as normal and recreate the client with backoff:
use React\EventLoop\Loop;
function connect(Factory $factory, string $url, callable $onClient): void
{
$factory->createClient($url)->then($onClient, function (Throwable $e) use (&$retry) {
// log and retry with backoff
});
// On the client:
// $client->on('close', fn () => reconnect with backoff);
}
After reconnecting, re-enable events and re-register handlers. Any pending action promises from the old connection need your own timeout/rejection handling.
Add keepalives. Idle TCP connections can be dropped by firewalls, NAT, or Asterisk restarts. A periodic Ping action gives you a cheap health check and keeps the session active:
Loop::addPeriodicTimer(30, function () use ($client) {
$client->action(new Action('Ping'))->then(null, function () {
$client->close();
});
});
Run it as a daemon, not in a web request. In Laravel, put the AMI listener in an Artisan command and run it under Supervisor. Bind the ReactPHP loop/factory in a service provider if useful, but do not open an AMI connection per HTTP request. High-volume event handlers should push work onto Laravel queues rather than writing to the database or making HTTP calls inline.
Shut down gracefully. Register SIGINT / SIGTERM, remove timers, close the client, and let the loop exit. This matters for Supervisor restarts and deploy scripts.
Test the mapping, not the socket. Extract event-to-domain mapping into pure functions and test those directly. For integration tests, run against a fake AMI server or a local Asterisk instance; do not point tests at production PBX credentials.
Events: on (or use the client's event-enabling helper). If your listener is silent, this is the first thing to check.Response: Error with a Message field rather than a transport-level failure. Inspect the response fields (Response, Message) instead of assuming a resolved promise means success.read or write permissions will connect successfully but fail individual actions or miss events. Check manager.conf, manager show user <name>, and Asterisk logs.user:pass@host:port parsing. URL-encode them or keep credentials in config and construct the URL carefully; never commit real secrets.tls://host:5039 only if Asterisk is configured for AMI TLS. Do not assume port 5038 supports TLS.try/catch and log; one malformed event should not kill your daemon.sleep() inside an event handler blocks all AMI reads and timers. Offload it to a queue or child process.Command or QueueStatus.ActionID deliberately. If the client or your wrapper assigns action IDs, keep them unique. If you generate your own, use them to correlate responses and avoid collisions across reconnects.Uniqueid, listener arrays, and caches will grow. Prune on terminal events and consider periodic worker restarts.Field: value) to leak into your code. It is not a full Asterisk domain model, so build a small adapter layer; that also isolates you from package changes.How can I help you explore Laravel packages today?