rudnev/laravel-settings
A Laravel package for storing and managing application settings in the database. It provides a simple API to read and write settings at runtime, making it easy to add editable configuration options without changing env files.
The package is a key/value settings store backed by a database table, with a facade (Settings) and helper wrapper over get/set. Install, migrate, then read/write. Nothing else is required.
Install and publish.
composer require rudnev/laravel-settings
php artisan vendor:publish --provider="Rudnev\Settings\SettingsServiceProvider"
Auto-discovery registers the provider; if you are on Laravel 11 you do not touch config/app.php. On older apps, add the provider manually.
Run the bundled migration.
php artisan migrate
This creates the settings table. If you renamed the table in the published config, edit it before migrating — otherwise you get two tables and a confusing empty read.
Smoke test the round-trip.
php artisan tinker
>>> Settings::set('site.name', 'Acme');
>>> Settings::get('site.name');
If set returns and get returns Acme, discovery, config merge, the migration, and the cache layer are all working.
Where to look first. The published config file — it controls the table name and the cache store, and those two settings account for most "it works locally but not in staging" reports.
First real use case. Store site-wide, operator-editable values that currently live in config/ but change too often to redeploy for: site name, support email, feature toggles, banner text. The pattern is:
$siteName = Settings::get('site.name', config('app.name'));
The second argument is the fallback used when the key is absent — always supply one for anything user-facing, or your first deploy renders null in the header.
Key namespacing. There is no grouping in the schema, so discipline has to come from the keys. Adopt a dotted namespace up front (mail.from, billing.currency, features.new_checkout) and never use bare keys. This keeps an admin screen, a grep, and a where key like 'mail.%' query all tractable later.
Reads are cheap, writes are not. get hits the cache; set writes the row and invalidates. So read freely in views and controllers, but treat set as an administrative operation — do it from a settings controller or an artisan command, not inside a request loop.
Type-aware values work by round-trip, so pass the real type. Store booleans and arrays as booleans and arrays, not JSON strings:
Settings::set('features.new_checkout', true); // bool
Settings::set('ui.allowed_locales', ['en','fr']); // array
Retrieving returns the same type. Do not json_encode before storing — you will get a string back and if (Settings::get('features.new_checkout')) will be truthy for the literal string "false".
Centralize access behind a small class. Wrap the facade in your own AppSettings service with typed methods, and inject that:
class AppSettings
{
public function newCheckoutEnabled(): bool
{
return (bool) Settings::get('features.new_checkout', false);
}
}
This gives you one place to change key names, one place to add memoization, and something you can swap() in tests without stubbing the vendor facade.
Admin screen pattern. A single controller with an explicit whitelist of keys is the safe shape — a free-form key editor is effectively a write-anything endpoint:
$allowed = ['site.name', 'mail.from', 'features.new_checkout'];
foreach ($allowed as $key) {
Settings::set($key, $request->validated($key));
}
Render the form by looping the same array and calling Settings::get($key, $default) — one source of truth for what is editable.
Seeding and defaults. For non-nullable behaviour, seed defaults in a seeder rather than relying on fallback arguments everywhere. Fallbacks scattered across the codebase drift; a seeded row is a single authoritative value. Seed idempotently with a check on existing keys so re-running does not clobber operator edits.
Testing. In tests use RefreshDatabase so the settings table is empty and your fallbacks are exercised, and set cache.default to array in phpunit.xml so cache invalidation is per-test. If you assert on values set by a test, call Settings::set inside the test, not in setUpBeforeClass — class-level setup runs before the database transaction rolls back.
Deploy integration. Because reads are cached, a settings change in one environment does not propagate to another. When promoting a change between environments, promote it as a seed/migration step, not as a manual admin edit, so it is reproducible.
Never read settings from a service provider's register(). The settings table may not exist yet — during php artisan migrate on a fresh clone, and during composer install in CI — and you get a Base table or view not found that points at an unrelated package. Read from boot() guards, middleware, or controllers instead, or wrap the read in a try/catch returning the config default.
Cache store must be shared across processes. If the cache driver is array (common in local .env) invalidation is per-process and looks fine in a single request, then fails silently once a queue worker or a second PHP-FPM pool serves traffic. Pin the package's configured cache store to redis/memcached/database, and treat array as test-only.
Boolean round-trips from form input. HTML checkboxes submit "1" — a string. "1" and true are different types in the store. Cast before writing ($request->boolean('features.new_checkout')) or your admin UI will flip between true and "1" on each save.
Config cache hides your changes. After editing the published config, run php artisan config:clear && php artisan cache:clear. In production with config:cache active, a stale table name or cache store is the first suspect for "the setting saved but the page did not change".
Do not store secrets. Values are plaintext in the database and plaintext in the cache. API keys, SMTP passwords, and tokens belong in .env/a secrets manager. Settings is for operator-editable configuration, not credentials.
Table name changes need a migration. Editing table in the config after you have migrated does not move the data — the package simply starts reading an empty table. Treat the table name as immutable once deployed, or write a data-copy migration.
Cache staleness by direct DB edits. Any UPDATE run by hand in SQL bypasses the package's invalidation. If you must patch a value directly (e.g. during an incident), follow it with a cache clear or re-save through Settings::set.
Extension point: the cache store, not the class. If you need custom behaviour, point the package at a dedicated cache store (or a tagged Redis connection) via config rather than subclassing the repository — subclassing is what breaks on minor releases. For everything else, wrap the facade in your own service and let the vendor class stay concrete.
Version and support lag. The 2.4.0 release is a Laravel 11 compatibility bump. Before adopting it, diff the tag against 2.3.x: if the diff is constraint-only, the API surface is unchanged and you can adopt with confidence; if it touches the repository or cache code, run your settings-specific tests (round-trip, type round-trip, invalidation) before merging.
How can I help you explore Laravel packages today?