›
byrcsc/laravel-custom-fields · 1.x
Generate a form request's rules from the current tenant's definitions, and run the same rules again on write.
A field's rules live on its definition, and two surfaces apply them: the rules array you return from a form request, and the check the trait runs before it writes. Both come from the same generator, so a value a form accepted cannot be refused by the trait, and a value the trait refuses cannot have got past the form.
use App\Models\Customer;
use ByRcsc\LaravelCustomFields\Facades\CustomFields;
use Illuminate\Foundation\Http\FormRequest;
final class StoreCustomerRequest extends FormRequest
{
public function rules(): array
{
return CustomFields::rulesFor(Customer::class, prefix: 'custom_fields');
}
public function attributes(): array
{
return CustomFields::attributeNamesFor(Customer::class, prefix: 'custom_fields');
}
}rulesFor() returns rules for every field the current tenant can see on the
model, keyed by field key. attributeNamesFor() returns each field's label,
keyed the same way, which is what the failure messages then use. Without it
Laravel falls back to the rule key, so a prefixed field reads as "The custom
fields.account_tier field is required" rather than "The Account tier field is required".
Merge the array when the request validates your own attributes too:
public function rules(): array
{
return [
'title' => ['required', 'string', 'max:255'],
...CustomFields::rulesFor(Customer::class, prefix: 'custom_fields'),
];
}prefix nests the keys for a form that posts its custom fields under one name.
A prefix of custom_fields produces custom_fields.account_tier, matching an input
named custom_fields[account_tier]. Leave it out and the keys are bare.
Whatever you choose, the same prefix belongs on attributeNamesFor(), or the
labels will not match the rules they are naming.
Pass the record being updated so a unique rule does not read its own stored
value as a duplicate of itself:
public function rules(): array
{
return CustomFields::rulesFor(
Customer::class,
prefix: 'custom_fields',
ignoring: $this->route('customer'),
);
}It matters only for fields with unique set. On a store request there is no
record yet, so leave it out.
The generated rules validate the input; writing it is still your call:
$customer->setCustomFields($request->validated()['custom_fields'] ?? []);setCustomField() and setCustomFields() validate before writing and throw
Laravel's own ValidationException on a failure. Nothing is written when a bulk
call carries one bad value.
Only the fields being written are checked. A required field the call never
mentioned is not being cleared, so its required does not fire. A field named
with a null value is checked, because clearing a required field is what
required exists to stop.
Turn it off for an application that validates every write itself:
// config/custom-fields.php
'validate_on_write' => env('CUSTOM_FIELDS_VALIDATE_ON_WRITE', true),With it off, a value that breaks a rule is stored. A value the field cannot
store at all still throws InvalidValueException, because that is the type
layer rather than the rules layer.
Before any per-field rule, the type contributes its own:
| Type | Generated rules |
|---|---|
text | string |
textarea | string |
email | string, email |
url | string, url |
number | numeric |
boolean | boolean |
date | date |
datetime | date |
select | string, Rule::in($options) |
multi_select | array, plus string and Rule::in() on each element |
json | none |
A multi-select gets a second entry keyed <field>.*, which is how Laravel
validates each element of the array.
Every field is required or nullable, and nullable is the default, so a
field with no rules accepts a null.
Set them on the definition as a map of rule name to setting:
CustomFields::define(Customer::class, [
'key' => 'annual_revenue',
'type' => FieldType::Number,
'rules' => ['required' => true, 'min' => 1, 'max' => 120],
]);| Rule | Setting | Becomes |
|---|---|---|
required | true or false | required, or nullable when false |
min | a number | min:<n> |
max | a number | max:<n> |
regex | a regular expression string | regex:<pattern> |
unique | true or false | The package's own rule, described below |
min and max are Laravel's, so they read a string's length, a number's value,
and an array's count. On a number field, 'min' => 1 means at least one, not
one digit.
Rule names and setting shapes are checked when the field is defined, not when
the rule runs. ['unqiue' => true] throws for the name, and ['min' => '3']
throws for the setting. A rule that quietly did nothing would leave a field
looking validated and not being, which is worse than one nobody put rules on.
unique means no other record of the same model type, under the same tenant,
holds this value for this field.
CustomFields::define(Customer::class, [
'key' => 'reference',
'type' => FieldType::Text,
'rules' => ['unique' => true],
]);A record rewriting its own value is not a duplicate of itself, and two different fields holding the same string are not duplicates of each other. Under a global definition, two tenants may each hold the same value, because the values are compared within one tenant.
It is advisory. The rule reads and then writes with no lock between, so two concurrent requests can both pass it and both insert. Enforcing it properly would need a partial unique index across one of six columns chosen by a value in another table, which no supported database expresses. Treat it as a check that catches the ordinary case, not as a constraint.
It is refused at definition time on multi_select and json:
The [service_interests] field is a multi_select and cannot be unique. Uniqueness compares one
column against one value, which the JSON-backed types cannot do portably: two
equal lists in different orders encode differently, and the engines disagree
about comparing JSON at all.