›
byrcsc/laravel-custom-fields · 1.x
Create field definitions through the CustomFields facade, which checks every attribute before it writes a row.
Definitions are readable through the CustomFieldDefinition model and writable
only through the CustomFields facade. The model guards every attribute against
mass assignment, so CustomFieldDefinition::create([...]) throws a
MassAssignmentException rather than writing a row that skipped the checks
below.
That leaves one place where key format, option lists, rule names, and duplicate keys are checked, instead of at each call site that happens to remember.
use ByRcsc\LaravelCustomFields\Enums\FieldType;
use ByRcsc\LaravelCustomFields\Facades\CustomFields;
$definition = CustomFields::define(Customer::class, [
'key' => 'account_tier',
'type' => FieldType::Select,
'options' => ['standard', 'premium'],
'label' => 'Account tier',
'description' => 'The service level assigned to this customer.',
'default' => 'standard',
'rules' => ['required' => true],
'section' => 'Account details',
'sort_order' => 10,
'tenant' => 'acme',
]);The first argument is a model class name or a model instance. Either way the definition is stored against the model's morph class, so a morph map alias is respected. A class that is not an Eloquent model throws.
Only key and type are required.
| Attribute | Type | Default | What it does |
|---|---|---|---|
key | string | required | How every later call names the field |
type | FieldType or its string | required | Which column the value is stored in and how it is cast |
label | string | built from key | The human name validation messages use |
description | string | null | Stored and returned; the package attaches no behaviour to it |
default | mixed | null | What a record that never set the field reads |
options | array<string> | null | The allowed values, for select and multi_select |
rules | array<string, mixed> | null | Per-field validation: required, min, max, regex, unique |
section | string | null | Metadata for grouping fields on a screen |
sort_order | int | 0 | Orders the definitions collection, ties broken by key |
tenant | string or null | current tenant | Which tenant owns the field |
Anything else is rejected rather than ignored:
Unknown custom field definition attribute(s): defualt. Accepted: key, type,
label, description, default, options, rules, section, sort_order, tenant.A misspelled attribute that silently did nothing would be found much later than
one that throws on the line that wrote it. The same reasoning runs through the
rest of this page: everything define() cannot honour throws
InvalidDefinitionException immediately, naming the field and saying what would
have been acceptable.
A key is lowercase, starts with a letter, and continues with lowercase letters,
digits, or underscores, up to 64 characters. account_tier, due_date, and
address_line_2 are keys; Account tier, due-date, and 2nd_line are not.
The pattern is narrow on purpose. A key appears in generated validation rule
names, in query scope arguments, and as an array key in what the customFields
accessor returns, and the excluded characters are the ones that would need
quoting or escaping in at least one of those.
label is what a validation failure calls the field. Leave it out and the
package builds one from the key by replacing underscores with spaces and
capitalising the first letter, so annual_revenue becomes Annual revenue.
A default is what a record reads when it has never set the field. It is stored as JSON on the definition and cast through the same path a stored value uses, so a default and a stored value of the same field come back as the same PHP type.
CustomFields::define(Customer::class, [
'key' => 'annual_revenue',
'type' => FieldType::Number,
'default' => 100000,
]);Defaults are virtual. They are resolved on read and never written as value rows, which has one consequence worth knowing before you rely on it: changing a definition's default changes what every unset record returns, retroactively. Records that did set the field are untouched. See reading and writing values.
select and multi_select need a non-empty list of strings, and every other
type refuses one:
CustomFields::define(Customer::class, [
'key' => 'service_interests',
'type' => FieldType::MultiSelect,
'options' => ['implementation', 'training', 'support'],
]);Duplicates in the list are dropped, keeping the first occurrence. A list holding anything that is not a non-empty string throws.
Options constrain writes, not reads. Removing an option later does not rewrite the records that already hold it, and they keep reading it back. See changing and deleting fields.
rules is a map of rule name to setting, and the five names are required,
min, max, regex, and unique:
'rules' => ['required' => true, 'min' => 1, 'max' => 120],Every name and every setting shape is checked here, so a field cannot be defined
with a rule that will never run. ['unqiue' => true] throws for the name and
['min' => '3'] throws for the setting, because a field that looks validated
and is not is worse than one nobody put rules on. What each rule generates, and
what unique does and does not guarantee, is covered in
validation.
section and sort_order are metadata. The package stores them and returns
them; grouping fields into panels and laying them out on a screen is your
application's job.
sort_order does one thing: it orders the collection
CustomFields::definitions() and the customFields accessor return, ascending,
with ties broken by key. It takes a whole number, or a numeric string, and
refuses anything else rather than coercing it. A sort_order of 'first'
quietly becoming 0 would put a field first in a list you meant to put it last
in.
CustomFields::definitions(Customer::class);Returns an Illuminate\Support\Collection of CustomFieldDefinition models
keyed by field key, holding every field the current tenant can see: its own,
plus the global ones it has not overridden. It is a plain collection rather than
an Eloquent one, so only() and except() filter by the field keys it is keyed
by.
CustomFields::definition(Customer::class, 'account_tier'); // or nullReturns one definition by key, and null when the current tenant has no such field. This is the only read path that answers null instead of throwing, which makes it the way to ask whether a field exists.
Definitions are also queryable like any other model, which is what an admin screen listing them wants:
use ByRcsc\LaravelCustomFields\Models\CustomFieldDefinition;
CustomFieldDefinition::query()
->where('model_type', (new Customer)->getMorphClass())
->whereNull('tenant')
->orderBy('sort_order')
->get();Note that a raw query is not tenant-scoped and does not apply the override rule.
CustomFields::definitions() is what answers "what does this tenant see".
There is no update() on the facade. Change a definition through its model:
$definition = CustomFields::definition(Customer::class, 'account_tier');
$definition->label = 'Customer tier';
$definition->save();Attributes are guarded, so assign them individually rather than through
update() or fill(). Values point at the definition's id rather than its key,
so renaming a field keeps every value attached to it.
Two changes carry consequences. A type change is refused outright while values
exist. A tenant change moves who can see the field, and any value written
under a tenant that can no longer see it stays in the table without being
readable through the trait. See changing and deleting
fields.
tenant and the override rule.