sitesource/laravel-polymorphic-settings-filament
Filament admin panel integration for polymorphic settings: manage key-value configuration attached to any Eloquent model through a clean UI, with per-model overrides, typed values, and easy extension for Laravel apps.
Install the package alongside Filament:
composer require sitesource/laravel-polymorphic-settings-filament
There is nothing to configure for Filament version support — the package detects the installed major (3.x, 4.x, or 5.x) at runtime. Your first use case should be a single settings page for one domain of configuration (e.g. "Commerce Settings") backed by dotted keys:
// Filament 3
public function form(Form $form): Form
{
return $form->schema([
TextInput::make('commerce.stripe.secret_key')->password(),
TextInput::make('commerce.stripe.publishable_key'),
])->statePath('data');
}
// Filament 4 / 5
public function form(Schema $schema): Schema
{
return $schema->components([
TextInput::make('commerce.stripe.secret_key')->password(),
TextInput::make('commerce.stripe.publishable_key'),
])->statePath('data');
}
Then register the page in your panel provider like any other Filament page. Look first at the base settings page class you extend from and at the two form() snippets above — those are the entire public surface you interact with. Mount loads current values from the settings store; save persists them. Don't write load/save logic yourself.
One page per settings domain. Use dotted keys as a poor-man's namespace (commerce.stripe.secret_key, mail.from_address, features.beta_enabled) and let each page own its prefix. This keeps the store flat-ish and makes it obvious which page a key belongs to.
Always keep ->statePath('data'). The package reads form state from under data; dropping or changing it breaks auto-load and auto-persist in ways that look like silent no-ops rather than errors.
Secrets via ->password(). Marking a field password() is what makes the value encrypt at rest — there's no separate "encrypted" flag to set. Pair it with ->revealable() if admins need to confirm a key matches without overwriting it.
Per-owner settings are the point. The "polymorphic" part means a settings page hangs off an owner model (Team, Tenant, User). Register the page against the owner in your panel and reuse the same page class across owners rather than duplicating classes per tenant type.
Read settings outside Filament from the store, not the page. Your application code should query the settings store directly (or a small typed accessor/service you wrap around it). Treating the Filament page as the read API will couple your domain code to Filament's request lifecycle.
Version-agnostic subclasses. If you maintain your own package or a multi-tenant base that must run on several Filament majors, either ship two subclasses or follow the package's own trick: reference version-specific symbols as strings and resolve them lazily, so static analysis doesn't choke on classes that only exist in the other installed major.
Testing. Filament's Livewire page tests (livewire(YourSettingsPage::class)->fillForm([...])->call('save')) cover mount-load and save-persist in one shot. Assert against the store, not the form state, or you'll pass tests that never persisted anything.
The Form vs Schema swap is the whole migration. Copying the Filament 3 snippet into a Filament 4/5 app produces a container type error at mount time, not at boot — you'll get a confusing failure the first time someone opens the page. Conversely, ->schema() on Filament 4/5 is a Filament-side deprecation, not a package concern.
The base class no longer declares form(). That means a subclass with a missing or misnamed form() fails with a generic undefined-method / discovery error instead of a helpful "you must implement this" message. If your page 500s on open, check the signature first.
Dotted keys means nested state. With statePath('data') and keys containing dots, the Livewire state is a nested array, not a flat map. Use data_get($this->data, 'commerce.stripe.secret_key') if you ever reach into state manually.
Encrypted values and APP_KEY. password() fields encrypted at rest are only readable with the key that wrote them. After a key rotation the field mounts blank — and saving the form without retyping the secret will persist an empty value over the old ciphertext. Require explicit re-entry when rotating keys, and consider a ->dehydrated(fn ($state) => filled($state)) guard on secret fields so a blank mount doesn't wipe the stored secret.
Duck-typed FormIntrospector. Field discovery works off whatever container Filament resolves. Components nested inside repeaters, builders, or custom wrappers are the likely place for introspection to miss a key; if a value doesn't round-trip, flatten that field to the top-level schema/components before assuming a storage bug.
Test across all three majors if you ship anything reusable. The package's own matrix is Filament 3.3.50 / 4.11.1 / 5.6.1 × PHP 8.3–8.5 × Laravel 12–13. Your subclass only gets that coverage if you add a Filament dimension to your CI, because a normal composer update pins exactly one major and hides the other two breakages.
PHP 8.5 is in the tested matrix — worth noting if your host app is still pinned to an older PHP floor, since the package's own support runs ahead of some Laravel apps.
How can I help you explore Laravel packages today?