DEV3 Technologies home

Filament settings page backed by a key-value table

A custom Filament 4 page with a form that loads and saves store settings from a simple key-value model, with a success toast.

FilamentFormsfilamentsettingspageform

Code

<?php

namespace App\Filament\Pages;

use App\Models\Setting;
use BackedEnum;
use Filament\Actions\Action;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Notifications\Notification;
use Filament\Pages\Page;
use Filament\Schemas\Components\Actions;
use Filament\Schemas\Components\EmbeddedSchema;
use Filament\Schemas\Components\Form;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Concerns\InteractsWithSchemas;
use Filament\Schemas\Contracts\HasSchemas;
use Filament\Schemas\Schema;
use Filament\Support\Icons\Heroicon;

class StoreSettings extends Page implements HasSchemas
{
    use InteractsWithSchemas;

    protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedCog6Tooth;

    protected static ?string $navigationLabel = 'Store settings';

    /** @var array<string, mixed> */
    public ?array $data = [];

    public function mount(): void
    {
        $this->form->fill(Setting::allAsArray());
    }

    public function form(Schema $schema): Schema
    {
        return $schema
            ->statePath('data')
            ->components([
                Section::make('General')->columns(2)->schema([
                    TextInput::make('store_name')->required()->maxLength(80),
                    Select::make('currency')->options(['USD' => 'US dollar', 'EUR' => 'Euro', 'GBP' => 'Pound'])->required(),
                    TextInput::make('tax_rate')->numeric()->suffix('%')->minValue(0)->maxValue(50),
                    Toggle::make('maintenance_mode')->label('Show maintenance banner'),
                ]),
            ]);
    }

    public function content(Schema $schema): Schema
    {
        return $schema->components([
            Form::make([EmbeddedSchema::make('form')])
                ->livewireSubmitHandler('save')
                ->footer([Actions::make([
                    Action::make('save')->label('Save changes')->submit('save')->keyBindings(['mod+s']),
                ])]),
        ]);
    }

    public function save(): void
    {
        Setting::putMany($this->form->getState());

        Notification::make()->title('Settings saved')->success()->send();
    }
}

Needs: filament/filament@^4.0

How it works

What it solves

Admins need a place to change global options such as the store name, currency or a maintenance banner, without a deploy and without creating a database column per option. A key-value table plus one Filament page covers that.

How it works

The page is a plain Page that implements HasSchemas. mount() fills the form from Setting::allAsArray(), and form() declares the fields and points them at the data state path. Filament 4 pages have no built-in form wrapper, so content() composes one: a Form component containing the embedded schema, a submit handler named save, and a footer with an Actions block (with a Cmd/Ctrl+S shortcut).

save() takes validated state from getState(), which already enforces required, numeric and the rest, then writes each pair with updateOrCreate and clears the cache. Values are cast to JSON, so booleans and numbers keep their types when you read them back with Setting::get('tax_rate', 0).

Adapting it

Add more Sections or tabs as the settings grow. To restrict the page, override canAccess() with a role or permission check. The model helpers are static for brevity; wrap them in an injectable service if you prefer.

Pitfalls

  • Create the settings migration with a unique key column, otherwise updateOrCreate can duplicate rows.
  • rememberForever is only correct if every write goes through putMany().
  • Do not store secrets here without encrypting them, JSON values are plain text.

How to use it

  1. Create a migration for a settings table: unique string key, json value, timestamps.
  2. Add Setting.php to app/Models/ and the page to app/Filament/Pages/.
  3. Edit the fields in form() to match the options you need.
  4. Read values anywhere with Setting::get('currency', 'USD').
  5. Restrict access by adding public static function canAccess(): bool with your own rule.

Licence: DEV3 Snippet Licence. Free to use, change and ship in personal and commercial projects. Attribution is appreciated, not required. Please do not redistribute snippets as a collection.

Related snippets