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

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:

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:

Models/ModuleExampleForm.php
<?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();
    }
}

Phalcon/SQLite conventions: the primary key property is untyped (public $id;), string columns are public ?string $name = '';, and even integer/boolean columns are declared as ?string and stored as the strings '0' / '1'. This matters in saveAction below, where checkbox values are normalized to '1' / '0'.

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.

Checkbox vs. Toggle. Both are created with addCheckBox() — there is no separate toggle element. The visual difference is purely a CSS class in the Volt template: <div class="ui checkbox"> versus <div class="ui toggle checkbox">.

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.

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.

saveEntity() already handles both AJAX and non-AJAX cases: on an AJAX POST it sets $this->view->success (which the global Form object reads), and on a normal POST it flashes a success message and optionally forwards to $reloadPath. You do not write a JSON response yourself.

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 id and hidden_field are rendered at the top, outside any visible layout.

  • Checkboxes use <div class="ui checkbox">; toggles use <div class="ui toggle checkbox"> — both wrap form.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).

The real example wraps these fields in a tabbed menu and an accordion. Those are optional layout niceties — the load-bearing parts are the form.render(...) calls, the matching field names, and the submitbutton partial. See the full template in Extensions/EXAMPLES/WebInterface/ModuleExampleForm/App/Views/ModuleExampleForm/modify.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 assemble settings.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:

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

  1. Render: the browser opens modifyAction, which queues form.js, builds the form from the loaded model row, and renders modify.volt.

  2. Submit: the JS calls Form.initialize(); on submit, Fomantic validates with validateRules, cbBeforeSendForm gathers values, and an AJAX POST hits /save.

  3. Persist: saveAction maps POST values onto the model (normalizing checkboxes/toggles) and calls saveEntity(), which saves and reports success back to the Form object; cbAfterSendForm then runs in the browser.

Checklist

Last updated

Was this helpful?