Skip to content

Settings · Open source

Record Settings

A free way to edit per-user, per-team and per-tenant settings in Filament 4 and 5: a settings page, a settings modal on table rows and settings fields inside resource forms, stored with glorand/laravel-model-settings or in a JSON column.

Filament 4 and 5 Laravel 12 and 13 PHP 8.2+ v1.0.2 Free · MIT
Record Settings: A free way to edit per-user, per-team and per-tenant settings in Filament 4 and 5: a settings page, a settings modal on table rows and settings fields inside resource forms, stored with glorand/laravel-model-settings or in a JSON column.

What it solves

Most apps end up with settings that belong to one record: a user's theme and email digest, a team's locale and seat limit, a tenant's single sign-on switch.

Building a Filament page or modal for them means loading the stored values, merging in defaults, writing back only what changed and checking who may edit them, for every model that has settings. quadrubo/filament-model-settings did this for Filament 3 and 4; its latest release on Packagist (v2.0.1) requires Filament 4, Laravel 10 to 12 and glorand 6 to 8, so it does not install on Filament 5, Laravel 13 or glorand 9.

Record Settings gives you three ways to edit one record's settings. RecordSettingsPage is a page that, with only a form(), edits the signed-in user's settings; override getSettingsRecord() to edit a tenant, a team or any other model. EditRecordSettingsAction opens the same kind of form in a modal for a table row or a record page. ->recordSetting() turns a field in a resource form into a setting of that record, saved on create and on edit. A model that uses glorand/laravel-model-settings is read and written through glorand (all() and setMultiple()), so its driver, defaults and $settingsRules apply; any other model keeps its settings as a JSON object in a column, settings by default. Saving writes the top-level keys the form holds and keeps every other stored key. When the record's policy has an updateSettings method, the page and the action ask it; without one the plugin adds no check of its own. It is a new implementation, written as a successor to quadrubo/filament-model-settings for Filament 5, Laravel 13 and glorand 9, and keeps that package's getSettingRecord() and isModelSetting() names working. It has no plugin class, migrations, config file or assets. An independent plugin, not made or endorsed by Filament or by the author of quadrubo/filament-model-settings.

  • RecordSettingsPage: a settings page for one record, the signed-in user by default, or whatever getSettingsRecord() returns
  • EditRecordSettingsAction: a Settings modal (gray, cog icon) for table rows, or a header action on a view or edit page
  • ->recordSetting() on any form field, so settings sit next to a record's own fields and are written after the record is saved, on the create page too
  • Storage through glorand/laravel-model-settings 8 or 9 when the model uses it, with whichever driver it is set to (field, table, Redis or cache)
  • Or a JSON column on the model's own table, settings by default, cast to array, json, collection or AsArrayObject, or not cast at all
  • Writes only the top-level keys in the form and keeps every other stored key
  • Defaults from the model ($defaultSettings or getDefaultSettings()) and from the page or action, filled into the form
  • An updateSettings policy check when the record's policy defines it, with the ability name changeable or turned off per page and per action
  • glorand $settingsRules errors shown under the matching field on the page, in the modal and on ->recordSetting() fields in a resource form
  • make:filament-record-settings-page writes a starter page into the panel's page directory
  • Page hooks (beforeFill, afterFill, beforeValidate, afterValidate, beforeSave, afterSave) and mutateFormDataBeforeFill() and mutateFormDataBeforeSave(), with the save run in a database transaction
  • Core Filament components only: no stylesheet, no build step, light and dark
A Team settings page in a light Filament panel, with Dashboard, Preferences, Orgs, Projects, Teams and Team settings (selected) in the sidebar. A Workspace section, described as stored with glorand/laravel-model-settings, holds a Locale select set to Français, a Seats field set to 20 with the hint "Up to 50 (glorand settingsRules)", and a Single sign-on toggle that is off, with a Save button below.
A Team settings page in a light Filament panel, with Dashboard, Preferences, Orgs, Projects, Teams and Team settings (selected) in the sidebar. A Workspace section, described as stored with glorand/laravel-model-settings, holds a Locale select set to Français, a Seats field set to 20 with the hint "Up to 50 (glorand settingsRules)", and a Single sign-on toggle that is off, with a Save button below.

Install

Requires PHP 8.2+, Filament 4 and 5, and Laravel 12 and 13.

