outerweb/filament-settings
Outerweb Filament Settings adds a settings page to your Filament admin panel where you define typed settings classes, edit values with familiar form fields, and save them to the database. Includes caching and validation.
composer require outerweb/filament-settings
php artisan vendor:publish --tag=settings-migrations # check README for exact tag
php artisan migrate
The package is a thin Filament layer over outerweb/settings. Two things matter:
app/Settings/) that extends the package's base Settings class and declares its fields using ordinary Filament form components. These components are the schema — the same TextInput/Toggle/FileUpload you already use in resources.SettingsPage, which renders those classes in tabs/sections and wires up the save action.Minimal first use case — a "General" settings tab for site name, logo, and a maintenance toggle:
// app/Settings/General.php
namespace App\Settings;
use Filament\Forms\Components\{TextInput, Toggle, FileUpload};
use Outerweb\Settings\Settings;
class General extends Settings
{
public static function fields(): array
{
return [
TextInput::make('site_name')->required(),
FileUpload::make('logo')->image()->directory('branding'),
Toggle::make('maintenance_mode'),
];
}
}
// app/Filament/Pages/ManageSettings.php
namespace App\Filament\Pages;
use App\Settings\General;
use Outerweb\FilamentSettings\Pages\SettingsPage;
class ManageSettings extends SettingsPage
{
protected static ?string $navigationIcon = 'heroicon-o-cog-6-tooth';
public function getSettings(): array
{
return [General::class];
}
}
Look first at: the published migration (to confirm the settings table shape and whether it's polymorphic), the base Settings class in vendor/outerweb/settings/src, and the package README's example page. Those three tell you everything the docs gloss over. Then read a value back in a Blade view or service with General::get('site_name') — that round trip is the whole point of the package.
One class per logical group. Don't build a 40-field monolith. Split into General, Mail, Social, Seo, and let the page render them as tabs. Each class is independently readable and testable, and the tab labels come from the class (usually a $title/getTitle() or the navigation label — check the base class).
Reading settings. The common pattern is static access from anywhere:
General::get('site_name', config('app.name')); // with fallback
General::set('maintenance_mode', true); // programmatic write
Use it in Blade ({{ General::get('site_name') }}), in a view composer, or in a View::share() call in a service provider — not directly inside config/*.php or a migration, both of which run before/without a database.
Model-scoped settings. The underlying settings package is polymorphic, so a model can hold its own settings via the HasSettings trait. This is the right escape hatch for per-user or per-team preferences; panel-wide settings should stay global. With Filament tenancy, decide explicitly: is the settings page global, or should each tenant get its own row?
Conditional field visibility. Because fields are Filament components, ->visible(fn (Get $get) => $get('enabled')) and ->live() work exactly as in resources. This is the main reason to prefer this package over hand-rolling a settings form: you get Filament's whole form API for free.
Custom save hooks. When a setting has side effects (logo replacement, cache warm-up, queue restart), override the page's save method or listen for the model's saved event rather than stuffing logic into the field definition. Keep the fields declarative.
Authorization. Override canView()/canAccess() on the page so only admins see ManageSettings. These settings are app-wide; leaking the page to all users is a common mistake.
"1"/"0", arrays need explicit encoding. Define casts on the settings class (or ->boolean()-style handling in the component) and verify with tinker — General::get('maintenance_mode') returning "0" and being truthy is the classic bug.Storage::disk('public')->url(...) at render time. Replacing an upload doesn't delete the old file unless you handle it.::get() call in a service provider, middleware, or Blade layout will explode on a fresh clone before migrate. Either default aggressively or guard with Schema::hasTable('settings'). Tests need RefreshDatabase plus the published migration.::set() outside the Filament page (a console command, an importer), clear the cache or the panel keeps serving stale values until the next save. Check whether the base class exposes a flushCache()/clearCache().->default(...) so a missing row renders sensibly instead of an empty required input that blocks the first save.mail.host in one class and a nested structure in another can confuse lookups. Flat snake_case names per class are safest.settings table directly to see raw stored values vs. what the form renders — most "it didn't save" reports are a cast mismatch or a validation rule on a hidden field. Log::info($getState()) inside the save is faster than guessing.Settings base class: replace its get/set with your own storage or swap to spatie/laravel-settings for typed, cast-aware settings classes while keeping the Filament page.How can I help you explore Laravel packages today?