trappistes/laravel-custom-fields
Flexible EAV custom fields for Laravel Eloquent: add dynamic attributes without schema changes. Supports strong types (int, bool, json, date, ranges), polymorphic relations for any model, eager loading to avoid N+1, auto casting, batch upsert, validation, and soft deletes.
Installation
composer require trappistes/laravel-custom-fields
php artisan vendor:publish --provider="Trappistes\CustomFields\Providers\CustomFieldsServiceProvider"
php artisan migrate
Enable on Model Add the trait to any Eloquent model:
use Trappistes\CustomFields\Traits\HasCustomFields;
class User extends Model
{
use HasCustomFields;
}
First Use Case
$user = User::find(1);
$user->setCustomField('premium_until', now()->addYear());
$expiryDate = $user->getCustomField('premium_until');
config/custom-fields.php for table/key overrides.database/migrations/[timestamp]_create_custom_fields_table.php for schema details.Dynamic Field Management
// Single field
$user->setCustomField('subscription_plan', 'pro');
// Batch upsert (atomic)
$user->setCustomFields([
'last_login' => now(),
'notifications' => ['email', 'sms'],
]);
Preloading for Performance
// Avoid N+1 queries
$users = User::with('customFields')->get();
foreach ($users as $user) {
$user->getCustomField('preferences'); // No extra query
}
Type-Safe Access
// Auto-inferred types
$user->setCustomField('age', 30); // Stored as `bigint`
$user->setCustomField('settings', ['theme' => 'dark']); // Stored as `json`
// Explicit casting
$user->setCustomField('expiry', now(), 'datetime');
Mass Assignment Integration
// Fill with custom fields
$user->fill([
'name' => 'John',
'bio' => 'Developer', // Custom field
])->save();
Polymorphic Usage
// Shared custom fields across models
class Product extends Model { use HasCustomFields; }
class Order extends Model { use HasCustomFields; }
$product->setCustomField('weight', 2.5);
$order->setCustomField('shipping_method', 'express');
validateCustomField to enforce rules:
$user->validateCustomField('age', 'required|integer|min:18');
CustomFieldObserver for custom logic:
CustomField::observe(CustomFieldObserver::class);
public function scopeWithPremium($query) {
return $query->whereHas('customFields', fn($q) =>
$q->where('field_key', 'premium_until')
->where('field_value', '>', now())
);
}
Field Key Conflicts
id, model_id) will throw InvalidArgumentException.if (!$user->isValidCustomFieldKey('user_@key')) {
throw new \InvalidArgumentException('Invalid field key');
}
N+1 Queries
with('customFields') in eager loading triggers N+1.Type Mismatches
string as int may cause silent failures.$user->setCustomField('count', '42', 'bigint');
Soft Deletes
forceDelete() to purge custom fields.JSON Depth Limits
json_max_depth (default: 64) throws RuntimeException.\DB::enableQueryLog();
$user->getCustomField('key');
\DB::getQueryLog();
hasCustomField() to avoid null errors:
if ($user->hasCustomField('metadata')) {
$data = $user->getCustomField('metadata');
}
Custom Field Types
Trappistes\CustomFields\Contracts\FieldType:
class CustomType implements FieldType {
public function getType(): string { return 'custom'; }
public function serialize($value): string { /* ... */ }
public function deserialize(string $value) { /* ... */ }
}
config/custom-fields.php:
'types' => [
'custom' => \App\CustomType::class,
],
Model Events
retrieving, saved, or deleted events:
CustomField::saved(function ($field) {
\Log::info("Field updated: {$field->field_key}");
});
Batch Operations
CustomField::upsert() for bulk inserts/updates:
CustomField::upsert([
['model_type' => User::class, 'model_id' => 1, 'field_key' => 'key1', 'field_value' => 'value1'],
['model_type' => User::class, 'model_id' => 2, 'field_key' => 'key2', 'field_value' => 'value2'],
]);
custom_fields_model_index and custom_fields_key_index. Ensure these are optimized in production.setCustomFields(), chunk data to avoid memory issues:
$user->setCustomFields(array_chunk($fields, 100));
How can I help you explore Laravel packages today?