nhwin/settings
A Laravel package for database-backed application settings. It offers a simple API to get, set, and manage grouped configuration values at runtime, with caching support for fast access and a clean way to avoid hardcoding editable options.
v2.0.0 is a major release, but the notes you have are a link-only changelog — no migration guide, no rename list. Treat the upgrade as a discovery task before you treat it as an install task.
Install / upgrade
composer require nhwin304/settings:^2.0
php artisan vendor:publish --provider="Nhwin304\Settings\SettingsServiceProvider" --tag=config
php artisan vendor:publish --provider="Nhwin304\Settings\SettingsServiceProvider" --tag=migrations
php artisan migrate
If you're coming from 1.1.2, pin the old version in a branch first and diff the published artefacts:
git diff v1.1.2..v2.0.0 -- config/ database/migrations/ src/
That diff is the real upgrade documentation. Look for: renamed config keys, changed default table name, changed cache key prefix, changed method signatures on the repository/facade, and any new migration that alters an existing column.
Look here first, in this order
config/settings.php — table name, cache store, cache TTL, encryption flag, casts map, any multi-tenant/scope key.text column? is key unique? is there a group column?).get, set, has, forget, all, maybe group/scope).First use case — app-wide feature flags and editable site config, read on every request, written rarely:
use Nhwin304\Settings\Facades\Settings;
// somewhere in a service provider or seeder
Settings::set('billing.trial_days', 14);
Settings::set('features.new_checkout', false);
// in a controller / middleware
if (Settings::get('features.new_checkout')) {
// ...
}
Do this in a throwaway branch against a copy of your real database before migrating production. v1.x data may need a backfill if the column shape changed.
Cache-first reads behind a helper. Wrap the package behind your own thin class so the rest of the app never touches the facade directly. This is the single highest-value move when you're on a v2 with an undocumented changelog — it localises the blast radius of any signature change to one file.
final class AppSettings
{
public function get(string $key, mixed $default = null): mixed
{
return Settings::get($key, $default);
}
public function flag(string $key): bool
{
return (bool) $this->get("features.{$key}", false);
}
}
Namespace your keys from day one. A flat key column without grouping gets unusable fast. Adopt a dotted convention (billing.trial_days, mail.from_name, features.x) and, if the package supports groups/scopes, map the first segment to it so you can flush a namespace independently.
Type coercion belongs in config, not in your controllers. If the package exposes a casts map, register booleans, integers, JSON arrays, and dates there:
'casts' => [
'features.*' => 'bool',
'billing.*' => 'int',
'integrations.*' => 'array',
],
If it doesn't, cast at your AppSettings boundary — never in Blade.
Form-driven admin screen. The pattern that pays off is: validate with a FormRequest, then write all fields in one pass and invalidate cache once.
public function update(SettingsRequest $request)
{
DB::transaction(fn () => collect($request->validated())
->each(fn ($value, $key) => Settings::set($key, $value)));
return back();
}
Guard the write path with Gate/policy — settings are usually global-write, per-role-read, and a missing authorization check is the most common way this package gets abused.
Defaults as seed data, not code. Create a SettingsSeeder (or a settings entry in your deploy script) that uses firstOrCreate-style semantics so new installs and existing installs converge. This is also where you record what changed in v2 — if a key was renamed, the seeder is the migration.
Multi-tenant scoping. If v2 adds or changes scoping, resolve the tenant from the request/queue context and never from a global singleton — queued jobs and Octane workers will leak tenant state across requests.
Testing. RefreshDatabase for the table, plus an explicit cache flush in setUp/tearDown; a stale cache store between tests is the classic source of "works alone, fails in the suite".
UPGRADING.md/README diff and the src/ diff, before touching production. Assume at least one silent behaviour change (default value handling, cast behaviour, cache TTL default).php artisan config:cache is the #1 breakage. Any env() call inside the package's config file returns null once cached. Verify your published config has hardcoded defaults and move environment-driven values into AppServiceProvider.array or file store works in development and silently fails to invalidate across multiple app servers. Use Redis/memcached/database, and confirm the cache key prefix changed (or didn't) in v2 — a stale prefix from v1 will serve old values after deploy.php artisan cache:clear (or the package's own flush command, if one exists) to your release script, immediately after migrate.null vs. missing. Decide and document: does get('missing') return your default, or null? Wrapping in AppSettings makes this consistent regardless of what v2 changed.'0'/'1'/'false' strings. Without casts, (bool) 'false' is true. Always cast explicitly.utf8mb4; encrypted values need enough column width (and if v2 changed the cipher or encoding, existing rows may not decrypt — test on a database copy).Settings::get() directly in tinker, (2) query the table, (3) check the cache store and key prefix, (4) confirm config:cache isn't pinning an old config. That order isolates it in under a minute.How can I help you explore Laravel packages today?