qcod/laravel-app-settings
Add a settings manager with UI to your Laravel app. Settings are stored in the database and cached for zero-query performance, with configurable inputs, sections, and styling (Bootstrap 4 by default). Includes a facade for easy access.
1. Install and scaffold (5 minutes).
composer require qcod/laravel-app-settings
php artisan vendor:publish --provider="QCod\AppSettings\AppSettingsServiceProvider" --tag="config"
php artisan migrate
On Laravel 5.5+ the provider and the AppSettings facade are auto-discovered. On 5.4 or lower, add QCod\AppSettings\AppSettingsServiceProvider::class to providers and "AppSettings" => QCod\AppSettings\Facade::class to aliases.
2. Look here first, in this order:
config/app_settings.php — this file is the feature. Every settings screen, input, validation rule, and default lives here. It's also the file you'll diff most in code review.php artisan route:list | grep settings — the package registers a GET/POST route (default /settings). Confirm the URL and, more importantly, confirm who can reach it before you ship.vendor/qcod/laravel-app-settings/src — small package; the input-type → Blade partial mapping is the part you'll actually want to read when you outgrow the built-in types.3. First real use case: the app name and mail-from pair.
// config/app_settings.php
'sections' => [
'app' => [
'title' => 'General Settings',
'icon' => 'fa fa-cog',
'inputs' => [
[
'name' => 'app_name',
'type' => 'text',
'label' => 'App Name',
'rules' => 'required|min:2|max:20',
'value' => config('app.name'),
],
],
],
'email' => [
'title' => 'Email Settings',
'inputs' => [
['name' => 'from_email', 'type' => 'email', 'label' => 'From Email', 'rules' => 'required|email'],
['name' => 'from_name', 'type' => 'text', 'label' => 'From Name'],
],
],
],
Visit /settings, save, then read it anywhere:
setting('app_name', config('app.name'));
AppSettings::get('from_email');
That's the whole loop. Everything else is refinement.
Treat config as the schema, the DB as the data. The input definitions are the contract; the settings table is just storage. Keep the two in sync by always adding a new input when you add a new key, and by reading through setting() rather than querying the table.
Bridge settings into Laravel's own config at boot. Settings are not automatically applied to config('mail.from'). Do it explicitly in a service provider so the rest of the app keeps using the standard config API:
// AppServiceProvider::boot()
if (Schema::hasTable('settings')) { // guard against fresh installs / migrations
config([
'mail.from.address' => setting('from_email', config('mail.from.address')),
'mail.from.name' => setting('from_name', config('mail.from.name')),
'app.name' => setting('app_name', config('app.name')),
]);
}
Cache the resolved values if boot-time DB access becomes a problem — the package already caches reads, so this is usually fine.
Use data_type deliberately. Everything from the DB comes back as a string unless cast. 'data_type' => 'int' (or integer/number), 'boolean'/'bool', 'array', 'string' are supported. checkbox_group requires 'data_type' => 'array'. For anything else, use an accessor/mutator rather than sprinkling casts at call sites.
Hybrid mode: UI for some keys, raw key-value for the rest. You don't need a section entry to use setting()->set() / get() / has() / remove() / all($fresh). But when you mix both, you must set 'remove_abandoned_settings' => false, or saving the UI page will delete every key not declared in config.
Groups for per-user / per-tenant settings. Set a closure in config:
'setting_group' => fn() => 'user_'.auth()->id(),
or switch at runtime for a scoped block of work:
app('app-settings')->setStorageGroup('tenant_'.$tenant->id);
// ... reads/writes ...
app('app-settings')->setStorageGroup(null);
Wrap the runtime call in a try/finally if you're doing long-running work in a queue worker.
Dynamic select options and uploads use closures/mutators in config. Options can return Model::pluck('name','id')->toArray(). For image/file inputs, the package handles upload/replace automatically given disk and path; only reach for mutator when you need resizing, thumbnailing, or S3 key control.
Expose settings to JS once, in the layout.
<script>window.App = {!! json_encode(['settings' => setting()->all()]) !!};</script>
Don't call setting() from multiple partials — one payload, one source of truth, and keep it out of authenticated-only concerns.
Fit the UI into your app either by overriding CSS classes in config/app_settings.php (section_class, input_class, input_error_class, …) or by pointing 'setting_page_view' at your own Blade view that does @include('app_settings::_settings'). The second option is almost always the right one for anything beyond a Bootstrap 4 admin.
Protect the route in a service provider or route file — see gotchas. Day-one task, not a follow-up ticket.
The settings page is reachable by anyone who finds the URL. The package ships a route but no authorization. Put it behind auth/authorization immediately — middleware on the route group, or a Gate check. If your URL is guessable and your app has untrusted users, this is a full-config-write vulnerability.
Closures in config/app_settings.php vs. php artisan config:cache. Config caching serializes the config array; closures are not serializable. If you use closures for options, setting_group, or mutator, verify config:cache in your exact Laravel version before putting it in your deploy pipeline — and prefer resolving dynamic values to plain scalars at boot if it breaks. This is the single most likely "works locally, dies in production" trap with this package.
Booleans aren't booleans. A boolean input stores true_value/false_value, which default to the strings 'on' and 'off'. setting('maintenance_mode') returns 'off', which is truthy. Use 'data_type' => 'boolean' and be explicit about which side is which, or compare against the configured values.
Validation only runs on the UI save path. setting()->set('from_email', 'not-an-email') bypasses rules entirely. If programmatic writes matter, validate in your own code — the config rules are for humans using the form.
remove_abandoned_settings is destructive. Renaming an input key (or temporarily commenting out a section) with this enabled deletes the stored value on the next save. Renaming is a data migration, not a config edit. Set it to false whenever you're in hybrid mode.
Caching means stale reads after out-of-band edits. All values are cached to avoid SQL. If something writes to the settings table directly (a seeder, another package, a manual SQL fix), the app won't see it until the cache clears. setting()->all(true) forces fresh for that call; otherwise clear the cache store as part of the fix.
auth()->id() is null in console, queue, and scheduler contexts. A setting_group closure based on the authenticated user silently resolves to a different group there. Anything running outside a request needs an explicit setStorageGroup().
Testing. In feature tests, flush the cache between test cases (an array cache store resets per test, but a real store won't), and use RefreshDatabase. Assert what the form actually does: POST the settings route and check setting('key'), rather than asserting on the DB table.
Debugging a blank or unchanged value. Work outward: (1) is the key present in config/app_settings.php and spelled identically, (2) does a row exist in the settings table, (3) is it a stale cache, (4) is the storage group what you expect. In that order — it's almost always (1) or (4).
Extension points when the config runs out. The built-in types are text, number, email, textarea, select, boolean, checkbox, checkbox_group, image, file. Past that, the documented path is a custom input type (a Blade partial plus a config entry); when even that isn't enough, drop to qcod/laravel-settings directly for the key-value layer and build your own small screen. With ~339 stars and zero dependents, do a quick read of the source before leaning on edge-case behavior — the package is a thin UI wrapper, and you may end up owning a fork of a few views.
How can I help you explore Laravel packages today?