laravel-custom-fields maintained by salah
Laravel Custom Fields
The Professional, Sealed-Lifecycle EAV Solution for Modern Laravel Applications.
Tired of messy "extra_attributes" JSON columns that are impossible to validate? Treat user-defined fields as first-class citizens. This package provides high-performance, strictly validated, and extensible custom fields with native support for both Blade (Full-Stack) and Headless (API) architectures.
🔥 Why This Package?
- 🛡 Strict Lifecycle: We validate the rules themselves. You can't save a
min > maxor an invalidregex. - 🚫 Intelligent Conflict Prevention: Automatically prevents assigning conflicting rules (e.g., you can't use
Letters OnlyandAlpha-Numerictogether). - ⚡️ Built for Speed: Uses database
upsertsand batch operations. Reduces database overhead from N queries to just one per request. - 🏗 Refactor-Safe Polymorphism: Uses a
configmap for models. High stability even if you change model namespaces. - 🧩 Dual-Nature Architecture:
- Blade: Ready-to-use Tailwind components with error handling and old-input support.
- Headless: Rich metadata API (
models-and-types) explaining rules, labels, and tags to your Frontend.
📦 Installation
composer require salah/laravel-custom-fields
Install the package (publishes config, migrations, and assets):
php artisan custom-fields:install
⚙️ Configuration
-
Map Your Models: In
config/custom-fields.php, define simple aliases for your models. This decouples your database from your class names.'models' => [ 'user' => \App\Models\User::class, 'product' => \App\Models\Product::class, ], -
Prepare Your Model: Add the
HasCustomFieldstrait.use Salah\LaravelCustomFields\Traits\HasCustomFields; } -
Advanced Configuration: Tune the package in
config/custom-fields.php.- Cache Strategy: Control
ttlandprefixto balance performance and freshness. - Security: Enable
sanitize_htmlto automatically strip tags from text inputs. - Maintenance: Configure
pruningretention periods for soft-deleted fields.
- Cache Strategy: Control
[!IMPORTANT] API Security: If you enable API or Web routes in config, the package will automatically check for authentication middleware. If missing, it will log a warning. Ensure your routes are protected by adding
authmiddleware in the config.
🧹 Maintenance & Pruning
To keep your database clean, you can permanently remove soft-deleted custom fields that are older than a configured threshold.
-
Configure: Set
'prune_deleted_after_days' => 30in your config file. -
Run Command:
php artisan custom-fields:pruneTip: Schedule this command in your
App\Console\Kernelto run weekly.
🧠 Architecture & Validation Concepts
This package separates the world into two distinct logical flows to prevent confusion:
1. The Admin Flow (Defining Fields)
- Goal: Define what a field is (e.g., "Age").
- Trait:
ValidatesFieldDefinition - Usage: Only used when creating/editing the field definitions themselves. It validates that your rules don't conflict (e.g., preventing
alphalogic on anumberfield).
2. The User Flow (Entering Data)
- Goal: Fill in the field (e.g., "25").
- Trait:
ValidatesFieldData - Usage: Used in your Application's forms. It applies the rules defined in Step 1 to the user's input.
🏛 Usage: The Laravel Way
1. Rendering the UI (Blade)
Automatically render all custom fields for a specific model using a single tag. It handles errors, old(), and specific input types.
<form action="{{ route('users.store') }}" method="POST">
@csrf
<!-- Standard Fields -->
<input type="text" name="name" />
<!-- Dynamic Custom Fields Magic -->
<x-custom-fields::render :model="$user ?? null" :customFields="\App\Models\User::customFields()" />
<button type="submit">Save</button>
</form>
2. Validation (Option A: Form Request - Recommended)
The cleanest way to validate custom fields is by using the ValidatesFieldData trait in your Form Request.
CRITICAL: If
strict_validationis enabled in config (default: true), you MUST use this trait. It not only merges rules but also "marks" the data as safely validated. Failure to use it will result in aValidationIntegrityException.
use Salah\LaravelCustomFields\Traits\ValidatesFieldData;
class StoreUserRequest extends FormRequest
{
use ValidatesFieldData;
public function rules(): array
{
// MERGE custom fields rules into your existing rules
return $this->withCustomFieldsRules(User::class, [
'name' => 'required|string|max:255',
]);
}
}
3. Validation (Option B: Controller)
If you prefer validating in the controller, use the helper method on the model:
$validated = $request->validate(array_merge([
'name' => 'required',
], User::getCustomFieldRules()));
// Note: getCustomFieldRules() is a helper from the HasCustomFields trait
// getCustomFieldModelAlias() is also available for programmatic model resolution
3. Validation (Option C: Manual Service)
For complex scenarios where you need granular control or are validating data outside of a request:
// Validate only custom fields (Throws ValidationException on failure)
app(CustomFieldsService::class)->validate(User::class, $data);
4. Storage & Updates
Use optimized batch methods to save or update custom values.
Recommendation: It is highly recommended to wrap the creation/update of your main model and the custom fields in a Database Transaction. This ensures that if the custom field validation fails (or any other error occurs), the main model is not created/updated partially.
use Illuminate\Support\Facades\DB;
// Storing
DB::transaction(function () use ($request) {
$user = User::create($request->validated());
$user->saveCustomFields($request->validated());
});
// Updating (Uses high-performance UPSERT logic)
DB::transaction(function () use ($request, $user) {
$user->update($request->validated());
$user->updateCustomFields($request->validated());
});
🔍 Retrieval & Powerful Querying
Get Single Value
$bio = $user->custom('biography');
Get All Values (Flat Array)
Perfect for API responses or data exports.
return response()->json([
'user' => $user,
'custom_data' => $user->customFieldsResponse()
]);
// Response: {"biography": "...", "age": 30, "city": "Cairo"}
Querying like a Pro
The package provides a powerful scope to filter your models by custom fields values.
// Find users where custom field 'city' is 'Cairo'
$users = User::whereCustomField('city', 'Cairo')->get();
⚡️ Performance & Eager Loading
To avoid the N+1 query problem when displaying multiple models, always use the withCustomFields scope. This eager loads all values and their field configurations in just two queries.
// Optimized for lists/tables
$users = User::withCustomFields()->paginate(20);
foreach ($users as $user) {
echo $user->custom('biography'); // No extra queries!
}
Optimize Show/Edit Pages
When displaying a single model (e.g., in show or edit methods), use the loadCustomFields() helper to ensure all data is loaded efficiently before rendering the view.
public function edit(User $user)
{
// Eager loads values relationship
return view('users.edit')->with('user', $user->loadCustomFields());
}
🧩 Built-in Field Types
| Type | Icon | HTML Control | Supported Rules |
|---|---|---|---|
text |
📝 | <input type="text"> |
min, max, regex, alpha, alpha_dash, alpha_num |
textarea |
📄 | <textarea> |
min, max, regex, not_regex |
number |
🔢 | <input type="number"> |
min, max |
decimal |
💹 | <input type="number" step="any"> |
min, max |
date |
📅 | <input type="date"> |
after, before, after_or_equal, date_format |
time |
🕒 | <input type="time"> |
required (Standard string validation) |
select |
🔽 | <select> |
required (Strictly validated against options) |
checkbox |
✅ | <input type="checkbox"> |
required |
phone |
📞 | <input type="tel"> |
phone, mobile, landline (Supports formats or AUTO detection) |
email |
✉️ | <input type="email"> |
min, max, regex (Native email validation) |
url |
🔗 | <input type="url"> |
min, max, regex (Native URL validation) |
color |
🎨 | <input type="color"> |
required (Validates hex color format) |
file |
📂 | <input type="file"> |
mimes, max_file_size (Secure storage & URL generation) |
🛡 Validation Rule Conflicts
The system is smart enough to prevent logical errors in your field configurations. If you try to apply conflicting rules, the system will throw a validation error during the field creation/update.
Common Conflicts Prevented:
alphavsalpha_numvsalpha_dashaftervsafter_or_equalbeforevsbefore_or_equal
🛠 Advanced Customization
Registering New Types
Create a class extending FieldType and register it in your AppServiceProvider.
public function boot() {
$this->app->make(FieldTypeRegistry::class)->register(new MyCustomType());
}
Extending Validation Rules
You can add your own validation rules. If your rule conflicts with another, simply override the conflictsWith() method:
class MyPremiumRule extends ValidationRule {
public function conflictsWith(): array {
return ['basic_rule_name'];
}
}
🏛 Headless & API Reference
This package is a first-class citizen for Headless architectures. It provides a built-in API to manage custom fields and provides the necessary metadata for frontends to render them.
1. The Blueprint (Metadata)
Before rendering any UI, your frontend (React/Vue/Mobile) should fetch the types and rules.
Endpoint: GET /api/custom-fields/models-and-types
Response:
{
"success": true,
"data": {
"models": ["user", "product"],
"types": [
{
"name": "text",
"label": "Text Field",
"tag": "input",
"type": "text",
"has_options": false,
"allowed_rules": [
{ "name": "min", "label": "Min Length", "tag": "input", "type": "number" }
]
}
]
}
}
2. Managing Fields (CRUD API)
If you are building your own Admin Dashboard in a JS framework, use these endpoints:
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/custom-fields |
List all fields (Paginated) |
| POST | /api/custom-fields |
Create a new field |
| PUT | /api/custom-fields/{id} |
Update field configuration |
| DELETE | /api/custom-fields/{id} |
Soft delete a field |
| POST | /api/custom-fields/{id}/restore |
Restore a soft-deleted field |
| DELETE | /api/custom-fields/{id}/force |
Permanently delete a field |
Example: Creating a Field
Payload (POST /api/custom-fields):
{
"name": "Technical Bio",
"model": "user",
"type": "text",
"required": true,
"validation_rules": {
"min": 10,
"max": 500
}
}
3. Storing Values (Entity Integration)
When your frontend sends data to update a model (like a User profile), send the custom fields as a flat object where the key is the slug.
Payload (PUT /api/users/12):
{
"name": "Salah Eldeen",
"email": "salah@example.com",
"technical-bio": "Full-stack developer with 10 years of experience."
}
Controller Implementation:
public function update(Request $request, User $user) {
$user->update($request->all());
$user->updateCustomFields($request->all()); // Scans for slugs and updates values automatically
return response()->json(['success' => true]);
}
🎨 Management UI
The package comes with a built-in, secure management interface to create and manage fields.
- Route:
/custom-fields(Configurable incustom-fields.php) - Features: List, Search, Create, Edit, and Trash management.
👨🍳 Cookbook: Advanced Scenarios
Creating a Dependent Dropdown Field Type
Scenario: You want a City field that updates its options based on a Country field.
1. Create the Field Type Class
Create app/CustomFields/Types/DependentSelectField.php. We will use the options array to store the "parent field" slug.
namespace App\CustomFields\Types;
use Salah\LaravelCustomFields\FieldTypes\FieldType;
class DependentSelectField extends FieldType
{
public function name(): string { return 'dependent_select'; }
public function label(): string { return 'Dependent Select'; }
public function htmlTag(): string { return 'select'; }
// We expect 'options' to contain the slug of the parent field
// e.g., options: ["country"]
public function hasOptions(): bool { return true; }
public function description(): string {
return 'A select menu that depends on another field.';
}
public function baseRule(): array {
return ['string']; // Basic validation
}
public function view(): string {
return 'components.custom-fields.dependent-select';
}
}
2. Register the Type
In AppServiceProvider::boot():
use Salah\LaravelCustomFields\FieldTypeRegistry;
use App\CustomFields\Types\DependentSelectField;
public function boot() {
app(FieldTypeRegistry::class)->register(new DependentSelectField());
}
3. Frontend Implementation
Since the dependency logic is frontend-heavy, your component (resources/views/components/custom-fields/dependent-select.blade.php) should listen to the parent field.
@props(['field', 'value', 'allFields'])
@php
$parentSlug = $field->options[0] ?? null;
@endphp
<div x-data="{
parentVal: '',
options: [],
init() {
// Pseudo-code: Listen to the parent field change
document.addEventListener('custom-field-changed:{{ $parentSlug }}', (e) => {
this.fetchOptions(e.detail.value);
});
}
}">
<select name="{{ $field->slug }}" x-model="value">
<option value="">Select Option</option>
<template x-for="opt in options">
<option :value="opt" x-text="opt"></option>
</template>
</select>
</div>
📄 License
The MIT License (MIT). Please see License File for more information.