›
›
›
  1. docs
  2. ›
  3. byrcsc/laravel-custom-fields
1.x
Browse documentationOpenClose

Getting started

  • Introduction
  • Installation and setup
  • Quick start

Core concepts

  • Defining fields
  • Field types and storage
  • Tenant scoping
  • Reading and writing values
  • Validation
  • Filtering records

Operations

  • Changing and deleting fields
  • Queries and eager loading
  • Events and listeners

Reference

  • Configuration
  • Console commands
  • Exceptions
  • Testing
  • Troubleshooting

Getting started

  • Introduction
  • Installation and setup
  • Quick start

Core concepts

  • Defining fields
  • Field types and storage
  • Tenant scoping
  • Reading and writing values
  • Validation
  • Filtering records

Operations

  • Changing and deleting fields
  • Queries and eager loading
  • Events and listeners

Reference

  • Configuration
  • Console commands
  • Exceptions
  • Testing
  • Troubleshooting

byrcsc/laravel-custom-fields · 1.x

Validation.

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.

In a form request

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

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.

ignoring

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.

Applying what passed

The generated rules validate the input; writing it is still your call:

$customer->setCustomFields($request->validated()['custom_fields'] ?? []);

On write

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.

What each type generates

Before any per-field rule, the type contributes its own:

TypeGenerated rules
textstring
textareastring
emailstring, email
urlstring, url
numbernumeric
booleanboolean
datedate
datetimedate
selectstring, Rule::in($options)
multi_selectarray, plus string and Rule::in() on each element
jsonnone

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.

Per-field rules

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],
]);
RuleSettingBecomes
requiredtrue or falserequired, or nullable when false
mina numbermin:<n>
maxa numbermax:<n>
regexa regular expression stringregex:<pattern>
uniquetrue or falseThe 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

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.

What to read next

  • Field types and storage for the difference between a type error and a validation failure.
  • Defining fields for where rules are set.
  • Exceptions for what each failure throws.
PreviousReading and writing valuesNextFiltering records
View source

On this page

  1. In a form request
  2. prefix
  3. ignoring
  4. Applying what passed
  5. On write
  6. What each type generates
  7. Per-field rules
  8. unique
  9. What to read next