Custom field types¶
When the built-in fields are not enough, write your own field class, register it, and use it in any form like a built-in field.
This guide builds a money field: an amount with a currency in front, which
accepts "1,500,000" and stores 1500000.
1. Write the field class¶
Most fields extend InputField (one <input>) or Field:
<?php
declare(strict_types=1);
namespace App\Forms\Fields;
use Ephraitech\Forms\Config\Forms;
use Ephraitech\Forms\Contracts\SchemaConfigurableInterface;
use Ephraitech\Forms\Fields\InputField;
/**
* An amount with a currency in front, such as "UGX 1,500,000".
*/
class MoneyField extends InputField implements SchemaConfigurableInterface
{
public const TYPE = 'money';
// A view name with a path is loaded from app/Views, not from the theme.
public const VIEW = 'forms/fields/money';
protected string $currency = 'UGX';
public function currency(string $currency): static
{
$this->currency = $currency;
return $this;
}
public function getCurrency(): string
{
return $this->currency;
}
// Shows a numeric keypad on phones.
public function getAttributes(): array
{
return array_merge(['inputmode' => 'decimal'], parent::getAttributes());
}
// "1,500,000" or "1 500 000" becomes "1500000" before validation and saving.
protected function normaliseScalar(mixed $value, Forms $config): mixed
{
if (is_string($value)) {
$value = str_replace([',', ' '], '', $value);
}
return parent::normaliseScalar($value, $config);
}
// "UGX 1,500,000" when showing stored data with $form->present().
public function presentValue(mixed $value, Forms $config): array|string
{
return is_numeric($value) ? $this->currency . ' ' . number_format((float) $value) : parent::presentValue($value, $config);
}
// Lets schemas use {"type": "money", "currency": "USD"}.
public static function schemaProperties(): array
{
return ['currency' => 'currency'];
}
public function schemaValues(): array
{
return $this->currency === 'UGX' ? [] : ['currency' => $this->currency];
}
}
Each part is optional; override only what your field needs:
| Member | Purpose |
|---|---|
TYPE |
The field's type name |
VIEW |
Its template. A name with a path, like forms/fields/money, is loaded from app/Views; a plain name, like field_input, comes from the active theme |
Setters such as currency() |
Your field's own options; return static so calls can be chained |
getAttributes() |
Extra HTML attributes |
normaliseScalar() |
Cleans submitted values before validation and in validated() |
presentValue() |
How a stored value is shown by $form->present() |
schemaProperties(), schemaValues() |
Extra keys for forms stored as schemas |
2. Write its view¶
<?php
/**
* View for App\Forms\Fields\MoneyField. It receives the same variables as the
* package's own field views: $field, $id, $inputName, $label, $value, $error,
* $help, $attributes, $requiredMarker, $bare, and the $e() and $class()
* helpers.
*
* @var \App\Forms\Fields\MoneyField $field
*/
?>
<?php if (! $bare): ?>
<div<?= $class('group') ?>>
<label for="<?= $e($id) ?>"<?= $class('label') ?>><?= $e($label) ?><?= $requiredMarker ?></label>
<?php endif ?>
<div class="input-group<?= $error !== null ? ' has-validation' : '' ?>">
<span class="input-group-text"><?= $e($field->getCurrency()) ?></span>
<input type="text" name="<?= $e($inputName) ?>" id="<?= $e($id) ?>" value="<?= $e($value) ?>"<?= $class('control') ?><?= $attributes ?>>
<?php if (! $bare && $error !== null): ?>
<div id="<?= $e($errorId) ?>"<?= $class('error') ?>><?= $e($error) ?></div>
<?php endif ?>
</div>
<?php if (! $bare && $help !== null): ?>
<div id="<?= $e($helpId) ?>"<?= $class('help') ?>><?= $e($help) ?></div>
<?php endif ?>
<?php if (! $bare): ?>
</div>
<?php endif ?>
Custom views receive the same variables as the package's own field views.
Always print values with $e(), which escapes them.
3. Register it¶
Add the class to $fieldTypes in app/Config/Forms.php:
4. Use it¶
The registered name becomes a method on every form:
<?php
declare(strict_types=1);
namespace App\Forms;
use Ephraitech\Forms\Form;
class QuoteForm extends Form
{
protected function define(): void
{
$this->text('item', 'Item')->rules('required');
// money() exists because MoneyField is registered in app/Config/Forms.php.
$this->money('budget', 'Budget')
->rules('required|is_natural_no_zero')
->placeholder('1,500,000');
$this->money('deposit', 'Deposit')
->currency('USD')
->rules('permit_empty|is_natural');
$this->submit('Request a quote');
}
}
money() returns your field, so its own setters and the usual ones (rules(),
placeholder(), width()) can be chained. It also works inside rows and
fieldsets, with addField('money', 'budget'), and in schemas as
{"type": "money", "currency": "USD"}.
Submitting "1,500,000" validates against is_natural_no_zero and
validated() returns '1500000'. $form->present() shows a stored
1500000 as "UGX 1,500,000".
Common mistakes¶
Choosing a name that already exists. A name such as text or render is
refused when the form is built, because the built-in field or Form method
would always win. Pick a new name such as money or phoneNumber.
Printing values without $e(). Every value in a field view can contain
text a visitor typed. Escape it.
Using the field in admin-written schemas without allowing it. Schemas
written by admins only accept the types their policy allows. Add yours with
SchemaPolicy::restricted()->allowTypes('money').
Related¶
- Customising views for changing the built-in fields' HTML