outerweb/settings
A Laravel package for managing persistent application settings with typed values, groups, and caching. Define settings in code or the database, then read and write them anywhere via a simple, fluent helper API—ideal for configurable options like site preferences, feature flags, and integrations.
Install the package, publish the migration, and migrate:
composer require outerweb/settings
php artisan vendor:publish --tag="settings-migrations"
php artisan migrate
Publish the config if you need to change the table name or cache behavior:
php artisan vendor:publish --tag="settings-config"
The entire public API is the Settings facade:
use Outerweb\Settings\Facades\Settings;
Settings::set('site_name', 'Acme');
Settings::get('site_name'); // "Acme"
Settings::get('missing', 'fallback'); // "fallback"
Settings::has('site_name'); // true
Settings::forget('site_name');
Where to look first
config/settings.php — table name and caching options.group, key, value, type) because the type column drives casting.Outerweb\Settings\Settings (the class behind the facade) — the authoritative API surface for your installed version; the README lags releases.Outerweb\Settings\Models\Setting — see how values are cast on read/write before you store arrays or booleans.First real use case: a small "general settings" screen — site name, contact email, maintenance-mode toggle, and a JSON array of social links. Seed sensible defaults so get() never surprises you with null in production.
Seed defaults, don't rely on null checks
Create a SettingsSeeder that guards with has() so re-running doesn't overwrite user edits:
foreach ([
'general.site_name' => config('app.name'),
'general.support_email' => '[email protected]',
'general.maintenance' => false,
] as $key => $value) {
if (! Settings::has($key)) {
Settings::set($key, $value);
}
}
Use dot notation as namespaces
Treat the prefix as a module (general., mail., billing.). It keeps the settings table browsable and makes it trivial to build admin screens per group: fetch Settings::all(), Arr::groupBy on the prefix.
Write in one controller action, validate as a unit
$data = $request->validate([
'site_name' => ['required', 'string', 'max:120'],
'maintenance'=> ['boolean'],
'links' => ['array'],
]);
Settings::set('general.site_name', $data['site_name']);
Settings::set('general.maintenance', $request->boolean('maintenance'));
Settings::set('general.links', $data['links'] ?? []);
Pass real booleans and arrays — not "1" or a JSON string. The package infers the type from the value; giving it a string stores a string and you'll fight casts on read.
Wrap it in your own class
A thin wrapper makes the rest of the app testable and greppable:
final class AppSettings
{
public const SITE_NAME = 'general.site_name';
public const MAINTENANCE = 'general.maintenance';
public function siteName(): string
{
return Settings::get(self::SITE_NAME, config('app.name'));
}
public function maintenanceMode(): bool
{
return (bool) Settings::get(self::MAINTENANCE, false);
}
}
Inject AppSettings rather than calling the facade in controllers, jobs, and middleware. When the package (or its cache) misbehaves, you only have one place to patch, and you can fake it in tests.
Blade
Read settings in a view composer or share them from AppServiceProvider::boot(), not inside templates. The values are cached, but keeping Settings:: calls out of Blade keeps templates dumb and makes it obvious where new keys are introduced:
View::composer('layouts.app', fn ($view) => $view->with([
'siteName' => Settings::get('general.site_name', config('app.name')),
'maintenance' => Settings::get('general.maintenance', false),
]));
Middleware and feature flags
Resolve the flag once per request and stash it on the request or a scoped singleton. Do not re-read the same key in five middleware classes — even cached, it's noise, and it makes the request lifecycle harder to reason about.
Bulk imports
If you ever load settings from a file or another system directly into the DB, go through the facade or explicitly flush the package cache afterwards, otherwise reads keep serving old values until the TTL expires.
tenant_42.site_name) — you'll hit cache and cleanup problems.UPDATE settings SET value = ... in a console will not invalidate the cached read. Always mutate via Settings::set()/forget(), or clear the cache in the same deploy step.null vs missing. Settings::get('x') returning null can mean "row exists with null value" or "row absent." Use has() when the distinction matters (e.g., "has the admin ever configured this?").type column. A checkbox posting "on" or "0" will store a string unless you cast first. Normalize in the controller: $request->boolean(...), (int), (float).foo.bar is indistinguishable from group foo, key bar. Pick one convention (segmented keys only) and enforce it in your settings admin form.config/settings.php (especially the table name), run php artisan config:clear. A stale cached config pointing at the old table produces confusing "missing column" or "all settings are empty" behavior.vendor:publish --tag="settings-migrations" reports "no publishable resources," list tags with php artisan vendor:publish and check vendor/outerweb/settings/src/SettingsServiceProvider.php.RefreshDatabase and force the array cache driver (or clear the settings cache in setUp) so one test's Settings::set doesn't bleed into the next.settings table is plain and frequently dumped to staging. API keys belong in .env/a secrets manager, or at minimum behind an encrypted cast in your own wrapper.Setting model and/or bind your own implementation of the settings class in a service provider rather than patching vendor code. Keep that binding in one provider so upgrades are a single diff.Overall: excellent fit when you have a handful of global, human-edited configuration values that need to change without a deploy. Poor fit for anything per-entity, high-volume, or secret — recognize that boundary early and you'll avoid most of the gotchas above.
How can I help you explore Laravel packages today?