›
byrcsc/laravel-custom-fields · 1.x
Add typed, filterable custom fields to any Eloquent model without changing its table.
Your customers table holds the details every customer record shares: a name,
an email address, and a phone number. Then sales wants an account tier, support
wants a preferred contact time, and one tenant wants an internal reference.
Adding each one as a column means another migration, form change, and deploy. A
column one tenant needs also sits empty on every other tenant's records.
Define the account tier without changing the customers table, then use it
from the model:
use App\Models\Customer;
use ByRcsc\LaravelCustomFields\Enums\FieldType;
use ByRcsc\LaravelCustomFields\Facades\CustomFields;
CustomFields::define(Customer::class, [
'key' => 'account_tier',
'type' => FieldType::Select,
'options' => ['standard', 'premium'],
'default' => 'standard',
]);
$customer = Customer::query()->findOrFail(1);
$customer->setCustomField('account_tier', 'premium');
$customer->getCustomField('account_tier'); // 'premium'
Customer::query()->whereCustomField('account_tier', 'premium')->get();The customer now has a typed account_tier field you can validate, read, and
filter. The customers table still contains only the stable details your
application owns. A single-tenant application needs no tenant configuration.
A multi-tenant application can share global fields, override them per tenant,
and keep each tenant's values separate.
Laravel Custom Fields stores definitions and values in two tables the package owns. You define a field at runtime, records store values against that definition, and your model tables stay unchanged.
| Requirement | Supported versions |
|---|---|
| PHP | 8.3, 8.4 |
| Laravel | 12.x, 13.x |
The package follows semantic versioning. Minor and patch releases within 1.x
do not introduce breaking changes. Source and issues live at
github.com/byrcsc/laravel-custom-fields.
A definition is one field on one model class: a key, a type, an optional
default, optional validation rules, and an optional tenant. You create
definitions through the CustomFields facade, which is the only writable path,
so key format and option lists are checked in one place. They live in
custom_field_definitions, one row per field per model class per tenant.
A value is what one record holds for one field. Values live in
custom_field_values, one row per field per record per tenant, with the value
in whichever typed column its field type names. The other typed columns on that
row stay null.
A tenant is a string the package asks a resolver for. Null is a real answer and means global, so a single-tenant application runs the whole package with every row null and nothing to configure. A definition with no tenant is visible to every tenant; where a tenant defines its own field under a key a global definition already uses, the tenant's field is what reads see.
int or float, a date as a Carbon
instance, a multi-select as a list of strings.getCustomField(), setCustomField(),
setCustomFields(), a customFields accessor, and two query scopes.spatie/laravel-multitenancy that configures itself when that package is
installed.These are boundaries the package sets deliberately. Treat none of it as planned work, and none of it as ruled out forever.
Four decisions shape the rest of the package.
Typed columns, not one JSON column. Each type is stored in the column its type names, which is what lets a filter compile to a plain indexed comparison rather than a JSON path expression. A number filters as a number and a date is stored as a date. See field types and storage.
Unknown keys throw, on reads as well as writes. A typo that quietly returned null reads exactly like a field nobody has filled in yet, and the two are found at very different times.
Values outlive their definition. There is no foreign key and no cascade, so
deleting a field by mistake does not take the data with it.
custom-fields:prune is how you say the removal was
meant.
One definition of valid. The array a form request validates against and the check the trait runs before writing come from the same generator, so a value a form accepted cannot be refused by the trait. See validation.