salah / laravel-custom-fields
Professional, flexible, and headless-ready Custom Fields (EAV) package for Laravel.
Package info
github.com/salaheldeen911/laravel-custom-fields
pkg:composer/salah/laravel-custom-fields
Fund package maintenance!
Requires
- php: ^8.2
- ext-intl: *
- illuminate/contracts: ^10.0||^11.0||^12.0
- propaganistas/laravel-phone: ^5.0
- spatie/laravel-package-tools: ^1.92
Requires (Dev)
- illuminate/support: ^10.0 || ^11.0
- larastan/larastan: ^2.9||^3.0
- laravel/pint: ^1.14
- nunomaduro/collision: ^8.1.1||^7.10.0
- orchestra/testbench: ^10.0.0||^9.0.0||^8.22.0
- pestphp/pest: ^2.0||^3.0
- pestphp/pest-plugin-arch: ^2.5||^3.0
- pestphp/pest-plugin-laravel: ^2.0||^3.0
- phpstan/extension-installer: ^1.3
- phpstan/phpstan-deprecation-rules: ^1.1||^2.0
- phpstan/phpstan-phpunit: ^1.3||^2.0
README
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 auth middleware 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:prune
Tip: 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.