›
›
›
  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

Reading and writing values.

Read one field or all of them, write one or several, and clear a field back to its default.

The HasCustomFields trait gives a model four ways in and out. All of them run against the definitions the current tenant can see, and all of them throw on a key that is not one of those.

$customer->getCustomField('account_tier');               // one value
$customer->customFields;                                 // every value, keyed by key
$customer->setCustomField('account_tier', 'premium');    // write one
$customer->setCustomFields(['account_tier' => 'premium']); // write several

Reading one field

$customer->getCustomField('account_tier');   // 'premium'

Returns the record's value, cast to the PHP type the field promises, or the definition's default where the record has never set it.

A key the current tenant has no definition for throws UnknownCustomFieldException rather than returning null. A typo that quietly returned null would read exactly like a field nobody has filled in yet, and the two are found at very different times.

Reading every field

$customer->customFields;         // Illuminate\Support\Collection
$customer->customFields->all();
// ['account_tier' => 'premium', 'annual_revenue' => 125000]

An accessor, not a method, holding every field the current tenant can see on the model, keyed by field key and ordered by sort_order then key. Fields the record has not set appear with their defaults, so the set is complete rather than sparse.

It is read-only. There is no setter half, because writing the whole set at once is what setCustomFields() is, and that can report an unknown key.

Being a Collection, everything a collection does is available:

$customer->customFields->only(['account_tier']);
$customer->customFields->filter(fn (mixed $value): bool => $value !== null);

Writing

$customer->setCustomField('account_tier', 'premium');

Writes persist immediately. There is no save() to follow it with, and the record's own attributes are untouched: the value goes to custom_field_values, not to your table.

The method returns the model, so calls chain:

$customer
    ->setCustomField('account_tier', 'premium')
    ->setCustomField('annual_revenue', 125000);

Prefer one call when you have several fields:

$customer->setCustomFields([
    'account_tier' => 'premium',
    'annual_revenue' => 125000,
]);

Every key is resolved and every value validated before anything is written, so a call naming one unknown field, or carrying one invalid value, writes none of them rather than half of them.

Both methods throw UnsavedModelException on a record that has never been saved. Values are stored against the record's key, so there is nothing to attach one to yet:

Save the App\Models\Customer before writing custom fields to it: a value is
stored against its key.

Validation on write

By default, writing validates the value against the field's own rules first and throws Laravel's ValidationException when it fails. Only the fields being written are checked, so a required field the call never mentioned is not treated as being cleared.

Set custom-fields.validate_on_write to false to turn that off, for an application that validates every write itself with the generated rules and would rather not pay for the check twice. Type coercion still applies either way: a value the field cannot store throws InvalidValueException regardless. See validation.

Clearing a field

Writing null clears the field:

$customer->setCustomField('account_tier', null);

The value row is deleted, and the field returns to the definition's default. There is no third state where a record holds an explicit null that outranks the default.

Clearing a field whose rules include required is a validation failure, which is what required exists to stop.

Defaults

A default is resolved when you read, and never written as a value row:

CustomFields::define(Customer::class, [
    'key' => 'account_tier',
    'type' => FieldType::Select,
    'options' => ['standard', 'premium'],
    'default' => 'standard',
]);

$customer = Customer::query()->create([
    'name' => 'Ada Lovelace',
    'email' => 'ada@example.com',
]);

$customer->getCustomField('account_tier');
// 'standard', with no row in custom_field_values

Two consequences follow from that, both intended:

  • Changing a default changes what every unset record returns. Records that did set the field are untouched.
  • A default is not stored, so it is not filterable. whereCustomField() matches value rows, and a record reading its default has none. A filter for 'standard' will not return the record above.

A default is cast through the same path a stored value uses, so the two come back as the same PHP type. A default that cannot be cast, which can only happen if the field's type changed under it, is treated as no default rather than throwing, because a read is the wrong place to discover it.

The relation underneath

Reads go through a relation rather than a query:

$customer->customFieldValues;   // MorphMany<CustomFieldValue>

That is what makes with('customFieldValues') work, and it is why reading custom fields across a page of records costs one extra query rather than one per record. See queries and eager loading.

The relation is scoped to the current tenant as well as to the record, so a global field's values do not leak between tenants. setCustomFields() unsets the loaded relation after writing, so the next read reflects the new values.

Writing to CustomFieldValue directly is not supported. The model guards every attribute, and a row written around the trait skips validation, the tenant stamp, and the clearing of the other typed columns.

What to read next

  • Field types and storage for what each type accepts and returns.
  • Filtering records for finding records by a value.
  • Validation for the rules that run before a write.
PreviousTenant scopingNextValidation
View source

On this page

  1. Reading one field
  2. Reading every field
  3. Writing
  4. Validation on write
  5. Clearing a field
  6. Defaults
  7. The relation underneath
  8. What to read next