arcanedev/laravel-settings
Laravel Settings provides a simple, persistent way to manage application settings in Laravel. Store and retrieve settings with database or config drivers, cache support, and an easy-to-use API for global configuration values.
composer require arcanedev/laravel-settings
php artisan vendor:publish --provider="Arcanedev\Settings\SettingsServiceProvider" --tag="laravel-settings:config"
php artisan vendor:publish --provider="Arcanedev\Settings\SettingsServiceProvider" --tag="laravel-settings:migrations"
php artisan migrate
The service provider is auto-discovered on modern Laravel; only older skeletons need the config/app.php entry. Publishing without --tag dumps every stub — always use the tags so you don't overwrite the migration on the next publish.
Where to look first, in this order:
database, json, redis, plus an in-memory driver used for tests) and the table/path/hash it writes to.vendor/arcanedev/laravel-settings/src/Repositories/ — read the abstract repository and the class for your driver. It is only a few dozen lines each and it answers the one question that matters before you build anything on top: does set() write immediately, or does it stage in memory until an explicit save()?Arcanedev\Settings\Facades\Settings (a setting() helper is also registered).First use case — the one this package exists for: values a non-developer needs to change without a deploy.
use Arcanedev\Settings\Facades\Settings;
Settings::get('site.name', config('app.name')); // read with fallback
Settings::set('site.name', 'Acme'); // write
Settings::has('site.name');
Settings::forget('site.name');
Settings::all(); // array of everything
If the value belongs in version control and changes with a code review, it belongs in config/. If it changes because marketing asked, it belongs here.
Always supply a default, and make the default the config() value. This lets a fresh install boot before anything is stored, and gives you a migration path from env-driven config to DB-driven settings.
$from = Settings::get('mail.from.address', config('mail.from.address'));
Group related keys with dot notation and write the group once.
Settings::set([
'site.name' => $request->input('site_name'),
'site.tagline' => $request->input('tagline'),
'site.locale' => $request->input('locale'),
]);
Reading Settings::get('site.tagline') after storing a nested array works, but be consistent: either always write leaves (site.tagline) or always write the parent array (site). Mixing the two is how you get silent overwrites.
Admin update flow — treat it like any other write endpoint:
public function update(UpdateSiteSettingsRequest $request)
{
$data = $request->validated(); // FormRequest whitelist, not $request->all()
DB::transaction(fn () => Settings::set($data));
Cache::forget('app.settings');
return back()->with('status', 'Settings saved.');
}
Never map request input straight into Settings::set(); the package has no schema and will happily persist any key a user posts.
Wrap reads in a tiny cached accessor. The repository is loaded on first access, but the moment settings appear in a Blade layout or a loop you want one read per request (or per cache window), plus a place to put type coercion:
final class AppSettings
{
public function all(): array
{
return Cache::rememberForever('app.settings', fn () => Settings::all());
}
public function bool(string $key, bool $default = false): bool
{
return filter_var($this->all()[$key] ?? $default, FILTER_VALIDATE_BOOL);
}
}
Inject AppSettings rather than the facade so you can swap the implementation later (see Gotchas).
Bridge to config() at boot when a third-party package only reads config. In a service provider's boot():
config(['services.mailgun.domain' => Settings::get('mailgun.domain', config('services.mailgun.domain'))]);
This is request-time only and is not baked into config:cache — which is exactly what you want, but also means it must run on every request (including queue workers).
Share with views once: a view composer that injects AppSettings::all() into your layout beats calling setting() from partials.
Testing: point the driver at the in-memory/array implementation in phpunit.xml so tests don't need a settings table; use the database driver plus RefreshDatabase only in the tests that specifically cover persistence.
Multi-tenant: the package has no scoping. Prefix keys (tenant.{id}.site.name) or use a per-tenant connection in the database driver config; don't rely on separate Settings instances.
set() without save() survives the request but never hits storage — and nothing errors. Write one tinker test (Settings::set(...), then Settings::all() in a fresh process) on day one.value column. Booleans and integers frequently come back as "1" / "0". Cast at the boundary ((bool), (int), filter_var), never inside Blade.flush() is a loaded gun. It wipes every key for the configured driver. Don't expose it in an admin route, and if you must, gate it behind app()->isLocal() plus a confirmation step.storage/app, breaks on read-only filesystems (Vapor, some container images), and read-modify-write on the whole file races under concurrency. Fine for a single VPS; wrong for a load-balanced app.php artisan cache:clear will not clear it, and it inherits your Redis persistence settings. Know whether you've configured Redis as cache or as store — flushing the wrong one hurts.register(), and guard boot-time reads on fresh installs. On the very first deploy (or in CI before migrate) the table may not exist and the provider throws. Wrap the read in a Schema::hasTable() check or a try/catch that falls back to config.config:cache deploy won't reveal a broken driver. Smoke-test the settings screen after deploy.rememberForever layer, invalidate on write, and remember queue:restart after changing anything that isn't cache-backed.mail as an array after mail.from exists (or vice versa) silently drops keys. Pick leaf-level or group-level writes and stick to it.extend(). Useful if you outgrow a driver — but the simpler escape hatch is a thin AppSettings interface in your own code, which also lets you migrate to spatie/laravel-settings (typed settings classes, validation, casts) later without touching call sites.composer.json, keep the facade calls confined to one wrapper class, and treat the repository source as something you may need to fork. For anything long-lived and business-critical, weigh spatie/laravel-settings or a plain settings table with a model — the amount of code you'd write is small, and the package's main value is the driver abstraction, not the API.Settings::all() in php artisan tinker tells you instantly whether the problem is storage, caching, or your admin form. Check storage/logs/laravel.log for driver-level exceptions (permissions, connection, missing table) — the facade call itself rarely throws a useful message.How can I help you explore Laravel packages today?