Module interface
Building the admin-cabinet UI: controllers, forms, Volt views, providers and JS/CSS.
A module's web interface lives inside the MikoPBX admin cabinet. It is a small, self-contained Phalcon MVC application that the core mounts as a module: your controllers extend the core BaseController, your forms extend the core BaseForm, and your views are rendered by the Volt template engine using the global Fomantic UI / jQuery toolkit shared with the rest of the cabinet.
This page walks the full UI recipe through a running example module, ModuleBlackList (config class BlackListConf, main class BlackListMain, model BlackListNumbers, table m_BlackListNumbers, JS file module-black-list-index.js). For every pattern there is a pointer to a real, working module you can read and copy:
Extensions/EXAMPLES/WebInterface/ModuleExampleForm/— the canonical single-form example. Read this first.Extensions/ModuleUsersUI/— a larger production module with several controllers, custom providers and a sidebar item.
The controller
A module controller extends MikoPBX\AdminCabinet\Controllers\BaseController. The base class wires the cabinet layout, the $this->view, $this->assets, $this->request, $this->translation services, and provides the saveEntity() / deleteEntity() helpers that persist a model and return a JSON/redirect response in the format the cabinet expects.
initialize() is called by Phalcon before every action. Always call parent::initialize() last — it finalises the cabinet layout based on the properties you set (logoImagePath, submitMode).
Actions
The cabinet routes module URLs as {module-slug}/{controller-slug}/{action}. For ModuleBlackList the controller slug is module-black-list, so the working URLs are module-black-list/module-black-list/index, .../modify, .../save, .../delete.
indexAction — the landing page
The index action loads page assets and picks the index view. For a settings module the index is often just a link to the edit form; for a list module it hosts a DataTable (see Create a DataTable).
$this->view->pick('Modules/<UniqueID>/<Controller>/<view>')selects the Volt template without its.voltextension. TheModules/<UniqueID>/prefix is what makes the cabinet resolve the template inside your module directory rather than the core views.Assets are attached, not echoed, through
$this->assets->collection(...). The constants live onMikoPBX\AdminCabinet\Providers\AssetProvider:AssetProvider::HEADER_CSS— CSS injected in<head>.AssetProvider::FOOTER_JS— JS injected before</body>(this is where module scripts go).also available:
HEADER_JS,HEADER_PBX_JS,SEMANTIC_UI_CSS,SEMANTIC_UI_JS,FOOTER_PBX_JS.
Reference the compiled asset under
js/cache/<UniqueID>/.../css/cache/<UniqueID>/.... You author the source inpublic/assets/js/src/and Babel compiles it one directory up intopublic/assets/js/— never into acache/folder, which is a symlink the installer creates (see JavaScript). The second argumenttruemarks the path as local (relative to the module asset root).
modifyAction — the edit form
modifyAction() loads the cabinet form engine (js/pbx/main/form.js), instantiates your form against the model record, and picks the modify view.
The form is exposed to Volt as $this->view->form, where it becomes the form variable used by {{ form.render('field') }}. The $options array carries data the form cannot compute on its own — typically dropdown contents built from the database:
See the working version in Extensions/EXAMPLES/WebInterface/ModuleExampleForm/App/Controllers/ModuleExampleFormController.php (buildFormOptions()).
saveAction — persisting the form
saveAction() reads the POST, maps it onto the model, and delegates to saveEntity(). The critical detail is checkbox/toggle handling: an unchecked Fomantic UI checkbox is simply absent from the POST, and a checked one arrives as the string 'on'. You must normalise both to '1' / '0' before saving.
saveEntity(mixed $entity, string $reloadPath = ''): bool is provided by BaseController. It saves the model and, on an AJAX request, populates the view variables the cabinet form engine reads back ($this->view->success and, when $reloadPath is set, $this->view->reload); on validation failure it pushes the model's messages to $this->flash. It returns true/false for the save result. Iterating foreach ($record as $key => $value) over the model copies only columns that actually exist on the entity — fields posted by JS that are not columns are ignored.
deleteAction — removing a record
deleteEntity(mixed $entity, string $reloadPath = ''): bool deletes the record and, like saveEntity(), sets the AJAX reload target ($this->view->reload = $reloadPath, a {module-slug}/{controller-slug}/{action} path) so the cabinet navigates there after deletion. See deleteAction() in ModuleExampleFormController.php.
The form
A module form extends MikoPBX\AdminCabinet\Forms\BaseForm and builds its elements in initialize($entity = null, $options = null). Always call parent::initialize($entity, $options) first — it registers the cabinet-wide behaviour shared by every form. The $entity is the model passed from the controller; $options is the array you built in buildFormOptions().
BaseForm mixes two kinds of elements:
Plain Phalcon 5 elements added with
$this->add(new Text(...))—Text,Password,Numeric,Hidden,TextArea, etc., from thePhalcon\Forms\Element\*namespace.Cabinet helper methods that wrap Fomantic UI widgets so they render and behave consistently:
addTextArea(string $areaName, string $areaValue, int $areaWidth = 90, array $options = []): voidaddCheckBox(string $fieldName, bool $checked, string $checkedValue = 'on'): voidaddSemanticUIDropdown(string $name, array $options = [], $value = null, array $attributes = []): void(protected)
Every element type above is demonstrated in the canonical example Extensions/EXAMPLES/WebInterface/ModuleExampleForm/App/Forms/ModuleExampleFormForm.php.
For a deeper walkthrough of form construction and validation see Forms overview and Create a module form.
Volt views
Views are Volt templates living under App/Views/<UniqueID>/. Use the translation function t._('key') for every user-facing string (see Translations).
index.volt
For a settings module the index can be a single button to the edit form:
This mirrors the real index.volt in Extensions/EXAMPLES/WebInterface/ModuleExampleForm/App/Views/ModuleExampleForm/index.volt. For a list page that hosts a DataTable instead, follow Create a DataTable.
modify.volt
The edit view opens a <form> whose action points at your save action and whose id matches the selector your JS uses. Render each element with {{ form.render('field') }}, wrapping it in the Fomantic UI field markup. End with the shared submit-button partial.
Key points, all verified against Extensions/EXAMPLES/WebInterface/ModuleExampleForm/App/Views/ModuleExampleForm/modify.volt:
{{ form.render('field') }}outputs the element; you supply the surrounding<div class="... field">and<label>.{{ form.getValue('field') }}reads the current value of a field for display.{{ partial("partials/submitbutton", ['indexurl': '...']) }}renders the standard Save / Back button row.indexurlis the path the Back button returns to.Tabs (
<div class="ui top attached tabular menu">+data-tabsegments), accordions (<div class="ui accordion field">) and popup icons (<i class="... icon popup" data-content="...">) are plain Fomantic UI markup, initialised from JS.
JavaScript
Module JS is authored in public/assets/js/src/ as ES6 and compiled with Babel into public/assets/js/ — one directory up from the sources. The js/cache/<UniqueID>/ path the controller attaches is the served path: on install PbxExtensionUtils::createAssetsSymlinks() symlinks your public/assets/js directory to sites/admin-cabinet/assets/js/cache/<UniqueID>. You never create a cache/ directory inside your module. The cabinet exposes globals you build against: Form (the form submission/validation engine loaded via js/pbx/main/form.js), globalRootUrl, globalTranslate (your translation keys), and PbxApi (the REST client for core endpoints).
A modify script wires Fomantic UI components, then hands the form to the global Form object:
The contract with the global Form object (verified in Extensions/EXAMPLES/WebInterface/ModuleExampleForm/public/assets/js/src/module-example-form-modify.js):
Form.$formObj— the jQuery form element.Form.url— the absolute save URL (globalRootUrl+ slug path).Form.validateRules— Fomantic UI form validation rules (see the Fomantic UI form behaviour docs).Form.cbBeforeSendForm(settings)— return the AJAXsettingswithresult.dataset; the canonical pattern collects values with$formObj.form('get values').Form.cbAfterSendForm()— post-save callback.Form.initialize()— binds everything and takes over submission.
For calls to core services (status, restart, reading PBX state, etc.) use the global PbxApi client rather than hand-rolling fetches; it knows the cabinet's CSRF and endpoint conventions.
Providers
Most modules need no providers at all. Volt views resolve without any DI wiring: on install PbxExtensionUtils::createViewSymlinks() symlinks your App/Views directory into src/AdminCabinet/Views/Modules/<UniqueID>, which is exactly what $this->view->pick('Modules/<UniqueID>/...') resolves against. The canonical example Extensions/EXAMPLES/WebInterface/ModuleExampleForm/ has an empty App/Providers/ directory and registers nothing but the dispatcher — and its views render fine.
Register the two providers below only when you need to take over view resolution or extend the Volt compiler (custom Volt functions, a module-specific cache directory). Extensions/ModuleUsersUI/ does this because it adds an isAllowed Volt helper for ACL-driven rendering.
To take that control, a module registers two service providers in its App/Module.php. Each provider implements Phalcon\Di\ServiceProviderInterface.
ViewProvider
Points the view service at the module's own App/Views directory and registers the .volt engine.
This is the verbatim pattern from Extensions/ModuleUsersUI/App/Providers/ViewProvider.php.
VoltProvider
Configures the Volt compiler — cache directory, debug-mode recompilation, and any custom Volt functions your views need.
See the full production version (including the isAllowed Volt helper) in Extensions/ModuleUsersUI/App/Providers/VoltProvider.php.
Registering providers in App/Module.php
App/Module.php implements Phalcon\Mvc\ModuleDefinitionInterface. Setting the dispatcher's default namespace to your controllers is the only mandatory part; add the register() calls just if you actually wrote the providers above:
Verified against Extensions/ModuleUsersUI/App/Module.php.
There is no separate "MenuProvider" service. The sidebar/menu integration is done through the module config-class hook or the installer helper described next — not through a registered DI provider.
Sidebar and menu integration
There are two distinct mechanisms, both confirmed in real modules.
onBeforeHeaderMenuShow — runtime header menu
Your module config class (BlackListConf, which extends the core ConfigClass) may implement onBeforeHeaderMenuShow(array &$menuItems): void. The constant for this hook name is MikoPBX\Modules\Config\WebUIConfigInterface::ON_BEFORE_HEADER_MENU_SHOW. You mutate the $menuItems array by reference to add a top-menu entry with an optional submenu:
Confirmed in Extensions/EXAMPLES/WebInterface/ModuleExampleForm/Lib/ExampleFormConf.php (onBeforeHeaderMenuShow). The same hook is used in Extensions/ModuleUsersUI/Lib/UsersUIConf.php and Extensions/ModuleTemplate/Lib/TemplateConf.php.
addToSidebar — persisted left sidebar item
To place a persistent item in the left sidebar, your installer (a class extending PbxExtensionSetupBase) overrides / calls addToSidebar(): bool. The base implementation stores a PbxSettings record keyed AdditionalMenuItem<UniqueID> whose JSON value describes the entry:
This is the exact shape written by addToSidebar() in Core/src/Modules/Setup/PbxExtensionSetupBase.php. It is invoked from the module install flow; override it in your setup class to customise iconClass, caption or group.
See also
Last updated
Was this helpful?