Skip to content
Viames Marino edited this page Mar 26, 2026 · 3 revisions

Pair framework: Select

Pair\Html\FormControls\Select renders <select> controls with flat options, object-backed options, or optgroups. It is one of the most important Pair controls because it often sits at the boundary between HTML widgets and application data.

This page is worth reading carefully: Select has more implementation details and caveats than simpler controls like Text or Textarea.

Main methods

The methods you will use most often are:

  • options(array|Collection $list, ?string $propertyValue = null, ?string $propertyText = null, ?array $propertyAttributes = null): self
  • grouped(array $list): self
  • empty(?string $text = null): self
  • multiple(): self
  • value(string|int|float|DateTime|array|null $value): static
  • render(): string
  • validate(): bool

Utility method:

  • hasOptions(): bool

Inherited helpers from FormControl remain available as usual: label(), required(), disabled(), readonly(), class(), data(), id(), description(), form(), and the other shared methods.

Most-used method: options(...)

options(...) is the main entry point when a select is built from application data.

It supports two common sources.

1. Associative arrays

$status = (new \Pair\Html\FormControls\Select('status'))
    // Key becomes the <option value>, value becomes the visible label.
    ->options([
        'draft' => 'Draft',
        'published' => 'Published',
        'archived' => 'Archived',
    ]);

2. Object lists or collections

$country = (new \Pair\Html\FormControls\Select('countryId'))
    // Pair reads id as the option value and name as the visible text.
    ->options($countries, 'id', 'name')
    // Keep the existing record selected while editing.
    ->value(39);

3. Object lists with per-option attributes

$country = (new \Pair\Html\FormControls\Select('countryId'))
    // propertyAttributes adds custom attributes to every generated option.
    ->options($countries, 'id', 'name', ['iso2', 'data-region'])
    ->value(39);

4. Method call as option text

If propertyText ends with (), Pair calls that method on each object.

$assignee = (new \Pair\Html\FormControls\Select('assigneeId'))
    // displayName() is called on each object to build the visible label.
    ->options($users, 'id', 'displayName()');

Grouped selects with grouped(...)

Use grouped(...) when you want <optgroup> blocks.

Expected structure:

  • each top-level item can expose group
  • each top-level item must expose list
  • every nested option should expose value and text
  • nested options can optionally expose attributes

Example:

$select = (new \Pair\Html\FormControls\Select('departmentId'))
    ->grouped([
        (object) [
            'group' => 'Sales',
            'list' => [
                // Every option needs value and text.
                (object) ['value' => 10, 'text' => 'Inbound'],
                (object) ['value' => 11, 'text' => 'Outbound'],
            ],
        ],
        (object) [
            'group' => 'Support',
            'list' => [
                (object) ['value' => 20, 'text' => 'Level 1'],
                (object) ['value' => 21, 'text' => 'Level 2'],
            ],
        ],
    ])
    ->value(21);

Empty option with empty(...)

empty(...) prepends a blank option before the configured list.

  • if you pass no text, Pair uses Translator::do('SELECT_NULL_VALUE')
  • if the field is disabled or readonly, the inserted empty option is rendered with empty visible text

Example:

$status = (new \Pair\Html\FormControls\Select('status'))
    ->options([
        'draft' => 'Draft',
        'published' => 'Published',
    ])
    // Show a first "no selection yet" option.
    ->empty('Select status')
    ->required();

Important implementation detail:

  • render() prepends the empty option by mutating the internal list, so rendering the same instance multiple times after empty() can duplicate that first option

Multiple values with multiple()

multiple() only adds the HTML multiple attribute. For practical usage you usually combine it with:

  • a name ending in [], or arrayName()
  • value([...]) to preselect multiple items

Example:

$tags = (new \Pair\Html\FormControls\Select('tags[]'))
    // multiple() affects HTML rendering only.
    ->multiple()
    ->options($allTags, 'id', 'label')
    // Preselect multiple option values.
    ->value([3, 7, 12]);

Rendering behavior

render() is one of the main reasons to document Select separately:

  • it builds <select ...>
  • it applies the multiple attribute when enabled
  • it prepends the empty option when configured
  • it renders flat <option> items or nested <optgroup> structures
  • it marks selected options based on the current value

Simple example:

$priority = (new \Pair\Html\FormControls\Select('priority'))
    ->label('Priority')
    // A flat associative array is the most predictable validation case.
    ->options([
        'low' => 'Low',
        'normal' => 'Normal',
        'high' => 'High',
    ])
    ->value('normal')
    ->class('form-select');

echo $priority->renderLabel();
echo $priority->render();

Validation behavior

Select::validate() overrides the base validation and adds list membership checks.

For the current implementation:

  • if the field is required and the submitted value is empty, validation fails
  • if the select has options, the submitted value must match one top-level option value
  • if the field is optional, has an empty option, and the submitted value is empty, validation passes
  • if the select has no options and is not required, validation passes

Example:

$status = (new \Pair\Html\FormControls\Select('status'))
    // required() and options(...) cover the common CRUD case.
    ->required()
    ->options([
        'draft' => 'Draft',
        'published' => 'Published',
    ]);

// validate() checks Post::get('status') against the internal option list.
$isValid = $status->validate();

Important caveat on grouped and multiple selects

The current validator checks only the top-level internal list and compares it against a single submitted value.

In practice this means:

  • flat single-value selects are the safest and best-supported case
  • grouped selects need extra attention because validation does not walk nested optgroup options
  • true multi-select submissions (tags[]) are not fully validated by Select::validate() because the posted value is an array

If your form relies on grouped or multiple select values, add explicit validation in the model/request/controller layer.

Secondary methods

  • hasOptions(): bool Quick helper to check whether the internal list is populated.
  • value(...) Accepts arrays for preselection, unlike the base FormControl::value(...).
  • grouped(...) Useful when the UI needs semantic grouping, but keep the validation caveat above in mind.

Practical examples

1. Simple status selector

$status = (new \Pair\Html\FormControls\Select('status'))
    ->label('Status')
    // Keep a visible empty option before the business values.
    ->options([
        'draft' => 'Draft',
        'review' => 'In review',
        'published' => 'Published',
    ])
    ->empty('Select status')
    ->required();

2. Database-backed selector

$customer = (new \Pair\Html\FormControls\Select('customerId'))
    ->label('Customer')
    // Pair uses object properties to build value/text.
    ->options($customers, 'id', 'companyName')
    // Keep the current record selected while editing.
    ->value($order->customerId);

3. Selector with custom option metadata

$country = (new \Pair\Html\FormControls\Select('countryId'))
    ->label('Country')
    // Each option can carry extra attributes generated from the source object.
    ->options($countries, 'id', 'name', ['iso2'])
    ->value(39);

4. Multi-select rendered from an array-style field name

$tagSelect = (new \Pair\Html\FormControls\Select('tagIds[]'))
    ->label('Tags')
    // multiple() changes the HTML widget, not the validator behavior.
    ->multiple()
    ->options($tags, 'id', 'label')
    // The selected value can be preloaded as an array.
    ->value([1, 4, 9]);

Notes

  • options(...) is the most common path and the best choice for normal CRUD selectors.
  • empty() is useful for optional fields, but remember that it mutates the list during render.
  • If you need strict validation for grouped or multi-select inputs, do not rely on Select::validate() alone.

See also: FormControl, Checkbox, Toggle, Collection, Form.

Clone this wiki locally