wg-vn / spatie-laravel-settings-ui
A plain Blade admin UI for Spatie's Laravel Settings. No Filament, no Livewire, no build step.
Package info
github.com/wg-vn/spatie-laravel-settings-ui
pkg:composer/wg-vn/spatie-laravel-settings-ui
Requires
- php: ^8.4
- illuminate/contracts: ^13.32.0
- spatie/laravel-settings: ^3.0
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
A plain Blade admin UI for Spatie's Laravel Settings.
A port of
filament/spatie-laravel-settings-pluginwith no Filament, Livewire or build step. It assumes you already usespatie/laravel-settings. It edits your settings classes and does not replace them.
Features
- A page for every settings class, at
/settings-ui, with no configuration - Forms guessed from property types: strings, booleans, numbers, enums, dates, arrays
- Custom pages with your own fields, access rules and save hooks
- Encrypted and password properties are never sent to the browser
- Locked properties shown read-only, since Spatie skips them on save
- Authentication, a gate and user/role allow-lists, on by default, and closed outside
localuntil you choose who gets in - Light and dark themes, one stylesheet served by the package
Requirements
- PHP >= 8.4
- Laravel 13
- spatie/laravel-settings ^3.0, with its
settingstable migrated
Installation
- Install the package
composer require wg-vn/spatie-laravel-settings-ui
- Create your settings as usual with Spatie: a settings class in
app/Settingsand a settings migration. See the Spatie documentation. - Visit the UI
Every settings class that Spatie registers or discovers (/settings-uisettings.settingsandsettings.auto_discover_settings) gets a page. Any signed-in user can open it locally. Before deploying, choose who gets in.
Optional: publish the resources.
php artisan vendor:publish --tag="settings-ui-config" # config/settings-ui.php php artisan vendor:publish --tag="settings-ui-views" # resources/views/vendor/settings-ui php artisan vendor:publish --tag="settings-ui-translations" # lang/vendor/settings-ui
Generated forms
A page with no custom fields guesses its form from the public properties of the settings class:
| Property | Field |
|---|---|
bool |
toggle |
int, float |
number (int also enforces whole numbers) |
| enum | select, with one option per case |
DateTimeInterface (and Carbon) |
date and time |
array |
JSON editor |
encrypted, or a name containing password |
password |
name containing email / phone, tel / url |
email / tel / url |
| anything else | text |
A property that is not nullable is required. Submitted values are converted back to the declared type (int, float, bool, enum, DateTime class, or a spatie/laravel-data object) before saving.
Password fields never show the stored value. Leaving one blank keeps it.
Custom pages
Create a page class for a settings class:
php artisan make:settings-ui-page ManageGeneral GeneralSettings
Add --generate to write out the guessed fields, so you can edit them. Pages in app/SettingsUi are discovered automatically. A page replaces the generated one for its settings class.
namespace App\SettingsUi; use App\Enums\Theme; use App\Settings\GeneralSettings; use WgVn\SettingsUi\Field; use WgVn\SettingsUi\SettingsPage; class ManageGeneral extends SettingsPage { protected string $settings = GeneralSettings::class; protected ?string $title = 'General'; // defaults to the settings class name protected ?string $slug = 'general'; // defaults to the settings group protected int $sort = 0; // navigation order, then title public function fields(): array { return [ Field::text('site_name')->label('Site name')->required(), Field::email('support_email')->help('Shown in the footer.'), Field::toggle('maintenance_mode'), Field::number('max_uploads')->integer()->min(1)->max(100), Field::select('theme')->options(Theme::class), Field::select('region')->options(['eu' => 'Europe', 'us' => 'United States']), Field::textarea('footer_text')->rules(['max:500']), Field::password('api_key'), Field::json('social_links'), Field::tags('alert_emails')->itemType('email'), ]; } }
The name of each field must match a property of the settings class. Available fields are text, email, password, url, tel, number, textarea, toggle, select, date, datetime, color, json and tags. Each field takes label(), help(), placeholder(), required(), disabled() and rules(). number also takes integer(), min(), max() and step(). Enum options use a getLabel() method when the enum has one. Email fields require a dotted domain, so user@localhost is rejected.
tags edits an array property as a list of removable chips. Enter, a comma or leaving the input adds what was typed, and a pasted list becomes one chip per item. Duplicates and blanks are dropped on save. itemType('email') or itemType('url') validates each item, marking invalid ones as they are added and rejecting them on save. required() means at least one item.
Authorization
public function canAccess(): bool { return auth()->user()->isAdmin(); } public function canEdit(): bool { return auth()->user()->isAdmin(); }
A page that fails canAccess() is hidden from navigation and returns 403. A page that fails canEdit() shows a disabled form and refuses saves.
canEdit()only gates saving. It does not stop a user from reading every value on the page. Restrict sensitive settings withcanAccess()instead.
Hooks
These work as in the Filament plugin:
protected function mutateFormDataBeforeFill(array $data): array { return $data; } protected function mutateFormDataBeforeSave(array $data): array { return $data; } protected function beforeSave(): void {} // also beforeFill, afterFill, protected function afterSave(): void {} // beforeValidate, afterValidate public function getSavedNotificationTitle(): ?string { return 'Settings updated'; } public function getRedirectUrl(): ?string { return null; // back to the page }
Saving runs in a database transaction, so throwing from a hook rolls it back.
Configuration
return [ 'route' => [ 'prefix' => 'settings-ui', 'middleware' => null, // replaces ['web']; auth is always appended 'domain' => null, ], 'authorization' => [ 'enabled' => true, // false => the UI is fully public 'gate' => 'viewSettingsUi', // default: signed-in users, `local` only 'guard' => null, ], 'access' => [ 'allowed_users' => [], // user email allow-list 'allowed_roles' => [], // role names (Spatie Permission, etc.) ], 'pages' => [ 'classes' => [], // custom pages to register explicitly 'discover' => app_path('SettingsUi'), 'auto' => true, // generate pages for the rest 'exclude' => [], // settings classes that get no page ], 'ui' => [ 'brand' => 'Settings', 'logo' => null, 'back_url' => null, // header link, e.g. to your admin panel ], ];
Authorization & access control
Access is decided in this order:
authorization.enabled(defaulttrue, orSETTINGS_UI_AUTHORIZATION) requires a logged-in user, then the gate.access.allowed_users/access.allowed_roles: if either is set, it is enforced regardless of step 1.- With authorization off and both lists empty, the UI is fully public, and anyone who can reach the URL can change your settings. Use that only for local development.
Access outside local
The UI can change every setting in your application, so outside the local environment it denies everyone until you choose who gets in. Signed-in users see a 403 that explains this. Do one of the following:
- Set an allow-list in
config/settings-ui.php:'access' => [ 'allowed_users' => ['admin@example.com'], 'allowed_roles' => ['admin'], ],
- Or define the gate yourself, for example in
AppServiceProvider::boot():Gate::define('viewSettingsUi', fn ($user) => $user->is_admin);
In the local environment, any signed-in user gets in.
Notes:
- Gate: your own
viewSettingsUigate replaces the default entirely. The allow-lists still apply on top of it. - Roles use
hasAnyRole()orhasRole()on your user model. Without either method, a user cannot match a role and is denied. route.middlewarereplaces the base stack (['web']) only. Authentication and the access checks are appended afterwards and cannot be removed.- Guests are redirected to your
loginroute (or whereverredirectGuestsTo()points). Without one, and for JSON requests, they get a401.
Translations
The save button and the saved message come in every language the Filament plugin shipped. The rest of the interface is in English. Publish the translations to add or change any of them.
License
The MIT License (MIT). See LICENSE for details. Based on filament/spatie-laravel-settings-plugin, copyright (c) Filament.
