laravel-custom-fields maintained by trappistes
Description
Laravel package for adding custom/dynamic fields to Eloquent models without schema changes
Last update
2026/05/21 11:26
(v1.0.0)
License
Downloads
0
Tags
Laravel Custom Fields
A flexible EAV (Entity-Attribute-Value) package for Laravel Eloquent models, allowing dynamic custom fields without modifying the database schema.
Features
- Dynamic Fields — Add new fields to models without migrations
- Strong Type Support — Supports int, float, bool, json, date, datetime, daterange and more
- Polymorphic Relations — One custom fields table supports any model
- Preloading Optimization — Supports Eloquent preloading to avoid N+1 queries
- Auto Type Conversion — Automatic serialization and deserialization of field values
- Batch Upsert — Efficient bulk field operations via atomic upsert
- Field Validation — Observer-based key format and reserved word validation
- Soft Deletes — Custom fields preserved on soft delete, deleted on force delete
Installation
composer require trappistes/laravel-custom-fields
Publishing Configuration
php artisan vendor:publish --provider="Trappistes\CustomFields\Providers\CustomFieldsServiceProvider"
Configuration
// config/custom-fields.php
return [
/*
|--------------------------------------------------------------------------
| Custom Field Model
|--------------------------------------------------------------------------
|
| You can override the default CustomField model to customize the primary key
| type (e.g., UUID, string) or add additional functionality.
|
*/
'model' => \Trappistes\CustomFields\Models\CustomField::class,
/*
|--------------------------------------------------------------------------
| Custom Table Name
|--------------------------------------------------------------------------
|
| Customize the table name for custom fields.
|
*/
'table_name' => 'custom_fields',
/*
|--------------------------------------------------------------------------
| JSON Maximum Depth
|--------------------------------------------------------------------------
|
| Maximum nesting depth for JSON encoding/decoding to prevent DoS attacks.
|
*/
'json_max_depth' => 64,
];
Run Migrations
php artisan migrate
Quick Start
1. Use Trait in Model
use Trappistes\CustomFields\Traits\HasCustomFields;
class User extends Model
{
use HasCustomFields;
}
2. Set Custom Fields
$user = User::find(1);
// Set single field
$user->setCustomField('nickname', 'John');
$user->setCustomField('age', 25);
$user->setCustomField('is_vip', true);
$user->setCustomField('preferences', ['theme' => 'dark', 'lang' => 'en']);
// Batch set (uses upsert for performance)
$user->setCustomFields([
'nickname' => 'John',
'age' => 25,
'is_vip' => true,
]);
3. Get Custom Fields
$user = User::find(1);
// Get single field
$nickname = $user->getCustomField('nickname');
$age = $user->getCustomField('age', 18); // with default value
// Using dynamic property
$nickname = $user->nickname;
// Check if field exists
$hasNickname = $user->hasCustomField('nickname');
// Get all custom fields
$allFields = $user->getAllCustomFields();
4. Preload Custom Fields
// Recommended: use preloading to avoid N+1 queries
$users = User::with('customFields')->get();
foreach ($users as $user) {
echo $user->getCustomField('nickname'); // no extra query
}
Supported Field Types
| Type | PHP Type | Storage Format | Example |
|---|---|---|---|
string |
string | raw string | "Hello World" |
bigint |
int | numeric string | "9223372036854775807" |
float |
float | numeric string | "3.14159" |
bool |
bool | "1" or "0" | "1" |
json |
array/object | JSON string | '{"key":"value"}' |
date |
Carbon/string | Y-m-d | "2024-01-15" |
time |
Carbon/string | H:i:s | "14:30:00" |
datetime |
Carbon/string | Y-m-d H:i:s | "2024-01-15 14:30:00" |
daterange |
CarbonPeriod/DatePeriod | JSON | '{"start":"2024-01-01","end":"2024-01-31"}' |
Types are automatically inferred:
$user->setCustomField('metadata', ['key' => 'value']); // auto inferred as json
$user->setCustomField('count', 42); // auto inferred as bigint
Field Naming Rules
- Must start with letter or underscore
- Can only contain letters, numbers and underscores
- Reserved keys:
id,model_type,model_id,field_key,field_type,field_value,created_at,updated_at,deleted_at,exists,incrementing,wasRecentlyCreated
Mass Assignment
$user = User::find(1);
// fill() method also supports custom fields
$user->fill([
'name' => 'John',
'nickname' => 'nick', // custom field
'age' => 25, // custom field
])->save();
// update() method separates regular and custom fields
$user->update([
'name' => 'Updated Name',
'department' => 'Engineering', // custom field
]);
Soft Deletes
When the main model uses soft deletes, custom fields are preserved on normal delete and deleted on force delete:
$user = User::find(1);
// Soft delete - custom fields preserved
$user->delete();
// Force delete - custom fields deleted too
$user->forceDelete();
// Restore - custom fields still available
$user->restore();
UUID / ULID Support
By default, the id primary key and model_id column use unsignedBigInteger. To support UUID or ULID:
Option 1: Use UUID
- Modify the migration to use
uuid()for the primary key andmodel_id:
// In your migration file
$table->uuid('id')->primary();
$table->uuid('model_id')->nullable();
- Create a custom model with the
HasUuidstrait:
namespace App\Models;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Trappistes\CustomFields\Models\CustomField as BaseCustomField;
class CustomField extends BaseCustomField
{
use HasUuids;
}
Option 2: Use ULID
- Modify the migration to use
ulid()for the primary key andmodel_id:
// In your migration file
$table->ulid('id')->primary();
$table->ulid('model_id')->nullable();
- Create a custom model with the
HasUlidstrait:
namespace App\Models;
use Illuminate\Database\Eloquent\Concerns\HasUlids;
use Trappistes\CustomFields\Models\CustomField as BaseCustomField;
class CustomField extends BaseCustomField
{
use HasUlids;
}
Apply the Custom Model
Update your config to use the custom model:
// config/custom-fields.php
'model' => App\Models\CustomField::class,
Database Schema
custom_fields
├── id (bigint, primary)
├── model_type (varchar, polymorphic type)
├── model_id (bigint, polymorphic ID)
├── field_key (varchar, attribute name)
├── field_type (varchar, data type)
├── field_value (text, serialized value)
├── created_at (timestamp)
├── updated_at (timestamp)
Indexes:
├── custom_fields_model_index (model_type, model_id)
├── custom_fields_key_index (field_key)
├── custom_fields_type_index (field_type)
└── custom_fields_unique_key UNIQUE (model_type, model_id, field_key)
Compatibility
| Laravel | PHP | Status |
|---|---|---|
| 9.x | 8.1+ | Supported |
| 10.x | 8.1+ | Supported |
| 11.x | 8.2+ | Supported |
| 12.x | 8.2+ | Supported |
Testing
# Run package tests with Orchestra Testbench
cd packages/laravel-custom-fields
composer install
vendor/bin/phpunit
# Run integration tests in main project
cd ../../
composer install
php artisan test
License
MIT License