The Admin Form Builder
Declare a form field by field in PHP. The builder produces the markup, the layout and the field names the operation reads back.
Overview
You describe the fields; the builder writes the markup. The name you declare is the name that arrives in the request. The form and the code that receives it cannot drift apart the way hand-written markup does.
Field methods return the builder, so calls chain. Every one of them ends with an options array, and that array is where most of the useful behaviour lives.
Reference
Creating the Form
public function __construct(?string $formId, ?string $action = '', ?array $options = []);
// $formId the form's DOM id, used by JavaScript that targets this form
// $action where it posts; usually the controller address
// $options ['disableStickySubmit' => true] to drop the pinned submit bar
Field Signatures
Read the parameter order carefully. Several fields take more arguments than they appear to, and the extra ones sit in the middle rather than at the end.
public function addText($name, $label = '', $value = '', $options = []): self;
public function addEmail($name, $label, $value = '', $options = []): self;
public function addPassword($name, $label = '', $value = '', $options = []): self;
public function addNumber($name, $label = '', $value = '', $options = []): self;
public function addAmount($name, $label = '', $value = '', $options = []): self;
public function addDate($name, $label = '', $value = '', $options = []): self;
public function addColor($name, $label = '', $value = '', $options = []): self;
public function addFile($name, $label = '', $value = '', $options = []): self;
public function addArea(string $name, string $label, string $value = '', array $options = []): self;
public function addHidden(string $name, string $value = '', array $options = []): self;
// $options here is the CHOICE MAP (value => label), $fieldOptions is the field's settings.
public function addSelect(string $name, string $label, array $options = [], $selected = '', array $fieldOptions = []): self|string;
// SEVEN parameters. $value is what gets submitted when ticked; $checked is the current state.
public function addCheckbox(string $name, $groupLabel = '', $labelDesc = '', $value = '', $checked = false, array $rowOptions = [], array $options = []): self;
public function addSwitch(string $name, $groupLabel = '', $labelDesc = '', $value = '', $checked = false, array $rowOptions = [], array $options = []): self;
// One field per installed language.
public function addMultiLang(string $type, string $name, string $label, array $langList, array $values, string $currentLang = '', array $options = []): self;
// These take a single descriptor array rather than positional arguments.
public function addRadioGroup(array $options = []): self;
public function addCheckboxGroup(array $options = []): self;
// Insert relative to a field that already exists, which is how a module extends a form it does not own.
public function addFieldBefore(string $targetElementId, string $type, array $options = []): array;
public function addFieldAfter(string $targetElementId, string $type, array $options = []): array;
public function render(array $options = []): string;
What the Options Array Accepts
['dir' => 'ltr', 'autocomplete' => 'off', 'required' => 'required'].
The Shape of Each Value
| Field | What you pass | What arrives in the request |
|---|---|---|
| text, area, email, password | the current string | the string, always present |
| number, amount | the current number | a string you cast |
| select | ['live' => 'Live', 'test' => 'Test'] plus the selected key | the chosen key |
| checkbox, switch | the submit value, then the current state as a boolean | the submit value, or nothing at all when unticked |
| multi-language | the language list and a lang => value map | name[en], name[tr], one per language |
| file | the current path, for display | an upload entry, read from the files source |
Example
A module settings form, then the operation that reads it back. The two halves are shown together on purpose. The names have to match, and the reading side is where the shapes above matter.
$form = new AdminFormBuilder('acmeSettingsForm', $actionUrl, ['disableStickySubmit' => true]);
// Carried through the form so the dispatcher knows which operation to run.
$form->addHidden('operation', 'save_acme_settings');
$form->addText('api_endpoint', $lang['api-endpoint'], $config['api_endpoint'] ?? '', [
'placeholder' => 'https://api.example.com',
'description' => $lang['api-endpoint-desc'],
'attributes' => ['dir' => 'ltr', 'autocomplete' => 'off', 'spellcheck' => 'false'],
]);
$form->addPassword('api_key', $lang['api-key'], $config['api_key'] ?? '', [
'description' => $lang['api-key-desc'],
]);
// Third argument is the CHOICE MAP, fourth is the selected key.
$form->addSelect('mode', $lang['mode'], ['test' => $lang['test'], 'live' => $lang['live']], $config['mode'] ?? 'test', [
'description' => $lang['mode-desc'],
]);
// Seven parameters: name, group label, the label beside the box, the SUBMITTED value,
// the current state, row options, field options.
$form->addSwitch('log_requests', $lang['logging'], $lang['logging-enable'], '1', (int) ($config['log_requests'] ?? 0) === 1);
echo $form->render();
public function save_acme_settings(Operation $operation): bool
{
$operation->demo();
$endpoint = Filter::init("POST/api_endpoint", "hclear");
if (!$endpoint) throw new Exception(Language::gc("acme/error-endpoint-required"));
$data = [
'api_endpoint' => $endpoint,
// Pass-through: every other filter removes the characters that make a key strong.
'api_key' => Filter::init("POST/api_key", "password"),
'mode' => Filter::init("POST/mode", "letters"),
// An unticked switch sends NOTHING, so absence is the value "off".
'log_requests' => Filter::init("POST/log_requests", "rnumbers") ? 1 : 0,
];
if (!Modules::getInstance('Addons', 'Acme')->save_config($data))
throw new Exception(Language::gc("acme/error-save-failed"));
return $operation->output(['status' => "successful"]);
}
Pitfalls
The third is the label beside the box. The fourth is the value that gets submitted, and the fifth is whether it is currently ticked. Passing the current state third silently turns it into a label, and the box then never reflects the saved setting.
It is absent, not zero. Code that only writes what it receives will keep the previous setting forever, so read the absence explicitly as off.
Any text filter strips exactly the characters that make a key or a password strong. The saved value is then quietly different from what was typed. This is the one field where filtering is the bug.
Related Articles
Thanks for your feedback!
Our support team is here around the clock for anything you can't find above.