Field behaviour
Behaviour beyond a static value is declared on the field itself - a dynamic default, validation and a value transform as closures, right in the form:
use DrevOps\Tui\Handler\Context;
$p->text('name', 'Project name')
->default(fn(Context $c): string => basename($c->directory))
->validate(fn(mixed $v): ?string => is_string($v) && trim($v) !== '' ? NULL : 'A name is required.')
->transform(fn(mixed $v): mixed => is_string($v) ? trim($v) : $v);
Reusable validators and transformers live as public static methods on a consumer
class. Reference one explicitly with a first-class callable -
->validate(Webroot::validate(...)) - or let the engine discover it: registering
a namespace (new Tui($form, ['App\\Handler'])) resolves the class by field id
(machine_name -> MachineName) and uses its static validate()/transform()
whenever the field declares none. The field declaration always wins.
The TUI only collects: it presents answers and never applies them. Applying
answers - writing files, renaming directories - is the consumer's job. A
consumer that processes answers defines its own processor interface, keeping the
form for collection and the processors for side effects - one class per field can
carry both its process() and its reusable static behaviour (this is exactly what
the Vortex CLI does).