›
byrcsc/laravel-custom-fields · 1.x
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$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.
$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);$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.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.
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.
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_valuesTwo consequences follow from that, both intended:
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.
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.