Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Settings Laravel Package

egough/laravel-settings

View on GitHub
Deep Wiki
Context7

Getting Started

Treat this package as a small, typed key–value store with two independent scopes (global and per-model) plus a thin feature-flag layer. Your first 10 minutes:

php artisan vendor:publish --tag=settings-config
php artisan vendor:publish --tag=settings-migrations
# only if you plan on per-model preferences:
php artisan vendor:publish --tag=settings-model-migrations
php artisan migrate

Before publishing anything, check for an existing config/settings.php, already-published migrations, and models that use the HasSettings trait — this package is usually added incrementally to an app that already has config. Publish only what's missing.

Then seed your defaults as literal dotted keys in config/settings.php:

'defaults' => [
    'site.name' => 'My Application',
    'billing.enabled' => false,
    'uploads.limit' => 10,
],

Your first use case should be the read path, not the write path: render something in a Blade layout.

<title>@setting('site.name')</title>

@hasFlag('billing.enabled')
    <x-billing-panel />
@endhasFlag

That gives you an immediately deployable win: a site name or feature toggle you can change without a release. Only after the read path works should you wire up writes (admin screen, Artisan, tinker).

Implementation Patterns

Pick one access style per layer and stick to it. The helper, facade, and injected manager all resolve the same service, so consistency is about testability, not behavior:

settings()->set('site.name', 'Customer Portal');          // helpers, tinker, closures
Settings::get('site.name');                               // facades, controllers
public function __construct(private SettingsManager $settings) {}  // services, jobs

Prefer constructor injection in anything you unit test — it lets you swap or mock the manager and makes the dependency visible.

Declare types when writing from untrusted or string sources. Inference is fine for native PHP values; be explicit at the boundaries (forms, Artisan, imports):

settings()->set('uploads.limit', '10', 'int');       // string → int: 10
settings()->set('billing.enabled', true, 'bool');
settings()->set('notifications.channels', ['mail', 'database']); // json

Prefer forget() over writing a default explicitly. forget() restores lookup of the config default, so a later change to settings.defaults actually propagates:

settings()->forget('site.name');

Model settings: use the trait and $model->settings(). Don't inject the model manager and forget to call for():

$user->settings()->set('ui.theme', 'dark');
$theme = $user->settings()->get('ui.theme', 'light');
$user->settings()->forget('ui.theme');

Express inheritance explicitly — model settings deliberately do not fall back to global settings or config defaults. If you want "per-user override, global default, hardcoded fallback", write the chain:

$theme = $user->settings()->get('ui.theme', settings()->get('ui.theme', 'light'));

Wrap that in a single method on the model or a small UserPreferences service so the chain lives in one place instead of being copy-pasted across views.

Feature flags: store PHP booleans, not strings. Use the flag helper for global flags and the scoped manager with a cast for model flags:

flag('billing.enabled', false);
Flag::enabled('billing.enabled', false);

$enabled = (bool) $user->settings()->get('notifications.enabled', true);

React to changes with the three events, filtering on both model and key. $event->model === null means global scope. Listen to SettingCreated as well as SettingUpdated if you care about any newly stored value, and remember that a creation matching a config default still emits SettingCreated.

Event::listen(SettingUpdated::class, function (SettingUpdated $event): void {
    if ($event->model === null && $event->key === 'billing.enabled') {
        Log::info('Billing availability changed', ['enabled' => $event->newValue]);
    }
});

Use the Artisan commands for ops and hotfixes, and always pass --type for anything non-string:

php artisan settings:get site.name
php artisan settings:set site.name "Customer Portal"
php artisan settings:set billing.enabled false --type=bool
php artisan settings:clear-cache

Testing pattern that actually catches bugs: use the real database with the package migrations, then cover the four things that break in production: native types round-tripping, stored override beating the config default, forget() restoring fallback, and stored null behaving differently from a missing key. For model settings, create two persisted owners and assert isolation (including isolation from global values). For caching, prime the cache with a read, write, then assert the next read is fresh. Test listeners by dispatching real events; only fake the dispatcher when you're asserting emission alone.

Gotchas and Tips

Dotted keys are literal. site.name is one key, not a path into a nested array. There's no settings()->get('site')['name'] and no way to read a subtree. If you need grouped settings, either fetch the whole stored set with all() and filter, or store a single JSON blob and read its members in your own code.

Scopes don't inherit. The most common surprise: a user has no ui.theme stored, the global setting says dark, and the user sees light because the fallback argument was 'light'. Always route through an explicit resolution helper as shown above.

Stored null is a real value. It wins over config defaults and over your fallback. settings()->has($key) returns true for a stored null, but @hasSetting($key) fails because it checks the resolved value for non-null. Test these two paths separately — this is the single easiest place to ship a subtle bug.

all() excludes config defaults. all() returns stored values only, while has() includes stored null. If you're building an admin screen that lists "everything configurable," merge config('settings.defaults') with all() yourself; don't assume all() is the full picture.

'false' is truthy. Feature flags cast to PHP boolean, and PHP's (bool) 'false' is true. The Artisan --type=bool flag exists precisely to avoid this — but a form field, a CSV import, or a .env-adjacent habit will bite you. Validate and cast at the input boundary.

Artisan writes are strings by default. settings:set billing.enabled false without --type=bool stores the string "false", which then evaluates truthy. Make this a standing code-review rule.

Direct DB writes, Eloquent writes, and repository bypasses break everything. You lose cache invalidation and events. Any migration, seeder, or one-off DB::table('settings') statement must be treated as an out-of-band change requiring a manual clearCache(). Use settings()->set() inside seeders.

Cache invalidation is scope-specific. php artisan settings:clear-cache clears only the global cache. Model caches are keyed by key_prefix plus owner identity and must be cleared via $model->settings()->clearCache(). Also note ttl => null means cache forever — correct for correctness (writes invalidate), but it makes a bypassed write effectively permanent until someone manually clears.

Events are post-commit. With the built-in repositories, events dispatch after the settings transaction commits and are discarded on rollback. Two consequences: listeners see the new value after other transactions may already have read it, so don't build read-your-writes logic on top of a listener; and in tests, an enclosing test transaction that never commits will silently defer every event. Explicitly commit or roll back the settings connection when testing transaction-sensitive behavior.

Don't put boot-critical config or secrets here. Anything needed before the database is available (queue names, mail transport credentials, encryption keys, service credentials) belongs in config/ + environment variables. The database round-trip in a service provider's boot() is a real deployment hazard, and storing secrets in a table that admins edit via a UI is a compliance problem, not a convenience.

Blade directives are global-only. @setting, @hasSetting, and @hasFlag read global settings; there is no per-model directive. For user-scoped output, resolve in the controller or a view composer and pass a variable into the view.

Model settings require integer primary keys. The current implementation casts model IDs to integers, so UUID/ULID-keyed models are not supported. Verify this before designing "per-team preferences" around a UUID team model — it's a cheap check now and an expensive refactor later.

Maturity caveat for anything load-bearing. With a small user base and no dependents, plan for reading the source rather than the docs when behavior gets subtle (event ordering and cache keying are the two places to look). Pin the version, and wrap the package behind your own thin service (SiteSettings, UserPreferences) so that a bug or an abandonment doesn't leak into every Blade template and controller. That wrapper is also your extension point: it's where you put the inheritance chain, the string-to-bool coercion, and the cache-clear-after-seed logic — the three things this package deliberately leaves to you.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console