trappistes / laravel-custom-fields
Laravel package for adding custom/dynamic fields to Eloquent models without schema changes
Package info
github.com/trappistes/laravel-custom-fields
Type:laravel-package
pkg:composer/trappistes/laravel-custom-fields
v1.0.0
2026-05-21 09:26 UTC
Requires
- php: ^8.1
- laravel/framework: ^9.0|^10.0|^11.0|^12.0|^13.0
Requires (Dev)
- orchestra/testbench: ^8.0|^9.0|^10.0
- phpunit/phpunit: ^9.0|^10.0|^11.0|^12.0
README
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