-
Notifications
You must be signed in to change notification settings - Fork 2
Form
Pair\Html\Form is a lightweight but now much more complete form builder that manages a collection of FormControl objects (Text, Select, Checkbox, Textarea, etc.).
It helps with:
- declaring controls in PHP with fluent APIs
- applying shared classes to controls, labels and the
<form>tag - reading/validating submitted values at control level
- CSRF token generation and validation
- binding values from arrays, generic objects and
ActiveRecord - rendering the outer
<form>tag when you want a more self-contained builder
Form still works as a control registry first, so existing view-driven layouts remain valid.
When you need it, the class can also render the outer <form> tag through open(), close(), render() and the related print helpers.
Recommended usage stays the same for complex layouts: keep wrappers in the view and let Form print controls, labels and tokens.
Minimal pattern:
$form = new \Pair\Html\Form();
// Declares one control and its validation rules.
$form->text('email')->required();<form method="post" action="">
<?php // Prints the CSRF hidden field backed by $_SESSION['csrf_token']. ?>
<?php $form->printToken(); ?>
<?php // Prints the <label> element configured on the control. ?>
<?php $form->printLabel('email'); ?>
<?php // Prints the actual input HTML. ?>
<?php $form->printControl('email'); ?>
<button type="submit">Save</button>
</form>Each method creates a control, registers it in the form, and returns the control object for chaining:
address($name, $attributes = [])button($name, $attributes = [])checkbox($name, $attributes = [])color($name, $attributes = [])date($name, $attributes = [])datetime($name, $attributes = [])email($name, $attributes = [])file($name, $attributes = [])googleAddress($name, $attributes = [])hidden($name, $attributes = [])image($name, $attributes = [])meter($name, $attributes = [])month($name, $attributes = [])number($name, $attributes = [])password($name, $attributes = [])progress($name, $attributes = [])search($name, $attributes = [])select($name, $attributes = [])tel($name, $attributes = [])text($name, $attributes = [])textarea($name, $attributes = [])time($name, $attributes = [])toggle($name, $attributes = [])url($name, $attributes = [])
Example:
$form = new \Pair\Html\Form();
// Defines a required text field with a custom label.
$form->text('firstName')
->label('First name')
->required()
->maxLength(80);
// Defines a required email field with a custom CSS class.
$form->email('email')
->label('Email')
->required()
->class('text-lowercase');
// Defines a select with inline options and an empty placeholder.
$form->select('role')
// For simple CRUD lists, an associative array is the most direct source.
->options([
'admin' => 'Admin',
'editor' => 'Editor',
])
->empty('Select role');Adds a class that is injected on each rendered control. The class is applied at render time, so it works even if controls were registered earlier.
Important detail:
- the class is applied when the control is rendered through
Form(printControl(),renderControls(),render()) - if you call
$form->control('email')->render()directly, the form-level class is not injected by that call
Sets common label class used when printing labels.
This class is applied when you print the label through Form::printLabel().
Adds one or more classes to the <form> tag itself.
The implementation splits space-separated classes and avoids duplicates.
Example:
// Adds the same input class to every printed control.
$form->classForControls('form-control');
// Adds the same label class to every printed label.
$form->classForLabels('form-label');
// Adds one or more classes to the <form> tag.
$form->classForForm('stacked-form card-body');These methods are additive: they do not change how existing layouts work, but they let Form manage the outer tag when you want a more self-contained builder.
Configure the main submission attributes of the form tag.
Current constraints:
-
method(...)only acceptsgetandpost -
enctype(...)only acceptsapplication/x-www-form-urlencoded,multipart/form-dataandtext/plain
Set the main identity and browser-behavior attributes on the form tag.
Practical note:
-
target(...)accepts the standard browser targets (_blank,_self,_parent,_top) or a custom frame name without spaces
classForForm(string $class): Form / attribute(string $name, ?string $value = null): Form / attributes(array $attributes): Form
Add CSS classes and arbitrary attributes to the form tag.
attribute($name, null) creates a boolean attribute.
Enable or disable native browser validation on the form tag.
Render the form tag only, the registered controls only, or the entire form with all registered controls.
Echo the same form-level markup directly in the view.
open() automatically uses multipart/form-data when the form contains at least one File control, unless you explicitly set a different encoding.
Example:
$form = (new \Pair\Html\Form())
// Give the form a stable id so detached controls can target it.
->id('profile-form')
->action('/profile/save')
->method('post')
->classForForm('stacked-form')
// Disable native browser validation if the module prefers server-side flows.
->novalidate();
echo $form->open();
echo $form->close();Registers a pre-built custom control.
Unlike most form-level methods, add() returns the control itself so you can continue chaining on that control immediately.
Returns a control by name.
- accepts names with
[]suffix - throws
AppException(FORM_CONTROL_NOT_FOUND) if missing
Example:
// Both names resolve to the same control instance.
$tags = $form->control('tags[]');
$sameTags = $form->control('tags');Checks if a control exists in the form.
Returns all registered controls (FormControl[]).
Removes control by name and returns success flag.
Renders all registered controls in insertion order, without labels and without the CSRF token unless you explicitly registered/printed it.
Prints one control HTML.
Prints one control label (<label>), applying form-level label class if configured.
Prints a CSRF hidden control generated by generateToken().
Typical rendering pattern:
<div class="field">
<?php // Label and input come from the same registered control. ?>
<?php $form->printLabel('email'); ?>
<?php $form->printControl('email'); ?>
</div>Practical note:
-
render()returnsopen() . renderControls() . close() - it does not generate labels, wrappers or the CSRF token automatically
-
printToken()is still an explicit step
Creates (or reuses) $_SESSION['csrf_token'] and returns hidden control csrf_token.
This assumes the Pair session is already active.
Important detail:
-
generateToken()registers or overwrites the hidden control namedcsrf_tokeninside the form -
printToken()simply prints the generated hidden control
Boolean check for submitted POST token.
Behavior:
- requires
POSTrequest - requires session token + posted token
- uses
hash_equals - does not regenerate the token on success
Use checkToken() for boolean flows and validateToken() for exception-based flows.
Exception-based CSRF validation.
- throws
AppExceptionwithCSRF_TOKEN_NOT_FOUNDif session token is missing - throws
AppExceptionwithCSRF_TOKEN_INVALIDon mismatch - regenerates token after successful check
Practical note:
- if the session token exists but the posted token is missing or different, the result is the same typed error:
CSRF_TOKEN_INVALID
Controller pattern:
try {
// Stops the request with a typed exception if the token is invalid.
\Pair\Html\Form::validateToken();
// Continue with the save flow.
} catch (\Pair\Exceptions\AppException $e) {
// Stop the save flow and route the CSRF failure through your error handler.
throw $e;
}Runs validate() on each registered control and returns combined result.
Returns only controls that fail validation.
Mass-apply disabled/readonly on all registered controls.
Example:
if (!$form->isValid()) {
foreach ($form->unvalidControls() as $control) {
// Inspect the failing control before re-rendering the form.
$fieldName = $control->name;
}
}Maps array keys or object properties to controls with the same name. This is the main value-binding method now.
Accepted sources:
- associative arrays
- generic objects
-
ActiveRecordinstances
When the source is an ActiveRecord, Pair uses getAllProperties().
For generic objects, Pair uses get_object_vars(...), so it reads the object's public properties.
Maps object properties to controls with the same name.
In the current implementation it is a thin wrapper around fill(...), kept mainly for the classic ActiveRecord workflow.
- call this after all controls are defined
- only matching control names are updated
Example:
$form = new \Pair\Html\Form();
// Declare controls first so values() can match them by name.
$form->hidden('id');
$form->text('firstName');
$form->email('email');
// Copies only properties that have a matching control.
$form->values($user); // $user extends ActiveRecord$form = new \Pair\Html\Form();
$form->text('firstName');
$form->email('email');
// fill() works with arrays too, not only with ActiveRecord objects.
$form->fill([
'firstName' => 'Marta',
'email' => 'marta@example.com',
]);use Pair\Html\Form;
$form = new Form();
$form->hidden('id');
$form->text('firstName')->label('First name')->required();
$form->email('email')->label('Email')->required();
// Preloads the form when editing an existing object.
$form->values($user);
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Validates CSRF before saving.
Form::validateToken();
// Re-runs control validation on submitted values.
if ($form->isValid()) {
// Persist the object here.
}
}ActiveRecord::getForm() can auto-generate a form based on mapped properties and DB metadata.
Typical behavior:
- key fields -> hidden
-
bool-> checkbox -
DateTime-> date/datetime - numeric -> number
- enum/set -> select
- text-like DB types -> textarea
- nullable/emptiable enum-like fields ->
empty()on select
Example:
// Builds the full form automatically from object metadata.
$form = $user->getForm();
// Adds shared classes after generation.
$form->classForControls('form-control');
$form->classForLabels('form-label');Useful when you need a single control HTML without managing a full Form instance.
// Renders one standalone input without creating a full Form object.
echo \Pair\Html\Form::buildInput('email', 'john@example.com', 'email', [
// Standalone builders still accept raw HTML attributes.
'class' => 'form-control'
]);Form::buildSelect($name, $list, $valName = 'value', $textName = 'text', $value = null, $attributes = null, $prependEmpty = null)
// Renders one standalone select with a preselected value.
echo \Pair\Html\Form::buildSelect(
'status',
[
// An associative array is the most compact valid source here.
'draft' => 'Draft',
'published' => 'Published',
],
'value',
'text',
'published',
['class' => 'form-select'],
'Choose status'
);// Renders one standalone textarea.
echo \Pair\Html\Form::buildTextarea('notes', 6, 60, 'Initial notes');// Renders one standalone submit button.
echo \Pair\Html\Form::buildButton('Save', 'submit', null, ['class' => 'btn btn-primary']);use Pair\Html\Form;
$form = new Form();
// Applies shared design-system classes.
$form->classForControls('form-control');
$form->classForLabels('form-label');
// Registers the controls used by the layout.
$form->hidden('id');
$form->text('firstName')->label('First name')->required()->maxLength(120);
$form->email('email')->label('Email')->required();
$form->password('password')->label('Password')->minLength(8);
$form->toggle('active')->label('Active')->value('1');
$form->select('role')->options([
// Associative arrays map cleanly to Pair select options.
'admin' => 'Admin',
'editor' => 'Editor',
])->empty('Select role')->required();<form method="post" action="" novalidate>
<?php // Prints the CSRF token required by validateToken(). ?>
<?php $form->printToken(); ?>
<div>
<?php // First name field. ?>
<?php $form->printLabel('firstName'); ?>
<?php $form->printControl('firstName'); ?>
</div>
<div>
<?php // Email field. ?>
<?php $form->printLabel('email'); ?>
<?php $form->printControl('email'); ?>
</div>
<div>
<?php // Password field. ?>
<?php $form->printLabel('password'); ?>
<?php $form->printControl('password'); ?>
</div>
<div>
<?php // Role select field. ?>
<?php $form->printLabel('role'); ?>
<?php $form->printControl('role'); ?>
</div>
<button type="submit">Save</button>
</form>These helpers are less central than printControl(), printLabel(), values() and the CSRF methods, but they are useful in real modules:
-
add(FormControl $control): FormControlRegisters a prebuilt custom control instance. -
controls(): arrayReturns the full control map when you need to inspect or loop over the form. -
removeControl(string $name): boolUseful for role-based forms that hide one field entirely. -
allDisabled(): voidTurns the whole form into read-only display mode. -
allReadonly(): voidSimilar toallDisabled(), but keeps fields submittable. -
renderControls(): stringUseful when the wrapper<form>tag is rendered elsewhere but you still want the controls block in insertion order.
- Forgetting to print CSRF token (
printToken()) while validating token server-side. - Using CSRF helpers without an active session.
- Calling
values($object)before controls are defined. - Passing arrays of arrays to
Select::options(...)examples copied from generic HTML habits; PairSelectexpects associative arrays or object lists. - Assuming
render()also creates labels, wrappers or layout structure. It only renders the outer tag and the registered controls. - Using
control('missing')without checkingcontrolExists()(throws exception).