The Admin Form Builder

7 views Markdown

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

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

exact signatures
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

placeholder Hint text inside the input. A hint, not a label; the field still needs its label.
description Help text under the field. This is where a rule or a consequence belongs.
attributes A map of raw HTML attributes written onto the input: ['dir' => 'ltr', 'autocomplete' => 'off', 'required' => 'required'].
id A fixed DOM id, for JavaScript that has to find this exact field.

The Shape of Each Value

FieldWhat you passWhat arrives in the request
text, area, email, passwordthe current stringthe string, always present
number, amountthe current numbera string you cast
select['live' => 'Live', 'test' => 'Test'] plus the selected keythe chosen key
checkbox, switchthe submit value, then the current state as a booleanthe submit value, or nothing at all when unticked
multi-languagethe language list and a lang => value mapname[en], name[tr], one per language
filethe current path, for displayan 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.

the form
$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();
the operation that receives it
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

A checkbox takes seven arguments, and the middle ones are easy to confuse

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.

An unticked box sends nothing

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.

Read a secret with the pass-through filter

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.

Was this helpful?

Thanks for your feedback!

Still Need Help?

Our support team is here around the clock for anything you can't find above.