-
Notifications
You must be signed in to change notification settings - Fork 2
FormControl
Pair\Html\FormControl is the shared base class behind all Pair form controls: Text, Email, Number, Select, Textarea, Checkbox, Radio, File, and the other HTML widgets exposed by the framework.
If you need to understand how Pair forms really behave, this is the page to read first. Concrete controls mostly add rendering details or stricter validation. The common fluent API, label generation, HTML attributes, and default server-side validation all live here.
It centralizes:
- control identity (
name,id,label) - common HTML attributes (
class,data-*,aria-*,placeholder,title,form) - reusable state flags (
required,disabled,readonly,autofocus, array mode) - default rendering helpers used by subclasses
- baseline server-side validation based on submitted
POSTvalues
new FormControl(string $name, array $attributes = [])Constructor behavior:
- the first argument becomes the HTML
name - if the name ends with
[], Pair removes the suffix and automatically enables array mode - custom attributes passed in
$attributesare stored and rendered later byprocessProperties()
Example:
// Pair strips [] from the internal name and remembers that this field is an array.
$tags = new \Pair\Html\FormControls\Text('tags[]', [
// Custom attributes are appended during render.
'data-role' => 'tag-input',
]);These are the methods you will use most often in day-to-day Pair code.
id(string $id): staticlabel(string $label): staticlabelClass(string $class): staticdescription(string $description): staticgetLabelText(): string
Use these methods to control how the field is presented and connected to its label.
$email = (new \Pair\Html\FormControls\Text('userEmail'))
// Use a stable DOM id when JavaScript or CSS relies on the field.
->id('profile-email')
// Uppercase labels longer than 3 chars are treated as translation keys.
->label('PROFILE_EMAIL')
// Add an explicit CSS class to the <label>.
->labelClass('form-label')
// Description adds a tooltip icon inside the label markup.
->description('Used for login notifications only.');$email = (new \Pair\Html\FormControls\Text('userEmail'))
// Leave label resolution to the base class.
->label('PROFILE_EMAIL');
// getLabelText() returns the final resolved label text.
$resolved = $email->getLabelText();value(string|int|float|DateTime|null $value): staticrequired(bool $required = true): staticdisabled(bool $disabled = true): staticreadonly(bool $readonly = true): staticarrayName(): staticautofocus(bool $autofocus = true): static
These methods define how the field behaves in HTML and, in part, how it is validated on submit.
$title = (new \Pair\Html\FormControls\Text('title'))
// Required affects both label rendering and default server-side validation.
->required()
// Preload the current value into the rendered control.
->value($record->title)
// Ask the browser to focus this field on page load.
->autofocus();$filters = (new \Pair\Html\FormControls\Text('filters'))
// Render the control as filters[] instead of filters.
->arrayName()
// Disabled fields are rendered but not editable.
->disabled();Practical note:
- the base implementation stores the value as a string; if you have a
DateTime, format it first to the exact string expected by the specific control
minLength(int $length): staticmaxLength(int $length): staticpattern(string $pattern): staticplaceholder(string $placeholder): staticinputmode(string $mode): static
These methods are especially common on text-like controls.
$slug = (new \Pair\Html\FormControls\Text('slug'))
->label('Slug')
// Pair prints minlength and maxlength on input-based controls.
->minLength(3)
->maxLength(80)
// Pattern is allowed only on a small set of text-like controls.
->pattern('^[a-z0-9-]+$')
// Placeholder is rendered only where supported.
->placeholder('my-page-slug')
// inputmode is a browser hint, not a validator.
->inputmode('text');class(string|array $class): staticdata(string $name, string $value): staticaria(string $name, string $value): statictitle(string $title): staticform(string $formId): static
These methods let you attach presentational or integration attributes without creating a custom control subclass.
$search = (new \Pair\Html\FormControls\Text('search'))
// Use an array if you want real class splitting and de-duplication.
->class(['form-control', 'js-live-search'])
// data-* attributes are useful for frontend hooks.
->data('endpoint', '/api/search/suggestions')
// aria-* helps when the control participates in richer widgets.
->aria('label', 'Search contacts')
// title is currently accepted by every control in the implementation.
->title('Type at least 3 characters');$floatingField = (new \Pair\Html\FormControls\Text('notes'))
// Associate the control with a form even if it sits outside the <form> tag.
// This pairs naturally with Form::id('order-form').
->form('order-form');render(): stringrenderLabel(): stringprint(): voidprintLabel(): void__toString(): string
Concrete controls implement render(), while FormControl provides label rendering and echo helpers.
$name = (new \Pair\Html\FormControls\Text('name'))
->id('customer-name')
->label('Customer name')
->required();
// Render label and control separately when building a custom layout.
echo $name->renderLabel();
echo $name->render();$summary = (new \Pair\Html\FormControls\Textarea('summary'))
->label('Summary')
->rows(4);
// print() is just a small echo helper.
$summary->printLabel();
$summary->print();getLabelText() follows three different paths:
- if no label is set, Pair derives it from the field name
- if the label is uppercase and longer than 3 characters, Pair treats it as a translation key and calls
Translator::do(...) - otherwise the raw label text is used as-is
Examples:
// Derived label: "User Email"
$a = new \Pair\Html\FormControls\Text('userEmail');
// Translation key: Translator::do('USER_EMAIL')
$b = (new \Pair\Html\FormControls\Text('userEmail'))
->label('USER_EMAIL');
// Literal label: "User e-mail"
$c = (new \Pair\Html\FormControls\Text('userEmail'))
->label('User e-mail');renderLabel() also adds two optional UI details:
-
<span class="required-field">...</span>when the field is required and not readonly/disabled - a tooltip icon when
description(...)has been configured
The default validate() implementation is intentionally simple and very important to understand:
- it reads the submitted value from
Post::get($this->name) - it does not validate against the current in-memory
$this->value - it checks
required,minLength, andmaxLength - it writes failures to
Logger - it returns
trueonly if all these checks pass
This means the base validator is a good baseline, not a full business validation layer.
$username = (new \Pair\Html\FormControls\Text('username'))
->required()
->minLength(3)
->maxLength(20);
// On submit, validate() checks Post::get('username'),
// not the value previously assigned with ->value(...).
$isValid = $username->validate();Concrete controls can override validate() to add stricter checks. Common examples are Email, Number, Url, and Select.
Some fluent methods are available only on specific controls.
Allowed only on:
ButtonMeterProgressTextarea
$message = (new \Pair\Html\FormControls\Textarea('message'))
// Textarea uses caption as inner text.
->caption("Initial line 1\nInitial line 2");$save = (new \Pair\Html\FormControls\Button('save'))
// Buttons can use caption as visible text.
->caption('Save');Allowed only on:
TextSearchTelEmailPasswordUrl
Calling it on unsupported controls throws an AppException.
Blocked on:
CheckboxRadioFileColorRangeHidden
Calling it on these controls throws an AppException.
Ignored without exception on:
HiddenFileImage
Ignored without exception on:
ButtonCheckboxColorDateDatetimeFileHiddenImageMonthPasswordSelectTextareaTime
These methods are useful to know when you extend controls or debug rendering:
-
nameProperty(): stringBuilds the escapedname="..."attribute and appends[]when array mode is active. -
processProperties(): stringCollects the common attributes (id,required,disabled,readonly,placeholder,pattern, classes, custom attributes). -
renderInput(string $type): stringShared helper used by simple input controls such asText,Email, andSearch. -
__get(string $name): mixedExposes internal properties and throwsAppExceptionif the property does not exist. -
__set(string $name, mixed $value): voidWrites arbitrary properties directly on the object.
Practical detail:
-
processProperties()does not print the HTMLrequiredattribute forCheckboxandRadio
$title = (new \Pair\Html\FormControls\Text('title'))
// Stable id for label, CSS and JavaScript hooks.
->id('article-title')
// Explicit label avoids relying on automatic name conversion.
->label('Title')
// Required affects both the label markup and base validation.
->required()
// Length limits are rendered on the input and checked server-side.
->minLength(3)
->maxLength(120)
// Placeholder is a hint, not a real default value.
->placeholder('Insert title')
// Use an array to add multiple CSS classes cleanly.
->class(['form-control', 'form-control-lg']);
echo $title->renderLabel();
echo $title->render();$quickNote = (new \Pair\Html\FormControls\Textarea('quickNote'))
->label('Quick note')
->rows(3)
// The control can belong to a form rendered elsewhere in the page.
->form('ticket-form')
->description('Useful when the toolbar is outside the modal form.');$form = (new \Pair\Html\Form())
// Give the real form a stable DOM id.
->id('ticket-form');
$subject = (new \Pair\Html\FormControls\Text('subject'))
->label('Subject')
// Link the detached control to the form by id.
->form('ticket-form');$tags = (new \Pair\Html\FormControls\Text('tags[]'))
// The constructor already activates array mode because of [].
->placeholder('Tag')
// data-* attributes are often used by autocomplete/tag widgets.
->data('role', 'tag-input');$notes = (new \Pair\Html\FormControls\Textarea('notes'))
// Textarea stores its content in the caption slot.
->caption('Initial notes');
$button = (new \Pair\Html\FormControls\Button('save'))
// Button caption becomes visible button text.
->caption('Save');
// This would throw an AppException because Text does not support caption().
// (new \Pair\Html\FormControls\Text('name'))->caption('Name');-
class('a b')is treated as one class string; useclass(['a', 'b'])when you want two separate classes. -
title(...)is accepted by the current implementation on every control, even though an internal unsupported list exists. -
value(...)stores a string in the base class; format dates explicitly before passing them in. -
validate()only covers generic checks. Domain rules, cross-field checks, and permission-sensitive validation still belong in the model/controller/request layer. -
__get()throwsAppExceptionwithErrorCodes::PROPERTY_NOT_FOUNDwhen you ask for a missing property.
See also: Form, Text, Select, Textarea, Checkbox, Post, Translator.