Looking to hire Laravel developers? Try LaraJobs

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
Links
Downloads
0

Comments
comments powered by Disqus

Laravel Custom Fields

A flexible EAV (Entity-Attribute-Value) package for Laravel Eloquent models, allowing dynamic custom fields without modifying the database schema.

🇨🇳 中文文档 | 🇺🇸 English

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

  1. Modify the migration to use uuid() for the primary key and model_id:
// In your migration file
$table->uuid('id')->primary();
$table->uuid('model_id')->nullable();
  1. Create a custom model with the HasUuids trait:
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

  1. Modify the migration to use ulid() for the primary key and model_id:
// In your migration file
$table->ulid('id')->primary();
$table->ulid('model_id')->nullable();
  1. Create a custom model with the HasUlids trait:
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