composer require amjadiqbal/filament-record-settings

Usage

use AmjadIqbal\FilamentRecordSettings\Pages\RecordSettingsPage;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Toggle;
use Filament\Schemas\Schema;

class ManagePreferences extends RecordSettingsPage
{
    protected static ?string $title = 'Preferences';

    public function form(Schema $schema): Schema
    {
        return $schema->components([
            Select::make('theme')->options(['system' => 'System', 'light' => 'Light', 'dark' => 'Dark']),
            Toggle::make('email_digest'),
        ]);
    }
}

Setup

Install the package; its service provider is discovered by Laravel. There is no plugin to add to the panel, and no migration, config file or assets: the provider registers the ->recordSetting() field macro, the make:filament-record-settings-page command and the English strings (Settings, Save, "Settings saved"). glorand/laravel-model-settings is optional and suggested, not required. The only stub is the page template the command writes.

  1. 1 composer require amjadiqbal/filament-record-settings
  2. 2 Either set up glorand/laravel-model-settings (^8.0 or ^9.0) on the model as its README says, or add a JSON column, for example $table->json('settings')->nullable(), to the model's table yourself.
  3. 3 php artisan make:filament-record-settings-page ManagePreferences (add --panel= to pick a panel, --force to overwrite), then fill in its form().

The settings page

RecordSettingsPage is an abstract Filament page with a cog icon. Its form is filled from the record's settings on mount and saved with a Save button (or Ctrl/Cmd+S), followed by a "Settings saved" notification.

  • The record is the signed-in user of the panel's auth guard unless you override getSettingsRecord(). A page that still defines quadrubo's getSettingRecord() is read through that method instead.
  • For a tenant, return Filament::getTenant() from getSettingsRecord(); for a team or any other model, return that model.
  • When getSettingsRecord() returns null (nobody signed in, no tenant), the page denies access and is left out of the navigation.
  • protected static ?string $settingsColumn names the JSON column; left null, a glorand model goes through glorand and any other model uses settings.
  • getSettingsDefaults() returns defaults for the JSON column store; glorand models keep their own.
  • Validation is your form's own Filament rules; glorand $settingsRules failures are shown under the matching field.
  • The save runs inside a database transaction and calls beforeValidate, afterValidate, beforeSave and afterSave, with mutateFormDataBeforeSave() in between; getRedirectUrl() is null by default, so the page stays where it is.
  • To open it from the user menu instead of the sidebar, add a userMenuItems() action pointing at the page's getUrl() and return false from shouldRegisterNavigation().

The settings modal

EditRecordSettingsAction (named editSettings) is a Filament action labelled Settings. Give it a schema() of fields; it fills them from the row's or page's record and saves them on submit.

  • Use it in a table's recordActions() or as a header action on a view or edit page.
  • ->settingsColumn('prefs') picks the JSON column and ->settingsDefaults([...]) sets defaults for it; both also take a closure.
  • ->settingsAbility('manageSettings') changes the policy method it asks; ->settingsAbility(null) turns the check off.
  • Your own ->authorize() adds to the policy check instead of replacing it: the action is authorized only when the updateSettings check and your authorize() both allow it. A ->visible() condition adds to it the same way.
  • A user the check refuses does not see the action, and Filament refuses it if it is called anyway.
EditRecordSettingsAction::make()
    ->settingsColumn('prefs')                   // optional
    ->settingsDefaults(['color' => '#0d9488'])  // optional, JSON column only
    ->schema([
        ColorPicker::make('color'),
        Toggle::make('archived_view'),
    ])

Settings fields in a resource form

Add ->recordSetting() to a field whose name is prefix.key, for example Toggle::make('settings.email_digest')->recordSetting(). The prefix is the JSON column for the column store; on a glorand model it only groups the fields and the value goes through glorand. ->recordSetting('prefs') sets the prefix explicitly. The isModelSetting() name from quadrubo/filament-model-settings is registered too, unless another package registered it first.

  • The field is left out of the record's own attributes. Its value is written through the settings store after the record is saved, so it works on the create page.
  • On an existing record the field is filled from the stored settings plus the model's own defaults; defaults set on a page or action do not apply here. On the create page the field's own default() applies.
  • Before the edit page saves the record, the plugin puts the stored settings back into the prefix column, so the empty prefix key Filament leaves in the form data does not wipe settings the form does not show.
  • A nested name such as settings.notify.email is read from inside its key, and saved inside that key while keeping its other values, in the JSON column and with glorand alike.
  • A glorand $settingsRules failure shows under the field and its value is not saved; on the edit page the record's own fields are not saved either.
  • The plugin adds no policy check to these fields; they follow the resource's own policy.
  • PHPStan does not know the macros; the README gives an ignoreErrors pattern for them.

