janiskelemen/laravel-setting
Laravel Setting gives your Laravel app a database-backed key-value store for configuration values. Read, write, and organize settings through a simple, expressive API, with support for typed values and caching so frequently used settings stay fast.
Install and publish
composer require janiskelemen/laravel-setting
php artisan vendor:publish --provider="JanisKelemen\Setting\SettingServiceProvider"
php artisan migrate
On Laravel 5.5+ the SettingServiceProvider and the Setting facade are auto-discovered. On older versions register them manually in config/app.php (JanisKelemen\Setting\SettingServiceProvider and the Setting alias). The publish step should give you config/setting.php (table name, cache behaviour, model binding) and a migration creating the settings table.
Where to look first
config/setting.php — table name, cache toggle, model class. This is the file you will actually touch.vendor/janiskelemen/laravel-setting/src/Setting.php — the model and the real method signatures. This package is small; reading one file answers most questions the README leaves vague.vendor/janiskelemen/laravel-setting/src/helpers.php — the global helper(s) and their exact argument order.First use case: an admin-editable settings page
The canonical use is "values a non-developer should be able to change without a deploy" — site name, support email, feature toggles, branding.
// read (with a safe fallback)
$name = setting('site.name', config('app.name'));
// write (bulk form)
setting([
'site.name' => $request->input('name'),
'site.support' => $request->input('support_email'),
'features.beta' => $request->boolean('beta'),
]);
That is genuinely the 80% case: a SettingsController with edit/update, a Blade form, and the helper doing the persistence. The package handles type casting so features.beta comes back as a boolean, not the string "1".
Pick one access style and stick to it. Three equivalent entry points exist: the global helper setting(), the Setting facade (Setting::get(...), Setting::set(...), Setting::has(...), Setting::forget(...)), and constructor injection of the model/repository. The helper is fine in Blade and controllers; the facade reads better in service classes; DI makes tests cleaner. Mixing all three in one codebase makes grepping for "where is this setting written?" painful.
Defaults belong at the call site, not in the database. Don't seed every key up front just so get() never returns null. Pass the default on read:
setting('mail.from', config('mail.from.address'))
This keeps config/ as the source of truth for infrastructure defaults and the DB as the override layer.
Read once, share everywhere. Settings are cached, but calling setting('x') in a loop or per-row in a Blade table still adds up. In a service provider or view composer:
View::composer('layouts.app', function ($view) {
$view->with('settings', setting()->all());
});
Grouping for larger apps. Keys can be namespaced (site.name, mail.host) and/or scoped by group. The pattern that scales: one group per domain (site, mail, billing), then setting()->group('mail')->all() to load a section for an admin tab. Verify the exact grouping signature in src/Setting.php before relying on dot-notation implying a group — some versions treat foo.bar as a literal key.
Admin update endpoint: whitelist, validate, write, forget nothing. The helper writes through the cache, so a normal update needs no manual invalidation:
$data = $request->validate([
'site.name' => ['required', 'string', 'max:120'],
'site.support' => ['required', 'email'],
'features.beta' => ['boolean'],
]);
foreach ($data as $key => $value) {
setting()->set($key, $value);
}
Only the whitelisted keys reach the DB — never setting($request->all()).
Seeding defaults. If you do want shipped defaults, guard them so re-running a seeder doesn't clobber operator changes:
foreach ($defaults as $key => $value) {
if (! setting()->has($key)) {
setting()->set($key, $value);
}
}
Localization. For multilingual site strings the package offers a localize() scope that stores per-locale values:
setting()->localize('de')->get('site.tagline');
setting()->localize('de')->set('site.tagline', 'Willkommen');
Use this for content strings, not for structural settings — otherwise every new setting multiplies by your locale count.
Testing. Use RefreshDatabase and set what you need in setUp(). If your suite shares a process across tests, call the cache flush in setUp() too, or a stale cached value from a previous test will leak:
protected function setUp(): void
{
parent::setUp();
setting()->flush();
}
The cache is the number one source of "it didn't save". Values are cached, so any write that bypasses the package — a raw SQL UPDATE, a seeder using the query builder, a second service writing to the same table — will not be reflected. After any out-of-band write, call setting()->flush() (or the equivalent cache-clear) and re-read. If a value looks stale in production, that's the first thing to check.
Keep settings out of config/ files. Config is evaluated during bootstrap, before you can reliably assume the DB is available (and it isn't on a fresh clone before migrate). Referencing setting() inside config/mail.php produces confusing failures during config:cache and in CI. Read settings in providers/services and pass them into config at runtime only if you must, guarded by a table-exists check.
Don't call it from migrations or early seeders. The settings table may not exist yet in the same migration run. Seeding settings belongs in a seeder executed after migration.
Typecasting is inferred, and inference can surprise you. Storing "false" (a string) and false (a boolean) round-trip differently; so do 1 and "1". When debugging a value that "comes back as a string", inspect the type column in the settings table — that's the source of truth for the cast. Pass real PHP types from your controller ($request->boolean(...), (int), arrays rather than json_encoded strings) and the casts take care of themselves.
Arrays and objects go through JSON. They work, but they land in a text column. Don't store large blobs, uploaded-file contents, or generated HTML here — store a path, an ID, or a reference. Settings rows should stay small enough to load in bulk on every request.
Uniqueness is per key (and group). Re-saving a key updates it; changing a key's group or renaming a key leaves the old row orphaned. There's no automatic pruning — write a small cleanup command if you renames keys often.
Multi-tenancy is not built in. There's no tenant column or scope. Options: prefix keys (tenant.42.site.name), use a group per tenant, or switch the DB connection in a middleware before the package reads. Key prefixing is the least invasive and the easiest to reason about.
Performance is fine if you read once. Expect roughly one query on a cold cache and zero afterwards. The failure mode is calling setting() inside a loop over 500 rows — hoist it.
Extension points worth knowing. The config lets you rebind the model class, so you can subclass the shipped Setting model and add scopes, accessors, or audit logging on saved. Publishing the migration means you can add columns (e.g. updated_by), but then you own future schema drift when upgrading the package — re-publish and diff after major upgrades. If the model fires Eloquent events, hooking saved/deleted is the cleanest place to attach audit trails or cache-busting for downstream consumers.
Upgrade hygiene. With a small, low-traffic package, the changelog is thin. Before bumping, diff the published config and migration against the vendor copies (php artisan vendor:publish --force into a scratch directory and compare). Also clear config:cache after any publish, or your app keeps reading the old config values.
How can I help you explore Laravel packages today?