Configuration
A form is a tree of panels, each holding fields, built fluently. Rules are named-argument spec objects, so the IDE completes them and a typo fails at declaration time:
use DrevOps\Tui\Builder\Form;
use DrevOps\Tui\Builder\PanelBuilder;
use DrevOps\Tui\Condition\Condition;
use DrevOps\Tui\Derive\Derive;
$form = Form::create('My form')
->panel('general', 'General', function (PanelBuilder $p): void {
// text | select | suggest | confirm | toggle | number | calendar
// textarea | password | search | reorder | filepicker | pause
// select, search and filepicker collect a list with ->multiple()
$p->text('name', 'Produce name')->required();
// Compute one field from others.
$p->text('slug', 'Slug')->derive(new Derive('{{name}}', transform: 'machine'));
$p->select('category', 'Category')
->default('fruit')
->options(['fruit' => 'Fruit', 'vegetable' => 'Vegetable']);
// Shown only when the condition holds; compose with Condition::all()/any()/not().
$p->text('variety', 'Variety')->when(new Condition('category', eq: 'vegetable'));
});
Each field builder chains ->description(), ->default(), ->required(),
->weight(), ->options() / ->option() (with per-option descriptions and
optional disabled state), ->heading() / ->separator() (non-selectable
option-list structure), ->when(new Condition(...)), ->derive(new Derive(...)),
->discover(...), ->validate(...) and ->transform(...).
The form declares its own chrome: ->banner() sets a start banner and
->buttons() controls the submit/cancel buttons. The global TUI runtime -
describing the terminal, not the questionnaire - is configured on the Tui facade
instead: ->theme()
names a theme, auto-detected from the terminal background when unset (see
Themes), ->keys() sets the key bindings (see
Key bindings), ->footer() toggles the key-hint footer,
->clearOnExit() keeps or clears the final frame, ->color() / ->unicode()
force a display mode, and ->translator() presents chrome and
questions in another language (see Translations).
Derived values
A Derive computes a field from other answers: a template with {{field}}
placeholders plus a transform. A transform is any
str2name conversion (machine,
kebab, pascal, ...) plus host, lower, upper and initials - an unknown
name throws when the form is declared. Chains of derives settle to a fixpoint:
$p->text('slug', 'Slug')->derive(new Derive('{{name}}', 'machine'));
$p->text('label', 'Label')->derive(new Derive('{{category}}/{{slug}}', 'lower'));
$p->text('code', 'Code')->derive(new Derive('{{name}}', 'constant'));
Conditional fields
A ->when() rule shows or hides a field based on other answers, with operators
eq / ne / in / contains, composable with Condition::all(),
Condition::any() and Condition::not():
$p->text('heat_level', 'Heat level')->default('mild')->when(new Condition('category', eq: 'vegetable'));
$p->confirm('warning_label', 'Add a heat warning?')->when(Condition::all(new Condition('category', eq: 'vegetable'), new Condition('heat_level', ne: 'mild')));
A form-level ->fixup(new Fixup(set: ..., to: ..., when: ...)) reconciles
dependent answers on every settle pass, so an answer that no longer makes sense
after another change is corrected instead of leaking through.
Derived values, conditional fields and fix-ups each have a runnable script in
playground/06-form-logic/.