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

Introduction.

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.

RequirementSupported versions
PHP8.3, 8.4
Laravel12.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.

The three nouns

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.

What is included

  • Eleven field types, each stored in a column of its own type and read back as its native PHP type: a number as int or float, a date as a Carbon instance, a multi-select as a list of strings.
  • A trait giving any model getCustomField(), setCustomField(), setCustomFields(), a customFields accessor, and two query scopes.
  • Virtual defaults, resolved on read rather than written as rows, so changing a default changes what every unset record returns.
  • A rules array generated from the current tenant's definitions, ready to return from a form request, and the same rules run again on write.
  • Filtering that compiles to a comparison on one indexed column inside an exists clause.
  • Tenant scoping through a one-method contract, with a bundled resolver for spatie/laravel-multitenancy that configures itself when that package is installed.
  • A prune command for values whose definition has been deleted.

What it does not do

These are boundaries the package sets deliberately. Treat none of it as planned work, and none of it as ruled out forever.

  • An admin UI. The screen used to manage fields sits on top of the package. Your application decides which fields exist, what they validate, and who may write them.
  • File and relation field types. Files bring disk storage and lifecycle, relations bring referential integrity and cross-tenant leakage.
  • Dedicated event classes. The definition and value models fire Laravel's standard Eloquent events, and listeners hook those.
  • Sorting by custom field value. Filtering is supported; ordering an index page by a custom field is not.
  • A type-migration command. Changing a field's type is refused while values exist, and migrating them is a loop you write.
  • Auditing, import and export, and conditional field logic.

Design decisions

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.

What to read next

  • Installation and setup to create the package tables and pick a tenant resolver.
  • Quick start to define a field and read it back.
  • Tenant scoping for what null means and how a global field is overridden.
NextInstallation and setup
View source

On this page

  1. The three nouns
  2. What is included
  3. What it does not do
  4. Design decisions
  5. What to read next