Create module form
Step-by-step: build a settings form for your module.
This recipe walks through building a complete, persistable settings form for a MikoPBX module: a database model, a Phalcon form class, a controller that renders and saves the form, a Volt view, and a babel-compiled JavaScript controller that wires up validation and AJAX submission.
The narrative uses a fictional module, ModuleBlackList (config class BlackListConf, main class BlackListMain, model BlackListNumbers, table m_BlackListNumbers, JS file module-black-list-modify.js — assets are named per action, not per module). Every pattern is anchored to the real, working example module that ships with MikoPBX:
Verbatim anchor: Extensions/EXAMPLES/WebInterface/ModuleExampleForm/. The example module exercises every form element type covered here. When in doubt, read its files — they are the source of truth this page is built from.
Before you start, you should already have a module skeleton (see module-interface-empty.md) and understand how MikoPBX models map to SQLite tables (see data-model.md). For the full list of form recipes, see the forms cookbook README.
The five pieces
A settings form is made of five files that all reference each other through shared field names. Keeping the names identical across all layers is the single most important rule:
Layer
File (in ModuleExampleForm)
Responsibility
Model
Models/ModuleExampleForm.php
Defines columns with @Column, maps to a m_* table.
Form
App/Forms/ModuleExampleFormForm.php
Declares form elements whose names match the model columns.
Controller
App/Controllers/ModuleExampleFormController.php
modifyAction builds the form, saveAction persists it.
View
App/Views/ModuleExampleForm/modify.volt
Renders each element with form.render('field').
JavaScript
public/assets/js/src/module-example-form-modify.js
Validation rules + AJAX submit via the global Form object.
Step 1 — Define the model
The model declares one property per column. Each property carries a Phalcon @Column annotation, the primary key is untyped, and the table name is set in initialize() via setSource(). Module models extend ModulesModelsBase.
For ModuleBlackList the model would be BlackListNumbers mapping to m_BlackListNumbers. The example module uses generic field names so it can demonstrate every element type:
<?php
declare(strict_types=1);
namespace Modules\ModuleExampleForm\Models;
use MikoPBX\Common\Models\Providers;
use MikoPBX\Modules\Models\ModulesModelsBase;
use Phalcon\Mvc\Model\Relation;
class ModuleExampleForm extends ModulesModelsBase
{
/**
* @Primary
* @Identity
* @Column(type="integer", nullable=false)
*/
public $id;
/**
* @Column(type="string", nullable=true)
*/
public ?string $text_field = '';
/**
* @Column(type="string", nullable=true)
*/
public ?string $text_area_field = '';
/**
* @Column(type="string", nullable=true)
*/
public ?string $password_field = '';
/**
* @Column(type="integer", default="3", nullable=true)
*/
public ?string $integer_field = '3';
/**
* @Column(type="integer", default="1", nullable=true)
*/
public ?string $checkbox_field = '1';
/**
* @Column(type="integer", default="1", nullable=true)
*/
public ?string $toggle_field = '1';
/**
* @Column(type="string", nullable=true)
*/
public ?string $select_field = 'medium';
/**
* @Column(type="string", nullable=true)
*/
public ?string $provider_field = '';
/**
* @Column(type="string", nullable=true)
*/
public ?string $hidden_field = '';
public function initialize(): void
{
$this->setSource('m_ModuleExampleForm');
parent::initialize();
}
}The full example model also declares a hasOne relation to Providers and an optional getDynamicRelations() block — see the complete file in Extensions/EXAMPLES/WebInterface/ModuleExampleForm/Models/ModuleExampleForm.php and the model recipe in data-model.md.
Step 2 — Build the form class
The form extends MikoPBX\AdminCabinet\Forms\BaseForm and overrides initialize($entity = null, $options = null). Always call parent::initialize($entity, $options) first.
BaseForm::initialize() (Core/src/AdminCabinet/Forms/BaseForm.php:37-46) is six lines long and does exactly two things: it substitutes a stdClass when $entity is null (so $entity->field never fatals on a fresh record), and it fires the WebUIConfigInterface::ON_BEFORE_FORM_INITIALIZE hook across every enabled module via PBXConfModulesProvider::hookModulesMethod(). It does not set up a CSRF token or any other shared configuration.
That second point is the reason the call is mandatory rather than merely polite: your parent::initialize() call is the only thing that lets other modules extend your form (see add a field to an existing form). Skip it and the extension point silently disappears.
BaseForm provides three convenience helpers on top of the plain Phalcon elements:
Helper
Signature (from Core/src/AdminCabinet/Forms/BaseForm.php)
addTextArea
addTextArea(string $areaName, string $areaValue, int $areaWidth = 90, array $options = []): void
addCheckBox
addCheckBox(string $fieldName, bool $checked, string $checkedValue = 'on'): void
addSemanticUIDropdown
addSemanticUIDropdown(string $name, array $options = [], $value = null, array $attributes = []): void
Plain Phalcon elements — Text, Password, Numeric, Hidden — are added with the standard $this->add(new ...). Each element name must equal a model column.
The static-vs-DB dropdown distinction is just the contents of the $options array the form receives. Both calls are identical; only the supplied option list differs. The full annotated form is in Extensions/EXAMPLES/WebInterface/ModuleExampleForm/App/Forms/ModuleExampleFormForm.php.
Step 3 — Wire up the controller
The controller extends MikoPBX\AdminCabinet\Controllers\BaseController. Three methods matter for a form: initialize(), modifyAction() (render) and saveAction() (persist). BaseController supplies the protected helpers saveEntity(mixed $entity, string $reloadPath = ''): bool and deleteEntity(mixed $entity, string $reloadPath = ''): bool.
modifyAction — render the form
modifyAction queues the required JavaScript assets, loads (or creates) the settings record, builds the dropdown option lists, and assigns the form to the view.
Always queue js/pbx/main/form.js first, then your compiled module script. form.js defines the global Form object your JS depends on (Step 5). Asset collection constants live on MikoPBX\AdminCabinet\Providers\AssetProvider (HEADER_CSS, FOOTER_JS).
saveAction — persist the posted data
saveAction reads the POST, loads the record by id (or creates one), then iterates the model's own properties and copies matching POST values onto them. This loop is the recommended pattern: it only touches real columns and handles the checkbox/toggle special case in one place.
Unchecked checkboxes and toggles are not posted by the browser at all. You must default them explicitly, otherwise a turned-off toggle would keep its old value. The loop below sets '1' when the field arrives as 'on', and '0' when the key is absent.
The complete controller (including indexAction) is in Extensions/EXAMPLES/WebInterface/ModuleExampleForm/App/Controllers/ModuleExampleFormController.php.
Step 4 — Render the Volt view
modify.volt is a plain <form> whose action points at the save route. Each field is emitted with {{ form.render('field_name') }}, wrapped in Fomantic UI markup, with labels translated via {{ t._('key') }}. The submit button is a shared partial.
Key rendering rules:
The hidden
idandhidden_fieldare rendered at the top, outside any visible layout.Checkboxes use
<div class="ui checkbox">; toggles use<div class="ui toggle checkbox">— both wrapform.render(...)plus a<label>.Close with
partial("partials/submitbutton", ['indexurl': '<index route>']), which renders the standard Save / Back buttons used across the admin cabinet (Core/src/AdminCabinet/Views/partials/submitbutton.volt).
Step 5 — JavaScript: validation and AJAX submit
The client script uses the global Form object (provided by js/pbx/main/form.js). You configure it, then call Form.initialize(). The standard contract is:
Form.$formObj— jQuery handle of your<form>.Form.url— absolute save URL (globalRootUrl+ your save route).Form.validateRules— Fomantic UI form-validation rule set.Form.cbBeforeSendForm(settings)— return the (possibly modified) AJAX settings; this is where you assemblesettings.data.Form.cbAfterSendForm()— runs after a successful save.
For ModuleBlackList this file would be module-black-list-modify.js (the list page gets its own module-black-list-index.js); the example uses module-example-form-modify.js:
Compile the source. Files under public/assets/js/src/ are written in modern ES and must be babel-compiled into public/assets/js/ — the parent directory, not a cache subfolder. The js/cache/<moduleUniqueID> path you see in modifyAction is a symlink the installer points straight at <moduleDir>/public/assets/js (PbxExtensionUtils::createAssetsSymlinks(), Core/src/Modules/PbxExtensionUtils.php:105-114), so the compiled file lands exactly where the URL resolves. MikoPBX never loads the src/ file directly. Recompile after every edit.
The full client controller (with TextArea/Password rules, accordion, tab, and popup initialization) is in Extensions/EXAMPLES/WebInterface/ModuleExampleForm/public/assets/js/src/module-example-form-modify.js.
How the pieces connect
Render: the browser opens
modifyAction, which queuesform.js, builds the form from the loaded model row, and rendersmodify.volt.Submit: the JS calls
Form.initialize(); on submit, Fomantic validates withvalidateRules,cbBeforeSendFormgathers values, and an AJAX POST hits/save.Persist:
saveActionmaps POST values onto the model (normalizing checkboxes/toggles) and callssaveEntity(), which saves and reports success back to theFormobject;cbAfterSendFormthen runs in the browser.
Checklist
Related pages
Last updated
Was this helpful?