For the complete documentation index, see llms.txt. This page is also available as Markdown.

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.

File layout assumed throughout this page. The module unique ID is the folder name (ModuleBlackList); the controller/route slug is the kebab-case form (module-black-list).

Modules/ModuleBlackList/
└── App
    ├── Module.php
    ├── Controllers
    │   └── ModuleBlackListController.php
    ├── Forms
    │   └── ModuleBlackListForm.php
    ├── Views
    │   └── ModuleBlackList
    │       ├── index.volt
    │       └── modify.volt
    └── Providers          # usually empty — see Providers below
        ├── ViewProvider.php   # optional
        └── VoltProvider.php   # optional
public/assets/js/src/
    └── module-black-list-modify.js

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).

Two controllers or one? A module with a single page extends BaseController directly — see Extensions/EXAMPLES/WebInterface/ModuleExampleForm/App/Controllers/ModuleExampleFormController.php. A module with several pages usually adds one shared intermediate base class so all controllers inherit common initialize() logic and helper queries. See Extensions/ModuleUsersUI/App/Controllers/ModuleUsersUIBaseController.php (which extends BaseController); every page controller there — ModuleUsersUIController, AccessGroupsController, etc. — extends ModuleUsersUIBaseController. Use the pattern Module{Feature}Controller extends {Feature}BaseController extends BaseController only when you actually have shared per-controller logic to hoist.

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 .volt extension. The Modules/<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 on MikoPBX\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 in public/assets/js/src/ and Babel compiles it one directory up into public/assets/js/ — never into a cache/ folder, which is a symlink the installer creates (see JavaScript). The second argument true marks the path as local (relative to the module asset root).

AssetProvider is a core service you consume via $this->assets. You do not register it from your module. The providers you register yourself are the view and Volt providers — see Providers.

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 the Phalcon\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 = []): void

    • addCheckBox(string $fieldName, bool $checked, string $checkedValue = 'on'): void

    • addSemanticUIDropdown(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.

Checkbox vs Toggle are the same PHP element (addCheckBox). The only difference is the wrapper CSS class in the Volt view: <div class="ui checkbox"> versus <div class="ui toggle checkbox">. Both submit 'on' when checked — which is exactly why saveAction() normalises 'on' → '1'.

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. indexurl is the path the Back button returns to.

  • Tabs (<div class="ui top attached tabular menu"> + data-tab segments), 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 AJAX settings with result.data set; 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.

Compile JS sources before packaging. The cabinet only ever serves the compiled file from public/assets/js/ (as js/cache/<UniqueID>/...); the src/ file is never loaded directly.

Providers

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 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?