Recipes
Capability building blocks: the recipes that compose a MikoPBX module.
A MikoPBX module is rarely a single thing. A real module is a combination of capabilities: a settings page, a background worker, a few dialplan hooks, maybe a REST resource. The recipe system is the vocabulary we use to talk about those capabilities one at a time.
A recipe is a self-contained capability. Each recipe answers three questions:
What files does it add to the module tree?
Which hooks or classes does it use from the Core?
Where is the canonical working example in the repository?
This vocabulary is shared by two consumers:
Hand-coders — you pick the recipes your module needs and copy the patterns from the linked examples.
The
/mikopbx-moduleAI skill — it generates modules by selecting and composing the same recipes. See what the skill generates.
Everything below is anchored to the running example of this guide — a fictional module called ModuleBlackList (config class BlackListConf, main class BlackListMain, model BlackListNumbers backed by table m_BlackListNumbers, front-end script module-black-list-index.js) — and to real, shipping example modules under Extensions/EXAMPLES/.
The recipe catalog
base
module.json, Setup/PbxExtensionSetup.php, ≥1 Model, Lib/{Feature}Conf.php, Messages/ru.php
Always present — the skeleton every module shares
ui
Controllers, Form, Volt views, JS, CSS
Phalcon MVC + onBeforeHeaderMenuShow()
rest-api
Lib/RestAPI/{Resource}/… (attribute-routed)
Auto-discovered v3 controllers — no manual routes
dialplan
Methods on Lib/{Feature}Conf.php
AsteriskConfigInterface hook methods
agi
agi-bin/{script}.php
MikoPBX\Core\Asterisk\AGI + a dialplan hook
workers
bin/Worker*.php
getModuleWorkers()
firewall
Methods on the Conf class
getDefaultFirewallRules(), onAfterIptablesReload(), generateFail2BanJails()
acl
Methods on the Conf class
onAfterACLPrepared(), applyACLFiltersToCDRQuery(), onGetControllerPermissions(), authenticateUser()
system
Methods on the Conf class
createCronTasks(), createNginxLocations(), onAfterPbxStarted(), …
Every method named above is a real hook. Their full signatures and timing are documented in the hooks reference. This page shows how each recipe uses them; the reference page is the exhaustive list.
Recipe: base (always included)
Every module — including ModuleBlackList — ships the base recipe. Without it there is nothing to install.
Files added:
Integration surface: the heart of the base recipe is the config class. It extends MikoPBX\Modules\Config\ConfigClass, which is abstract and itself extends AsteriskConfigClass. That single inheritance is what makes every hook in every other recipe available to your module — dialplan, workers, firewall, ACL and system hooks are all inherited from this one base class.
The config class name is the feature name plus Conf (BlackListConf), not the full module name. The module directory and module.json uniqid use the Module prefix (ModuleBlackList); the Lib class drops it. This mirrors the shipping examples — ModuleExampleForm → ExampleFormConf.
See the working example in Extensions/EXAMPLES/WebInterface/ModuleExampleForm/ — specifically:
Extensions/EXAMPLES/WebInterface/ModuleExampleForm/Setup/PbxExtensionSetup.phpExtensions/EXAMPLES/WebInterface/ModuleExampleForm/Lib/ExampleFormConf.phpExtensions/EXAMPLES/WebInterface/ModuleExampleForm/Models/ModuleExampleForm.phpExtensions/EXAMPLES/WebInterface/ModuleExampleForm/Messages/ru.php
Recipe: ui (web interface)
The ui recipe gives ModuleBlackList a settings page inside the MikoPBX admin cabinet.
Files added:
Asset filenames are per controller action, not per module: the index action loads module-black-list-index.*, a modify action would load module-black-list-modify.*. This is the convention the Core example follows (module-example-form-index.js, module-example-form-modify.js), and the controller below hardcodes those names, so they must match exactly.
How assets are registered (important)
Modules do not ship a standalone AssetProvider.php file. The Core already provides MikoPBX\AdminCabinet\Providers\AssetProvider, and you reference its collection constants from inside your controller action. This is the verified pattern from the shipping example:
A module controller extends the Core class MikoPBX\AdminCabinet\Controllers\BaseController (there is no separate "module controller" base class). AssetProvider::HEADER_CSS and AssetProvider::FOOTER_JS are the verified collection names; $moduleUniqueID is a property you declare on the controller (set to your module's uniqid) so cached assets are namespaced per module.
How the menu item is added
The sidebar entry is not a MenuProvider.php file either — it is the onBeforeHeaderMenuShow() hook on your config class. The Core calls it while building the admin menu and passes the menu array by reference:
The caption values are translation keys resolved from Messages/ru.php (and en.php).
See the working example in Extensions/EXAMPLES/WebInterface/ModuleExampleForm/:
Controller —
App/Controllers/ModuleExampleFormController.php(theindexAction()/modifyAction()asset pattern shown above is copied verbatim from here)Form —
App/Forms/ModuleExampleFormForm.phpView —
App/Views/ModuleExampleForm/index.voltJS source —
public/assets/js/src/module-example-form-index.jsMenu hook —
Lib/ExampleFormConf.php(onBeforeHeaderMenuShow())
Recipe: rest-api (REST API v3)
This recipe exposes ModuleBlackList data over HTTP. The 2025 v3 pattern is attribute-routed and auto-discovered — you write PHP 8 attributes on a controller, and ControllerDiscovery registers the routes. You never register a route manually, and there is no Conf hook for this.
Files added per resource:
The controller declares routing entirely through attributes; its method bodies are intentionally empty ({}) because the Processor and Actions do the work:
Key facts, all verified against the shipping v3 example:
#[ApiResource]marks the class auto-discoverable;ControllerDiscoveryscansLib/RestAPIfor*Controller.phpandRouterProviderregisters the routes.#[HttpMapping]maps HTTP verbs to method names and splits them into resource-level (/numbers/{id}) vs collection-level (/numbers) operations.#[ResourceSecurity]declares auth requirements withSecurityTypeenum values —SecurityType::LOCALHOST,SecurityType::BEARER_TOKEN.BaseRestControlleris the base class;$processorClasspoints at yourProcessor.
For the full attribute set, the 7-phase Action lifecycle, and DataStructure / OpenApiSchemaProvider, see REST API in modules.
See the working example in Extensions/EXAMPLES/REST-API/ModuleExampleRestAPIv3/:
Lib/RestAPI/Tasks/Controller.phpLib/RestAPI/Tasks/Processor.phpLib/RestAPI/Tasks/DataStructure.phpLib/RestAPI/Tasks/Actions/SaveRecordAction.php
Recipe: dialplan (Asterisk dialplan hooks)
The dialplan recipe adds no new files — it adds methods to Lib/BlackListConf.php. Because ConfigClass implements AsteriskConfigInterface, every method below is a real, overridable hook. Implement only the ones you need.
extensionGenContexts()
Build custom dialplan contexts
string (context text)
extensionGenInternal()
Add rules to [internal]
string
getIncludeInternal()
#include a custom context into [internal]
string
generateIncomingRoutBeforeDial(string $rout_number)
Modify an incoming route before Dial()
string
generateIncomingRoutAfterDialContext(string $uniqId)
Modify an incoming route after Dial()
string
generateOutRoutContext(array $rout)
Modify an outgoing route after EXTEN is set
string
generateOutRoutAfterDialContext(array $rout)
Modify an outgoing route after Dial()
string
extensionGlobals()
Add [globals] variables
string
extensionGenHints()
Add BLF hints
string
getFeatureMap()
Add star-codes to featuremap
string
For ModuleBlackList, the natural hook is generateIncomingRoutBeforeDial() — intercept the inbound call before it rings and route blacklisted callers into an AGI check:
These methods return dialplan text that the Core stitches into the generated extensions.conf. Return an empty string ('') when a given call does not concern your module — never null.
See working examples in real shipping modules:
Extensions/ModulePhoneBook/Lib/PhoneBookConf.php— AGI injection on incoming callsExtensions/ModuleAutoDialer/Lib/AutoDialerConf.php— multi-context dialplan generation
The verified hook signatures live in Core/src/Core/Asterisk/Configs/AsteriskConfigInterface.php.
Recipe: agi (AGI scripts)
The agi recipe pairs with the dialplan recipe: a dialplan hook calls AGI(script.php), and that script reads/writes channel variables and runs dialplan applications.
Files added:
Correct AGI API
Use the Core class MikoPBX\Core\Asterisk\AGI and its snake_case methods: get_variable(), set_variable(), exec(). Instantiate it with new AGI().
Do not write AGI\AgiClient, getVariable() or setVariable() — that class and those camelCase methods do not exist in this codebase. Every shipping AGI script (ModuleAutoDialer, ModuleQualityAssessment, ModulePhoneBook, …) uses new AGI() with snake_case methods. Verify against Core/src/Core/Asterisk/AGI.php.
The method signatures, verified in Core/src/Core/Asterisk/AGI.php:
get_variable(string $variable, bool $getvalue = false): array|string— passtrueto get the trimmed value directly.set_variable(string $variable, string $value): arrayexec(string $application, mixed $options): array
Channel request data is also available as $agi->request['agi_callerid'] (and similar agi_* keys), as used by ModuleAutoDialer.
See working examples in real shipping modules:
Extensions/ModuleQualityAssessment/agi-bin/quality_agi.phpExtensions/ModuleAutoDialer/agi-bin/get-client-info.phpExtensions/ModulePhoneBook/agi-bin/agi_phone_book.php
Recipe: workers (background processes)
The workers recipe gives ModuleBlackList a long-running background process — for example, periodically syncing the blacklist from an external feed.
Files added:
(The shipping example keeps its worker classes under Lib/; getModuleWorkers() references the worker by its fully-qualified class name, so any autoloadable location works.)
Integration surface — the getModuleWorkers() hook:
WorkerSafeScriptsCore supervises and restarts your worker. The verified type constants are:
WorkerSafeScriptsCore::CHECK_BY_BEANSTALK
checkWorkerBeanstalk
Beanstalk queue / event processing
WorkerSafeScriptsCore::CHECK_BY_AMI
checkWorkerAMI
Real-time AMI call-event tracking
WorkerSafeScriptsCore::CHECK_BY_REDIS
checkWorkerRedis
Workers that report liveness through Redis
WorkerSafeScriptsCore::CHECK_BY_PID_NOT_ALERT
checkPidNotAlert
Long-running daemons (PID monitored)
A module can register multiple workers — the shipping ExampleFormConf::getModuleWorkers() returns both a Beanstalk worker and an AMI worker.
For the worker base class, the start($argv) entry point, Beanstalk subscription and ping/restart loop, see workers.
See working examples in:
Extensions/EXAMPLES/WebInterface/ModuleExampleForm/Lib/WorkerExampleFormMain.phpExtensions/EXAMPLES/WebInterface/ModuleExampleForm/Lib/WorkerExampleFormAMI.phpExtensions/EXAMPLES/WebInterface/ModuleExampleForm/Lib/ExampleFormConf.php(thegetModuleWorkers()definition)
Recipe: firewall (firewall + fail2ban)
Adds methods to the Conf class to open ports and define fail2ban jails when the module is enabled.
getDefaultFirewallRules(), onAfterIptablesReload() and generateFail2BanJails() are all real hooks on the config class. Full timing in the hooks reference.
Recipe: acl (access control)
Adds methods to the Conf class that integrate with the MikoPBX role/permission system. All four are verified hooks:
For ModuleBlackList you would use onGetControllerPermissions() to gate the settings page behind a custom permission.
See working examples in Extensions/ModuleUsersUI/Lib/UsersUIConf.php and Extensions/ModuleUsersGroups/Lib/UsersGroupsConf.php.
Recipe: system (system integration)
Adds methods to the Conf class for cron, nginx and lifecycle integration. All verified hooks:
A typical ModuleBlackList use is a nightly cleanup via createCronTasks():
modelsEventChangeData() is how a module reacts to core configuration changes. The shipping example restarts its services when the PBX language changes — see Extensions/EXAMPLES/WebInterface/ModuleExampleForm/Lib/ExampleFormConf.php.
The recipe combination matrix
Modules are built by combining recipes. The base recipe is always present; everything else is additive. Common shapes:
Simple settings page
base + ui
A page with a few options
REST service (no UI)
base + rest-api
Headless integration endpoint
Call processing
base + ui + dialplan + agi
Caller lookup / routing (ModuleBlackList)
CRM integration
base + ui + rest-api + workers + dialplan
Bi-directional sync + screen-pop
Security module
base + ui + firewall + dialplan
Fraud / blocklist with port control
Full-featured module
base + ui + rest-api + dialplan + agi + workers + system
Everything
Worked example: ModuleBlackList
ModuleBlackList is base + ui + dialplan + agi + system:
base —
module.json,BlackListConf,BlackListNumbersmodel (m_BlackListNumbers),Messages/ru.php, Setup class.ui — a settings page to manage blacklisted numbers, menu item via
onBeforeHeaderMenuShow(), assets via the controller,module-black-list-index.js.dialplan —
generateIncomingRoutBeforeDial()injects an AGI call on inbound calls.agi —
agi-bin/module-black-list.phpchecks the caller againstBlackListNumbersusingnew AGI()and snake_case methods.system —
createCronTasks()runs nightly cleanup.
Each recipe contributed its files and its hooks independently; together they form one coherent module.
Where to go next
Hooks reference — exhaustive, verified signatures for every hook named here.
REST API in modules — the full v3 attribute/Action lifecycle.
Workers — worker base class and supervision details.
What the AI skill generates — how
/mikopbx-modulecomposes these same recipes.
Last updated
Was this helpful?