Hook on incoming call
Run custom logic on an incoming call using dialplan generation hooks.
When a call arrives from a provider, MikoPBX routes it through a per-route incoming context generated in extensions.conf. A module participates in that context by implementing dialplan-generation hooks on its configuration class (the subclass of MikoPBX\Modules\Config\ConfigClass). Each hook returns a string of dialplan lines that MikoPBX splices into the right place in the incoming context — most usefully an AGI(...) line that runs your PHP script, so you can inspect or modify the call before it is dialed to its destination.
This recipe shows how the fictional ModuleBlackList rejects blacklisted callers, and points to the real modules that ship these hooks today.
The incoming hooks and when they fire
A single incoming route produces one context. The core builds it in this order, calling each module hook in turn. The three "before dial" hooks fire before the Dial() to the route's destination; the "after dial" hook fires after.
generateIncomingRoutBeforeDialPreSystem
(string $rout_number): string
First, before MikoPBX's own system logic
generateIncomingRoutBeforeDialSystem
(string $rout_number): string
After PreSystem, still before Dial()
generateIncomingRoutBeforeDial
(string $rout_number): string
Last of the "before" group, immediately before Dial() — the common choice
generateIncomingRoutAfterDialContext
(string $uniqId): string
After the default action's Dial()/Goto() in the provider summary context
The exact ordering is visible in IncomingContexts.php:
// Core/src/Core/Asterisk/Configs/Generators/Extensions/IncomingContexts.php
$rout_data .= $this->hookModulesMethod(AsteriskConfigInterface::GENERATE_INCOMING_ROUT_BEFORE_DIAL_PRE_SYSTEM, [$rout_number]);
$rout_data .= $this->hookModulesMethod(AsteriskConfigInterface::GENERATE_INCOMING_ROUT_BEFORE_DIAL_SYSTEM, [$rout_number]);
$rout_data .= $this->hookModulesMethod(AsteriskConfigInterface::GENERATE_INCOMING_ROUT_BEFORE_DIAL, [$rout_number]);
// ... Dial() to the route destination ...
$rout_data .= $this->hookModulesMethod(AsteriskConfigInterface::GENERATE_INCOMING_ROUT_AFTER_DIAL_CONTEXT, [$uniqId]);The $rout_number argument is the DID (the dialed inbound number) of the route being generated; "" denotes the default/any route.
The three "before dial" hooks fire once per incoming route. The after-dial hook does not. It is emitted from createSummaryDialplanGoto() inside the [<provider>-incoming] summary context, and only when a default action with a non-empty extension exists — a route set to Playback or Busy never reaches it. $uniqId there is the provider id, falling back to the incoming route's uniqid (IncomingContexts::createSummaryDialplan(), lines 532-565).
Channel variables available at the "before dial" hook point
By the time the before-dial hooks run, the core has already set these channel variables in the context (see the same IncomingContexts.php):
${FROM_DID}
The dialed inbound number (same as ${EXTEN} here)
${FROM_CHAN}
The originating channel name
${M_CALLID}
The channel callid
${CALLERID(num)}
The caller's number
${CALLERID(name)}
The caller's display name
Your emitted dialplan (and any AGI script it launches) can read and modify these.
Step 1 — emit an AGI line before the call is dialed
The most common pattern is to drop a single AGI() line into the incoming context that runs a PHP script bundled with your module. The script does the work — lookup, blocking, caller-ID rewriting — and the dialplan continues afterward.
ConfigClass exposes the module's installation directory as the protected property $this->moduleDir, so you can reference your script by absolute path.
Real example — verbatim from production. ModulePhoneBook uses exactly this hook to inject a per-call AGI on incoming calls. See Extensions/ModulePhoneBook/Lib/PhoneBookConf.php:
ModuleSmartIVR does the same in Extensions/ModuleSmartIVR/Lib/SmartIVRConf.php, emitting same => n,AGI({$this->moduleDir}/agi-bin/SmartIVR_AGI.php).
Notes on the emitted line:
Use
same => n,...(relative priority) — the line is appended inside an existingexten => ...block, soncontinues from the previous priority.Always terminate the returned string with
PHP_EOL; multiple lines are simply concatenated.The second AGI argument (
in) is passed to the script as$argv[1]— handy for sharing one script between the incoming and outgoing paths.
Step 2 — write the AGI script (correct AGI API)
The script runs as a standalone PHP process that talks AGI over stdin/stdout. Instantiate the core MikoPBX\Core\Asterisk\AGI class and use its snake_case methods.
Use the real API: the class is AGI, and the methods are get_variable() / set_variable() / set_var(). There is no AgiClient class and no getVariable() camelCase method — those do not exist in MikoPBX and will fatal. Confirm signatures in Core/src/Core/Asterisk/AGI.php.
The request fields (agi_callerid, agi_extension, agi_channel, agi_uniqueid, …) are parsed at construction time and exposed on $agi->request (see AGIBase::readRequestData() in Core/src/Core/Asterisk/AGIBase.php).
set_variable() sets a channel variable (e.g. CALLERID(name)), get_variable() reads one, verbose() writes to the Asterisk console/log, hangup() ends the channel. The convenience helper getCallerIdName(string $number): string returns CALLERID(name) when it differs from the number.
Real example. Extensions/ModulePhoneBook/Lib/PhoneBookAgi.php reads $agi->request['agi_callerid'] on incoming calls and rewrites the display name with $agi->set_variable('CALLERID(name)', $result->call_id). That file is the canonical, production reference for an incoming-call AGI script.
Step 3 (optional) — route into a custom context
Instead of (or in addition to) a bare AGI() line, you can publish your own dialplan context with extensionGenContexts() and jump into it from the before-dial hook. This keeps complex per-call logic in a self-contained block and lets you reuse it from multiple routes.
ExtensionsConf::ALL_NUMBER_EXTENSION is the catch-all match pattern (a real constant in Core/src/Core/Asterisk/Configs/ExtensionsConf.php) used to accept any dialed number in a custom context.
Real example. PhoneBookConf::extensionGenContexts() publishes a [phone-book-out] context with an AGI line, and generateOutRoutContext() wires the outbound side into it — the same extensionGenContexts() + hook pattern, applied to outgoing routes.
Step 4 (optional) — act after the call ends
To run logic once the call is over (e.g. record an outcome), implement generateIncomingRoutAfterDialContext(string $uniqId). It is appended after the Dial() in the incoming context, so ${M_DIALSTATUS} (ANSWER, BUSY, NOANSWER, …) is already set.
Reloading the dialplan
Dialplan-generation hooks only take effect after extensions.conf is regenerated and Asterisk reloads it. Trigger it after enabling the module (as in onAfterModuleEnable() above), or any time your settings change:
Verify it works
Enable ModuleBlackList in the web UI (or call
PBX::dialplanReload()).Inspect the generated context:
Place an inbound test call and watch the console:
You should see your
verbose()line and, for a blacklisted number, the hangup.
See also
Last updated
Was this helpful?