Where settings are stored

The store is picked per record. With no column named, a model whose settings() method returns a glorand settings manager goes through glorand; any other model uses the settings column. Naming a column (on the page, the action or a field prefix on a non-glorand model) always uses that JSON column.

  • JSON column: the stored object is read whether the column is cast to array, json, collection or AsArrayObject, or left as a JSON string. Writing merges the form's top-level keys over the stored ones, keeps every other key, sets the column back in the form it is cast to (an array, or a JSON string when uncast) and saves the record.
  • glorand: the plugin reads all() (stored values with glorand's defaults) and writes with setMultiple(), so the model's driver (field, table, Redis or cache), $defaultSettings and $settingsRules apply. glorand validates the result and the plugin shows its errors under the field on the page, in the modal and on resource form fields.
  • A field that holds a nested array, such as a key-value or repeater field, replaces that top-level key as a whole.
  • Keys stored but not in the form are kept. Keys only in the defaults are not saved until a form that shows them is saved.

How defaults are worked out

For the JSON column, the form is filled from three layers, each overriding the one before it: the defaults given to the page (getSettingsDefaults()) or action (->settingsDefaults()), then the model's getDefaultSettings() or $defaultSettings, then the stored values. The layers are merged recursively, so a nested default survives when only some of its keys are stored. Note the order: a model-level default wins over a page or action default for the same key. For a glorand model, glorand's own defaults apply and the page and action defaults are ignored.

Who can edit whose settings

The plugin has one check, run on the page (when it is listed, opened and saved) and on the action. Out of the box, with no policy method, every user who can open the page edits their own settings, and every user who can see a table edits the settings of any row in it.

  • If the record has a policy and that policy has the ability method (updateSettings by default), Gate::allows() on it decides.
  • If there is no policy, or the policy has no such method, the plugin allows it and leaves access to you: the page's canAccess(), the action's visible() or authorize(), the resource's policy.
  • The ability can be renamed per page ($settingsAbility) and per action (->settingsAbility()), or set to null to skip the check.
  • The page also requires its parent canAccess() and a non-null record, and checks again when it saves.
class TeamPolicy
{
    public function updateSettings(User $user, Team $team): bool
    {
        return $user->ownsTeam($team);
    }
}

Multi-tenancy

The plugin does not scope settings by tenant on its own. A page that keeps the default record edits the signed-in user's settings, which are the same in every tenant. For per-tenant settings, return Filament::getTenant() from getSettingsRecord(); outside a tenant that returns null and the page denies access. The modal edits whichever record its row or page holds, so it follows the tenant scoping of the table it is on.

Coming from quadrubo/filament-model-settings

This is a separate, new implementation, not a fork; no code is shared. It keeps the older names working so moving is mostly an import change. Saving differs: only the keys in the form are written.

  • ModelSettingsPage with HasModelSettings becomes RecordSettingsPage, with no interface.
  • A static getSettingRecord() on the page is still read; getSettingsRecord() is the new name and defaults to the signed-in user.
  • ->isModelSetting() and ->isModelSetting('prefix') still work; ->recordSetting() is the new name.
  • make:filament-model-settings-page becomes make:filament-record-settings-page.
  • The page form uses the Schema class of Filament 4 and 5.

Credits

The idea and the getSettingRecord() and isModelSetting() names follow quadrubo/filament-model-settings (MIT); that package's author is not affiliated with and has not endorsed this one. Storage through glorand/laravel-model-settings (MIT). Record Settings is an independent plugin, not made or endorsed by Filament.

Hire me

Need this built into your app, or something custom?

I wrote Record Settings. I can wire it into your Filament panel, extend it for your case, or build the feature around it. Tell me what you need and you'll get a plan and a quote, free.

Support

Found a bug or want a feature? Open an issue on GitHub. For help adding it to your own project, use the quote form above.

Need something built?

Free quote in 60 seconds