ruangdeveloper/laravel-settings
A lightweight Laravel package for managing application settings stored in the database. Easily get and set configuration values with helper functions, caching support, and a simple API for organizing settings by group or key.
What this package is: ruangdeveloper/laravel-settings is a database-backed key/value settings store for Laravel — an alternative to sprinkling config() files or a hand-rolled settings table. Values live in a table, are read through a facade/helper, and (in the v2 line) go through a cache layer so reads don't hit the DB on every request.
Install (3 commands):
composer require ruangdeveloper/laravel-settings
php artisan vendor:publish --provider="RuangDeveloper\LaravelSettings\LaravelSettingsServiceProvider"
php artisan migrate
If the provider isn't auto-discovered or the tag differs, check the package README for the exact publish tag — publish tags are the single most common source of "the table doesn't exist" errors with settings packages.
Where to look first, in this order:
config/laravel-settings.php (or similarly named) — table name, cache store, cache TTL, cache key prefix, and the model class binding. This is where 90% of production surprises come from.value is text/json and whether there is a unique index on key. Multi-tenancy or grouping is not included out of the box; you'll add it yourself.First use case that pays for itself immediately: user-editable runtime settings for an admin panel — site name, support email, feature flags, maintenance-mode toggles, third-party API keys (encrypted at rest via a custom cast, not by the package). Anything an ops person should be able to change without a deploy is a good fit; anything that changes per-request is not.
Setting::set('site.name', 'Acme');
Setting::get('site.name', 'Default');
// or the helper, depending on version
setting('site.name');
Version note for v2.3.0: the release is a single compatibility PR adding Laravel 13 support. There are no breaking changes, no new features, no deprecations — the upgrade is composer update ruangdeveloper/laravel-settings and re-run your test suite. The only thing worth verifying is the illuminate/* constraint in composer.json: when maintainers "add" a framework version they sometimes bump the minimum as a side effect. If you're still on Laravel 11 or 12, confirm ^2.3 still resolves before bumping, and pin "ruangdeveloper/laravel-settings": "^2.3" if you intend to move to Laravel 13.
Wrap it in your own repository, don't call the facade from controllers. The package API is a thin key/value layer; your domain isn't. A single App\Settings\SiteSettings class that exposes typed properties (public string $siteName, public bool $maintenanceMode) means you can swap the storage engine later, and it gives you one place to add validation, defaults, and casts.
final class SiteSettings
{
public function __construct(private SettingStore $store) {}
public function siteName(): string
{
return (string) $this->store->get('site.name', config('app.name'));
}
public function maintenanceMode(): bool
{
return (bool) $this->store->get('site.maintenance', false);
}
}
Namespace your keys. Adopt a dotted convention (billing.currency, mail.from_address, features.new_checkout) from day one. The package stores keys as flat strings; a prefix convention is what makes a later export, audit, or "reset group" feature tractable.
Seed defaults idempotently, in a seeder — not in a migration. Migrations run once; settings should be re-assertable. Use firstOrCreate-style semantics so re-running the seeder on an existing environment doesn't clobber values an admin has already edited:
foreach ($defaults as $key => $value) {
Setting::has($key) || Setting::set($key, $value);
}
Cache-invalidate at the write, and again at deploy. The package's caching is the reason reads are cheap. That means every write path must go through the package (or your repository) — a direct DB::table('settings')->update(...) in a hotfix script will leave stale values in Redis until TTL expiry, which can be hours. If you must write raw, follow it with the package's cache-clear call (or Cache::forget() on the package's key prefix). Add a cache clear to your deploy script as a belt-and-braces step.
Prefer a typed cast over stringly-typed values. Booleans stored as "0"/"1" and arrays stored as JSON both round-trip badly when a human edits the row in a DB client. Either use the package's type/cast option if it exposes one, or normalize in your repository: JSON-encode on write, json_decode(..., true) on read, and treat null as "not set" rather than false.
Read-through config fallback is the pattern that keeps you honest: settings should override a sane default that lives in config/, never be the only source of a value. That way a fresh clone, a broken settings table, or an empty cache still boots.
For tests: the settings table is real state. Use RefreshDatabase, and set the package's cache store to array (or null) in phpunit.xml — otherwise a cached maintenance_mode = true leaks from one test into the next and you spend an afternoon on a phantom 503.
In Blade / Inertia, resolve settings once per request (a view composer or a shared Inertia prop) rather than calling the facade repeatedly inside loops.
array and file stores will silently diverge; array in production is a bug, not a performance choice.config:cache does not help you here. Settings are runtime DB state, not configuration. Don't try to bake them into config files for "speed" — you'll reintroduce deploys for things that were supposed to be runtime-editable, and you'll break config:cache ordering.composer why-not ruangdeveloper/laravel-settings 2.3.0 — if a transitive illuminate/support pin is holding you back, that's a Composer problem, not a package problem.key turns set() into silent duplicate rows and makes get() non-deterministic. Check with SHOW INDEX FROM settings after upgrading.tenant_id (or group) column, override the model binding in config, and add a global scope. Budget for this; don't discover it in month three.text column are readable by anyone with DB access, and by every php artisan db:dump in your CI logs. Add an encrypted cast on the model (or store a reference to your secrets manager) before someone commits a production dump.vendor/, stop.SELECT * FROM settings WHERE key = ...), (2) what cache store is active (php artisan tinker → Cache::getStore()), (3) is a config fallback masking a null read. That's 95% of reported issues with this class of package.How can I help you explore Laravel packages today?