COMPONENTS FEATURE
Live field validation
Live field validation in PHP: rules are signed into the page and checked on the server as you type. The client never holds a copy of the rules.
Live field validation
platform.field is a composed component: a label, the input primitive, optional help/error slots, and an optional validation target. Its validation rules are declared once on the component, signed into an inert event manifest, evaluated on the server as you type, and the result is patched back into the DOM. The browser never holds a copy of the rules — it cannot, because they live inside a signed claim it can only pass through opaquely.
This is the Interactive HTML pillar in miniature: a live, responsive field with no bespoke endpoint and no client-trusted logic.
Declaring the field and its rules
The live preview on this page renders this exact markup:
{{ ui_page_sse_session_meta() }}{{ component('platform.field', { label: 'Username', name: 'username', placeholder: 'try empty, "ab", or 30+ characters', required: true, help: 'Validated on the server as you type.', showValidationTarget: true, rules: ['required', ['minLength', 3], ['maxLength', 20]],}) }}Rules use a small DSL: a parameterless rule is a bare string ('required'), a parametrised rule is a [name, …params] list (['minLength', 3]). They are normalised at render time and signed into the manifest's ctx claim as cfg.r — so a client cannot change the rules through the request payload (the payload guard rejects payload.rules / payload.r / payload.cfg).
Built-in rules
The default registry ships three rules; first-failure wins (the first rule whose validate() returns a result short-circuits the pipeline):
| Rule | Params | Behaviour | Failure message |
|---|---|---|---|
required |
— | rejects empty / whitespace-only | This field is required. |
minLength |
int ≥ 0 | trims, compares mb_strlen ≥ min; empty passes (pair with required) |
Please enter at least {min} characters. |
maxLength |
int ≥ 0 | compares mb_strlen ≤ max; empty passes |
Please enter no more than {max} characters. |
For honesty: a fourth built-in (sameAsField, a cross-field comparator) exists and the registry is extensible — apps add their own rules by binding a custom UiFieldRuleRegistryInterface that composes the default and resolves new names through a fixed match (never via reflection on the rule name). Custom rules are roadmap from this showcase's perspective — the demo exercises only the three built-ins.
How it works end to end
- Render — the field emits its markup plus a signed event manifest carrying a
#[UiOn(part: 'input', event: 'change')]entry whosectxincludes the signed rules. - Capture — the frontend event runtime captures the native
input.change, builds acapturedpayload, and the opt-in transport posts the minimal body{ ctx, dispatchId, payload: { value } }toPOST /__ui/dispatch. - Evaluate — the dispatcher verifies
ctx, resolvesFieldComponent::onInputChangedfrom the registry (the method name never left the server), reads the rules from the verifiedcfg.r, and runs them through the statelessUiFieldValidator. - Patch — the handler returns a
UiInteractionResult::patch([...])and the message is updated in place via asetTextpatch scoped to this component instance. The patch can only target this instance —UiPatchValidatorenforcestarget.instance === claims.i.
A simplified handler shape:
#[UiOn(part: 'input', event: 'change')]public function onInputChanged(UiInteractionEvent $event): UiInteractionResult{ $result = $this->validator->validate($event->value(), $this->rulesFrom($event->claims), $context); return UiInteractionResult::patch( patches: [ new UiResponsePatch( op: UiResponsePatch::OP_SET_TEXT, targetInstance: $event->instanceId, targetPart: null, targetName: 'validation', value: $result->message, )], );}When to use it
Reach for platform.field when you want validation that feels live but must be trustworthy — anything where a client-side rule mirror could be bypassed (uniqueness-style checks, business constraints, format rules that gate a real action). Try the demo: type nothing, two characters, then thirty-plus, and watch the message change — every verdict came from the server.
How it ties to the philosophy
Declaring rules next to the field and running them server-side keeps validation honest and tamper-proof while still feeling immediate. It is the Interactive HTML pillar's promise kept: interactivity without escaping into bespoke API glue, with the server still owning the verdict. And it rides the same single SSE session (ui_page_sse_session_meta()) and the same signed-context substrate as the rest of the UI — one architecture, not a special case bolted on for one field.
How it works
Rules are signed into the manifest ctx as cfg.r, so the client cannot change them through the payload (the guard rejects payload.rules/.r/.cfg). The frontend runtime captures input.change and posts { ctx, dispatchId, payload: { value } } to POST /__ui/dispatch; the dispatcher resolves FieldComponent::onInputChanged from the verified ctx, runs the stateless validator against the signed rules (first-failure-wins), and returns a setText patch scoped to this instance. The three built-ins — required, minLength, maxLength — are showcased; the registry is extensible (custom rules via a bound UiFieldRuleRegistryInterface) but that is roadmap here.
Why it matters
Declaring rules next to the field and running them server-side keeps validation honest and tamper-proof while still feeling immediate — the Interactive HTML pillar kept. It rides the same single SSE session and the same signed-context substrate as the rest of the UI: one architecture, not a special case bolted on for one field.