aureuserp / custom-fields
A Filament v5 plugin that lets end-users add dynamic custom fields to any Eloquent model + Filament resource at runtime.
Requires
- php: ^8.2
- filament/filament: ^5.0
- filament/forms: ^5.0
- filament/infolists: ^5.0
- filament/tables: ^5.0
- spatie/eloquent-sortable: ^4.0
- spatie/laravel-package-tools: ^1.15
This package is auto-updated.
Last update: 2026-04-21 11:32:28 UTC
README
Let your end-users add dynamic fields to any Eloquent model + Filament resource at runtime, without writing a single migration. Ships an admin CRUD for managing field definitions, an Eloquent trait that auto-merges the new columns into your model's fillable/casts, a Filament resource trait with five merge helpers that inject fields into forms, tables, filters, infolists, and a runtime schema manager that creates the underlying DB columns for you.
Table of contents
- Features
- Requirements
- Installation
- Quick start
- API reference
- Enums
- Real-world example
- Translations
- Publishing resources
- Testing
- Troubleshooting
- Security
- Contributing
- Credits
- License
Features
- Eloquent trait (
HasCustomFields) — drop onto any model; custom field codes auto-merge into$fillableand$castsat runtime - Filament resource trait — 5 one-line merge helpers:
mergeCustomFormFields,mergeCustomTableColumns,mergeCustomTableFilters,mergeCustomTableQueryBuilderConstraints,mergeCustomInfolistEntries - Admin CRUD (
/admin/custom-fields) — create, edit, sort, soft-delete dynamic fields per resource, with full validation / formatting settings - 11 field types via
FieldTypeenum — Text, Textarea, Select, Checkbox, Radio, Toggle, CheckboxList, DateTime, Editor, Markdown, ColorPicker - 8 text input types via
InputTypeenum — Text, Email, Numeric, Integer, Password, Tel, Url, Color - Schema manager (
CustomFieldsColumnManager) — programmatically add / drop DB columns when fields are created or deleted - Table integration — the defined fields automatically surface as
CustomColumns(ifuse_in_table=true) andCustomFilters - Spatie-sortable — drag-to-reorder fields with
order_column_name=sort - Soft deletes — recover deleted field definitions
- Policy + permissions — Filament Shield compatible, with
view_any_field_field,create_field_fieldetc. - Translations —
enandarshipped (navigation + form labels + validation names + setting names) - Pest test suite — architecture + model + policy + trait + components + enums (31 tests)
Requirements
- PHP 8.2+
- Laravel 11+
- Filament v5+
spatie/eloquent-sortablev4 (already a Filament dependency)
Installation
composer require aureuserp/custom-fields
The service provider is auto-discovered. The migration is registered via Spatie's package-tools and run on php artisan migrate.
php artisan migrate
Note
Migrating from webkul/fields: the custom_fields table migration keeps its original timestamp filename, so existing installations will see it already applied — no duplicate-table errors, no re-run.
Quick start
1. Mark your model
use Illuminate\Database\Eloquent\Model; use Webkul\CustomFields\Concerns\HasCustomFields; class Employee extends Model { use HasCustomFields; }
Any custom field code defined against Webkul\Employee\Models\Employee is now mass-assignable and properly cast.
2. Extend your Filament resource
use Filament\Resources\Resource; use Filament\Schemas\Schema; use Webkul\CustomFields\Filament\Concerns\HasCustomFields; class EmployeeResource extends Resource { use HasCustomFields; public static function form(Schema $schema): Schema { return $schema->components( static::mergeCustomFormFields([ // your base fields ]) ); } public static function table(Table $table): Table { return $table ->columns(static::mergeCustomTableColumns([ /* base */ ])) ->filters(static::mergeCustomTableFilters([ /* base */ ])); } }
3. Define fields in the admin CRUD
Visit /admin/custom-fields → New field → pick a resource → pick a field type → configure options/validations → save.
The field now appears in the resource's form, table, filters, and infolist automatically.
API reference
Eloquent trait
Webkul\CustomFields\Concerns\HasCustomFields
Boots three lifecycle listeners (retrieved, creating, updating) that call loadCustomFields():
loadCustomFields()— queriesField::where('customizable_type', get_class($this)), merges every field'scodeinto$fillableand applies type-appropriate casts.mergeFillable(array $attributes)— public helper exposed for ad-hoc merging.mergeCasts($attributes)— public helper; accepts either an array (passes through to parent) or a Collection of Field records.getCustomFields()(protected) — override this if you want to scope the query (e.g. to a specific tenant).
Type-to-cast mapping:
| Field type | Cast |
|---|---|
select (multiselect) |
array |
select (single) |
string |
checkbox, toggle |
boolean |
checkbox_list |
array |
| everything else | string |
Filament resource trait
Webkul\CustomFields\Filament\Concerns\HasCustomFields
Five static helpers — each accepts a base array + optional include/exclude lists and returns base + custom:
static::mergeCustomFormFields(array $base, array $include = [], array $exclude = []): array static::mergeCustomTableColumns(array $base, array $include = [], array $exclude = []): array static::mergeCustomTableFilters(array $base, array $include = [], array $exclude = []): array static::mergeCustomTableQueryBuilderConstraints(array $base, array $include = [], array $exclude = []): array static::mergeCustomInfolistEntries(array $base, array $include = [], array $exclude = []): array
include=[] means "all fields"; a non-empty list whitelists field codes. exclude blacklists field codes.
Components
Each injector is also usable standalone:
use Webkul\CustomFields\Filament\Forms\Components\CustomFields; use Webkul\CustomFields\Filament\Infolists\Components\CustomEntries; use Webkul\CustomFields\Filament\Tables\Columns\CustomColumns; use Webkul\CustomFields\Filament\Tables\Filters\CustomFilters; CustomFields::make(MyResource::class) ->include(['hobbies']) ->exclude(['internal_notes']) ->getSchema();
Column manager
Webkul\CustomFields\CustomFieldsColumnManager
Three static methods called on Field model lifecycle (create/update/delete):
createColumn(Field $field)— adds the column to the customisable model's table with the appropriate DB typeupdateColumn(Field $field)— creates the column if missing (for rename/resurrect scenarios)deleteColumn(Field $field)— drops the column
The DB type mapping uses getColumnType() which routes via FieldType::tryFrom($field->type):
| Field type | DB column type |
|---|---|
text |
string / integer / decimal (based on input_type) |
textarea, editor, markdown |
text |
select (multiselect) |
json |
select (single), radio, color |
string |
checkbox, toggle |
boolean |
checkbox_list |
json |
datetime |
datetime |
Enums
| Enum | Cases → values | Default |
|---|---|---|
FieldType |
Text='text', Textarea='textarea', Select='select', Checkbox='checkbox', Radio='radio', Toggle='toggle', CheckboxList='checkbox_list', DateTime='datetime', Editor='editor', Markdown='markdown', ColorPicker='color' |
FieldType::Text |
InputType |
Text, Email, Numeric, Integer, Password, Tel, Url, Color |
InputType::Text |
Both enums expose a default() static for symbolic-constant fallbacks:
use Webkul\CustomFields\Enums\FieldType; use Webkul\CustomFields\Enums\InputType; $type = FieldType::tryFrom($raw) ?? FieldType::default();
Real-world example
Here's a full Employee resource adopting the trait. End-users can add a "hobbies" multiselect via the admin CRUD; the field flows through form, table, and infolist automatically.
use Filament\Forms\Components\Select; use Filament\Forms\Components\TextInput; use Filament\Resources\Resource; use Filament\Schemas\Schema; use Filament\Tables\Columns\TextColumn; use Filament\Tables\Table; use Webkul\CustomFields\Filament\Concerns\HasCustomFields; use Webkul\Employee\Models\Employee; class EmployeeResource extends Resource { use HasCustomFields; protected static ?string $model = Employee::class; public static function form(Schema $schema): Schema { return $schema->components(static::mergeCustomFormFields([ TextInput::make('name')->required(), Select::make('department_id')->relationship('department', 'name'), ])); } public static function table(Table $table): Table { return $table ->columns(static::mergeCustomTableColumns([ TextColumn::make('name'), ])) ->filters(static::mergeCustomTableFilters([])); } }
And the Eloquent side:
use Webkul\CustomFields\Concerns\HasCustomFields; class Employee extends Model { use HasCustomFields; protected $fillable = ['name', 'department_id']; } // After an admin defines a "hobbies" checkbox_list field: $employee = Employee::create([ 'name' => 'Alice', 'department_id' => 1, 'hobbies' => ['chess', 'hiking'], // ← custom field, automatically fillable + cast to array ]); $employee->hobbies; // ['chess', 'hiking'] ← automatically cast from JSON
Translations
Ships with en and ar under the custom-fields:: namespace. Publish to customise:
php artisan vendor:publish --tag="custom-fields-translations"
The translation file includes 70+ validation rule labels, 200+ Filament setting names, plus navigation and form labels for the admin CRUD.
Configuration
Every navigation / identity / placement setting is configurable two ways — pick whichever fits your project:
1. Fluent setters in your panel provider (recommended for per-panel overrides)
// app/Providers/Filament/AdminPanelProvider.php use Webkul\CustomFields\CustomFieldsPlugin; ->plugins([ CustomFieldsPlugin::make() ->navigationGroup(__('admin.navigation.setting')) ->navigationLabel('Custom Fields') ->navigationIcon('heroicon-o-puzzle-piece') ->navigationSort(50) ->navigationBadge(fn () => \Webkul\CustomFields\Models\Field::count()) ->navigationBadgeColor('primary') ->slug('admin/custom-fields') ->cluster(\App\Filament\Clusters\AdminTools::class), ])
2. Publishable config file (recommended for app-wide defaults)
php artisan vendor:publish --tag="custom-fields-config"
That writes config/custom-fields.php to your app. Edit any key:
// config/custom-fields.php return [ 'navigation' => [ 'label' => 'Custom Fields', 'group' => 'Settings', 'icon' => 'heroicon-o-puzzle-piece', 'sort' => 50, 'badge' => null, 'register'=> true, ], 'resource' => [ 'register' => true, 'slug' => 'admin/custom-fields', 'cluster' => \App\Filament\Clusters\AdminTools::class, 'model_label' => null, 'plural_model_label' => null, ], ];
Resolution order
When the Resource renders, each value is resolved by the first matching rule:
- Fluent setter on
CustomFieldsPlugin::make()->…()— highest priority - Config file
config('custom-fields.*')— if the setter was not called - Hardcoded fallback in
getPluginDefaults()— when both above arenull
Full setter → config key map
| Fluent setter | Config key |
|---|---|
navigationLabel($v) |
custom-fields.navigation.label |
navigationGroup($v) |
custom-fields.navigation.group |
navigationIcon($v) |
custom-fields.navigation.icon |
activeNavigationIcon($v) |
custom-fields.navigation.active_icon |
navigationSort($v) |
custom-fields.navigation.sort |
navigationBadge($v) |
custom-fields.navigation.badge |
navigationBadgeColor($v) |
custom-fields.navigation.badge_color |
navigationBadgeTooltip($v) |
custom-fields.navigation.badge_tooltip |
navigationParentItem($v) |
custom-fields.navigation.parent_item |
subNavigationPosition($v) |
custom-fields.navigation.sub_position |
registerNavigation($bool) |
custom-fields.navigation.register |
modelLabel($v) |
custom-fields.resource.model_label |
pluralModelLabel($v) |
custom-fields.resource.plural_model_label |
slug($v) |
custom-fields.resource.slug |
cluster($class) |
custom-fields.resource.cluster |
tenantRelationshipName($v) |
custom-fields.resource.tenant_relationship |
registerResource($bool) |
custom-fields.resource.register |
Disable the admin CRUD entirely
Only want the Eloquent trait + table-injection API, not the admin menu?
CustomFieldsPlugin::make()->registerResource(false), // or in config/custom-fields.php: 'resource' => ['register' => false],
The FieldResource routes are skipped; everything else still works.
Publishing resources
php artisan vendor:publish --tag="custom-fields-config" php artisan vendor:publish --tag="custom-fields-migrations" php artisan vendor:publish --tag="custom-fields-translations"
Testing
vendor/bin/pest plugins/aureuserp/custom-fields/tests/Feature
31 tests (114 assertions) across:
| Area | Coverage |
|---|---|
| Architecture | Field model extends Eloquent Model + implements Sortable, Plugin implements Filament\Contracts\Plugin, SP extends Spatie, no debug calls in shipped code |
| Enums | FieldType / InputType — all cases have expected values, default() works, tryFrom returns null for unknown |
| Field model | Table name, fillable, casts, SoftDeletes trait, Sortable config |
| Policy | All 10 CRUD + soft-delete permission methods exist |
| Eloquent trait | Trait exists, applies to host model without error, mergeFillable dedups, declares fill / mergeCasts |
| Filament trait | Trait exists, all 5 merge helpers declared, merge helpers combine base + custom arrays |
| Component API | CustomFields/Entries/Columns/Filters::make()->include()->exclude() chainable and return static |
| Column manager | Exposes createColumn/updateColumn/deleteColumn static methods |
Troubleshooting
| Symptom | Fix |
|---|---|
Class not found for Webkul\CustomFields\… |
composer dump-autoload && php artisan optimize:clear |
| Trait methods not firing | Confirm the Eloquent trait is on your model (use HasCustomFields;) and that Field::where('customizable_type', …) returns rows |
| Custom column doesn't appear in the DB | Check CustomFieldsColumnManager::createColumn() ran — it's called from CreateField::afterCreate() on save. Verify the host table already exists. |
| Policy denies everything | Generate Shield policies: php artisan shield:generate --resource=FieldResource |
custom_fields table missing |
Run php artisan migrate |
Security
Email support@webkul.com for security-related reports instead of opening a public issue.
Contributing
PRs welcome. Before submitting:
vendor/bin/pest plugins/aureuserp/custom-fields/tests/Feature vendor/bin/pint plugins/aureuserp/custom-fields
Credits
- Webkul — plugin author
- Filament team — the excellent admin framework
- filamentphp/plugin-skeleton — structural template
License
MIT. See LICENSE.md.