pharaonic/laravel-settings
A Laravel package for managing application settings through a simple, flexible API. Persist configuration values in the database, retrieve them easily at runtime, and organize settings cleanly for small apps or larger projects that need centralized control.
Pin and confirm the API before you build on it. The package had a single tagged release in December 2022. Treat the README as the source of truth, not your memory of a similar package. Before writing code:
composer require pharaonic/laravel-settings in a branch you can throw away.php artisan vendor:publish --provider="Pharaonic\Laravel\Settings\SettingsServiceProvider" — you want the config/ file and the migration. If the provider class name differs, check composer.json under extra.laravel.providers for the exact FQCN.text/json, is there a model_type/model_id pair for per-model settings, is there a unique index on key?vendor/pharaonic/laravel-settings/src and read the model and any helper/facade. The public surface is small — a get, a set/put, maybe an all/forget. You need to know the exact method names and whether the helper is a function, a facade, or both.Where to look, in order: published config/settings.php → SettingsServiceProvider::register()/boot() (what binds, what merges, what publishes, what caches) → the Setting model (casts, table name, mass-assignment) → the migration → the facade class and its @method docblocks. The docblocks in a small package are often the only place the full method list exists.
First use case: one read-only global setting behind a route. Add a site_name row via a seeder or tinker, then read it in a controller:
Route::get('/hello', fn () => view('hello', [
'name' => setting('site_name', 'My App'),
]));
That single read exercises the whole chain — migration, model, cache, config, fallback — with nothing to unwind if the package turns out to be unusable. Do not start with per-model settings, bulk writes, or an admin UI; those are where the undocumented quirks live.
Wrap it in a settings service, even if it's three methods. You will want to change cache TTLs, add logging on writes, or swap the package later. A thin App\Support\Settings that delegates to the package gives you one file to edit:
final class Settings
{
public function get(string $key, mixed $default = null): mixed
{
return cache()->remember(
"settings.{$key}",
now()->addHour(),
fn () => setting($key, $default),
);
}
public function set(string $key, mixed $value): void
{
setting()->set($key, $value); // however the package exposes write
cache()->forget("settings.{$key}");
}
}
Wrap reads in Cache::remember regardless of whether the package claims built-in caching. In a 7-star package the cache invalidation on write is the first thing to be wrong. Owning the cache key namespace (settings.) lets you flush precisely and lets you verify by killing the cache in a test.
Use it for runtime-configurable values, not for configuration. Good fits: feature flags, maintenance banner text, contact email, pagination defaults, per-tenant branding. Bad fits: DB credentials, driver selection, anything baked into a service provider at boot. If a value must be present before the first request reaches your app, put it in config/, not in the database.
Per-model settings belong on the model, not scattered through controllers. If the package supports model-scoped settings, expose them through the model (a settings() relation or a small trait) rather than calling the package directly from twelve controllers. A User::setting('timezone', 'UTC') accessor is far easier to mock and to reason about than a package call with $user->id threaded through.
Type discipline at the boundary. Values round-trip through a database column. Cast on read, not on use:
$flags = (array) setting('feature_flags', []);
$limit = (int) setting('page_size', 25);
$enabled = filter_var(setting('beta'), FILTER_VALIDATE_BOOLEAN);
Never branch on a raw string from a settings row — "false", "0", "", and null all walk into truthy branches differently than you expect.
Seed defaults; don't rely on call-site fallbacks everywhere. A SettingsSeeder that inserts the canonical keys with sane values means your admin UI shows real current values, setting('x') never surprises you with a package-internal default, and a fresh DB in CI matches production. Call-site defaults become a second-line defence.
Testing. Settings are persisted state — use RefreshDatabase and seed only the keys under test. If the package caches, call cache()->flush() (or your namespace forget) in setUp(), otherwise test order will bite you. Assert both the happy path and the missing-key path:
it('falls back when unset', function () {
expect(setting('missing', 'fallback'))->toBe('fallback');
});
Admin surface: build a whitelist, not a generic editor. A loop over Setting::all() lets anyone with access set arbitrary keys, including ones another part of your app trusts. Define the editable keys in config (config('settings.editable')), validate against that list, and log every write with the old and new value.
composer.json Laravel constraint before you install. If you adopt it, budget for the possibility of forking or wrapping tightly enough that replacing it is a one-file change. This is the single biggest risk and should drive every "should I wrap it" decision toward yes.config:cache interacts badly with runtime reads. If any code path calls config() or env() expecting to see a setting written earlier in the request, it won't after php artisan config:cache. Settings belong in the package's own store or the cache, never written into config(). When behaviour differs between artisan serve and a cached production boot, run php artisan optimize:clear first.--force. Your published config/settings.php will silently miss new keys added upstream, so config('settings.new_key') returns null and the package falls back to an internal default you never chose. Always diff the vendor config against yours and merge manually.set actually clears the cache key it uses for get. If it doesn't (or you don't know), own the cache key layer yourself. Add a test: write, read, assert you see the new value in the same process and after cache()->flush().get after set in the same request is where stale caches surface. Read-your-writes is the test that catches a missing invalidation. Assert it explicitly; don't just trust it.get → mutate → set sequence on a JSON value is not atomic. Two requests can silently lose one another's changes. For anything an admin or user can edit concurrently, wrap in a transaction with a row lock, or store independent scalar keys instead of one blob.setting('typo') returns null and your page shows a blank header, not an exception. In strict environments, wrap the package's get to log when the resolved value is null and the key is in your known-keys list, or assert it in a boot test.serialize() blob, or a text column with casts on the model. If it's serialize(), that column is not portable across PHP versions and is a real migration risk later. Test with true, false, 0, "", [], and a nested array and check each one round-trips identically. Don't assume; prove.config() or a encrypted-cast column the package doesn't manage.$user->setting('theme') is 30 queries (or 30 cache misses). Eager load the settings relation if the package exposes one, or batch-fetch the settings you need with a single whereIn on the model type and keys.@method docblocks in a small package are often incomplete. Expect PHPStan/Larastan false positives on setting() and Setting::. Add a local stub in phpstan.neon rather than lowering your level.singleton(); if in doubt, flush the package's cache on RequestTerminated or register a scoped binding in your own provider.Settings class → extending the Setting model or the service provider → forking. Reaching for a fork first is the most common regret with a 7-star package — wrap it instead and keep the fork as the last resort.How can I help you explore Laravel packages today?