Add Custom Fields To Laravel Model using Filament PHP

Howdy!
Adding custom fields to a Laravel model using Filament PHP can greatly enhance your application's functionality. Filament provides an easy way to manage these fields, making development more efficient. In this guide, we'll show you how to seamlessly integrate custom fields into your Laravel models with Filament PHP, streamlining your workflow and boosting your app's capabilities.

Author

Note: This article assumes you have Filament up and running.

In this tutorial we will be using Laravel Settings package from Spatie, but you can use whatever solution you want.

We just need to save the fields data into the database.

So let's dive in.

The first thing we will do is to prepare the database column to store the schema for the custom fields.
Using Laravel Settings package, let's create the settings.

php artisan make:setting Customization --group=customization

Running this command will create a new class called Customization in the Settings folder.

Next thing, we will register the recently created class.
Head to settings.php (the configuration file), and add it into the settings array.

And the final step for this, is to create the migration.

To do so, run the following command:

php artisan make:settings-migration CreateCustomization

The command will create a new migration file, open it and add the following into the up method :

$this->migrator->add('custom.contact_custom_fields', json_encode([]));

Now that we have our migration in place. We can proceed to the interesting part, which is creating the component that will allow us to create the schema or the blueprint for the custom fields.

First thing is create the Livewire component.

php artisan livewire:make CustomFieldsComponent 

This will create a class under Livewire folder called CustomFieldsComponent.

The next to do is defining the form schema that will holder the blueprint.

In the CustomFieldComponent class, we will add Filament Forms, so we can define the blueprint.

The Livewire component class should be like this :

class CustomFieldsComponent extends Component implements HasForms
{
    use InteractsWithForms;

    public $customFields;

    public ?array $data = [];

    public function mount()
    {

        $this->customFields = app(CustomFields::class)->contact_custom_fields;

        if (is_string($this->customFields)) {
            $this->customFields = json_decode($this->customFields);
        }

        $this->form->fill([
            'custom_fields' => $this->customFields,
        ]);
    }

    public function form(Form $form): Form
    {
        return $form->schema([
            Builder::make('custom_fields')->label(__('Custom Fields'))
                ->blocks([
                    Block::make('text')
                        ->label(__('Text Input'))
                        ->schema([
                            $this->getFieldNameInput(),
                            Checkbox::make('is_required')->label(__('Required')),
                        ]),
                    Block::make('select')
                        ->label(__('Select'))
                        ->schema([
                            $this->getFieldNameInput(),
                            KeyValue::make('options')
                                ->label(__('Options'))
                                ->addActionLabel(__('Add option'))
                                ->keyLabel(__('Value'))
                                ->valueLabel(__('Label')),
                            Checkbox::make('is_required')->label(__('Required')),
                        ]),
                    Block::make('checkbox')
                        ->label(__('Checkbox'))
                        ->schema([
                            $this->getFieldNameInput(),
                            Checkbox::make('is_required')->label(__('Required')),
                        ]),
                ]),

        ])->statePath('data');
    }

    public function save()
    {
        $settings = app(CustomFields::class);
        $settings->contact_custom_fields = $this->form->getState()['custom_fields'];
        $settings->save();

    }

    protected function getFieldNameInput(): Grid
    {

        return Grid::make()
            ->schema([
                TextInput::make('name')
                    ->lazy()
                    ->afterStateUpdated(function (Set $set, $state) {
                        $label = Str::of($state)
                            ->kebab()
                            ->replace(['-', '_'], ' ')
                            ->ucfirst();
                        $set('label', $label);
                    })
                    ->required(),
                TextInput::make('label')
                    ->required(),
            ]);
    }

    public function render()
    {
        return view('livewire.custom-fields-component');
    }

Now let's explain the code :
At first, we implemented the HasForms trait, so we can leverage Filament power.

Then we defined the array of the forms data so we can save it.
Also we defined the customSettings variable, so it will hold the previous settings, just in case we needed to edit it.

Later on, in the mount function, we initialized everything.

The interesting part here is the form function. This is where all the magic happens.

Here, we used the Builder component, and according to the Filament docs :

Similar to a repeater, the builder component allows you to output a JSON array of repeated form components. Unlike the repeater, which only defines one form schema to repeat, the builder allows you to define different schema "blocks", which you can repeat in any order. This makes it useful for building more advanced array structures.

So you can why we used the builder component instead of the repeater. Simply because, we want to define multiple blocks. However the repeater will allow us to repeat just a single data structure (select, text input, text area...)

In our example we defined only 3 data structures, which is the TextInput, the Select, and the Checkbox. But you have all the freedom to define what you want.
Every block we defined also, will have a checkbox to mark it as required or not. So later on, when using the predefined blueprint, we can check if the input is required or not.

Moving on to the getFieldNameInput function. This method is just to define the name and the label of the input. Since every input in a form requires to have a name, and optionally a label.

Now finally to the save method, as the name suggests, it will save the blueprint into the database, precisely in the earlier created settings.

This screenshot will show an example :

This is the first part of the article, which is defining the logic to create the blueprint.

Now we will move on to the next part, which is attach this blueprint to a model.

In this example, we will use a Contact model, but again, you can use whatever you want / need.

In the model migration file, we will add a json field, which will contain the custom fields with their values.

$table->json('custom_fields')->nullable();

And one more thing to do is to cast that field into an array, so in the model class, add this :

protected $casts = [
    'custom_fields' => 'array',
];

Then we will head to the component where you create the model, in our case the Contact model.

Of course the Livewire component will implement HasForms interface.

In the form method, add the following :

public function form(Form $form): Form
    {

        $settings = app(CustomFields::class)->contact_custom_fields;

        if (is_string($settings)) {
            $settings = json_decode($settings);
        }

        return $form->schema([
...
          Section::make()->schema(function () use ($settings) {
                return array_map(function (array $field) use ($settings) {
                    $config = $field['data'];
                    return match ($field['type']) {
                        'text' => TextInput::make($config['name'])
                            ->label($config['label'])
                            ->required($config['is_required']),
                        'select' => Select::make($config['name'])
                            ->label($config['label'])
                            ->options($config['options'])
                            ->required($config['is_required']),
                        'checkbox' => Checkbox::make($config['name'])
                            ->label($config['label'])
                            ->required($config['is_required']),
                    };
                }, $settings);
            }),
...
        ])->statePath('data')->model(Contact::class);
    }

In this example, we will get the blueprint from the settings. Then we will map through them, and show them into the form.

So if we will visit the form, it will appear like this, since in our example, we defined a VAT field.

Now all what's left is to save the custom fields values in the database.

Using this code in the save function you are using, will make sure that the data you are saving is well structured, so you can use it later on.

The structure will be as follows :

array:1 [▼ 
  "VAT" => "1645070A"
]

And this is the code that will do the transformation :

$settings = app(CustomFields::class)->contact_custom_fields;

if (is_string($settings)) {
    $settings = [];
}

$names = array_map(function ($field) {
    return $field['data']['name'];
}, $settings);

$customFields = [];
foreach ($names as $field) {
    if (isset($this->form->getState()[$field])) {
        $customFields[$field] = $this->form->getState()[$field];
    }
}

Finally all you have to do is to save that array into your model.

This is a walkthrough on how to implement Custom Fields in Laravel using Filament.
This example won't limit you on how to use this technique, more or less, or where to use it.
I hope everything were clear and easy to understand.
As usual, if you have a question or an improvement, you can contact me via Twitter .
Thank you for your reading and happy coding.