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

hatchyu/laravel-settings

Laravel package for managing application settings in the database with a simple API, caching, and typed values. Define defaults, read and update settings at runtime, and organize configuration by groups for maintainable apps.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and publish

composer require hatchyu/laravel-settings
php artisan vendor:publish --provider="Hatchyu\Settings\SettingsServiceProvider"
php artisan migrate

Publishing typically gives you two things to read first: config/settings.php and the settings table migration. Those two files define almost all of the package's behaviour, so open them before writing any application code.

Where to look first (in order)

  1. config/settings.php — scope definitions (global, tenant, user, etc.), the cache store and TTL, table name, and the default cast. This tells you what the inheritance chain actually looks like.
  2. The migration — note the columns (key, value, scope, scope_id, cast, group) and the unique index. You will query against this shape when debugging.
  3. src/SettingsManager.php (or similarly named class) — the public get/set/forget methods and how the scope chain is resolved. This is the file to read when behaviour surprises you.
  4. The cast classes — confirm the exact identifiers (bool, int, json, array, encrypted, etc.) rather than guessing.

Your first use case

Model a setting that has a sensible default but can be overridden per tenant. For example, a mail "from" address:

use Hatchyu\Settings\Facades\Settings;

// Global default (scope = null / 'global')
Settings::set('mail.from_address', '[email protected]', cast: 'string');

// Tenant override
Settings::forScope('tenant', $tenant->id)
    ->set('mail.from_address', '[email protected]', cast: 'string');

// Read the effective value — no scope argument needed if a scope is bound
Settings::get('mail.from_address'); // '[email protected]' for Acme, default otherwise

The value of the package is exactly this: write once at the top of the hierarchy, override narrowly, and read the effective value everywhere else without scattering if ($tenant->setting) checks through your code.

Seed the defaults. Do it in a seeder or a small InstallSettingsCommand, and make it idempotent (updateOrCreate-style) so re-running after a deploy doesn't clobber operator changes.

Implementation Patterns

Bind the scope once per request, not per call. Resolve the tenant in middleware and bind it there, so controllers, mailers, and Blade views all read the same effective settings:

// app/Http/Middleware/BindSettingsScope.php
public function handle($request, Closure $next)
{
    if ($tenant = $request->user()?->tenant) {
        Settings::forScope('tenant', $tenant->id);
    }

    return $next($request);
}

Everything downstream then uses the plain Settings::get('key') and inherits correctly. Avoid passing $tenant into every call site — it's the fastest way to leak cross-tenant values.

Group your keys and load them as a bundle. Prefer a small number of grouped reads over dozens of single-key reads, especially if you're rendering a settings page or sharing values with Inertia/Blade:

$ui = Settings::group('ui')->all();   // ['theme' => 'dark', 'density' => 'compact', ...]
View::share('uiSettings', $ui);

One resolution pass, one cache hit, one place to audit.

Treat config as the bottom of the inheritance stack. A pattern that works well:

$from = Settings::get('mail.from_address') ?? config('mail.from.address');

Config = deploy-time defaults (versioned, reviewed), settings = runtime overrides (operator-editable). Don't duplicate the same value in both.

Write through a service, not from controllers. Wrap writes in a small SettingsService that validates the key against an allow-list, applies the cast, and saves. This gives you one place to add audit logging and one place to guarantee cache invalidation.

Invalidate on write, and test it. If you write via raw models (Setting::create(...)) rather than the package API, model events may not fire and the cache will go stale. Either always write through the facade, or add your own saved/deleted observers on the settings model.

Casting: be deliberate. Use bool/int casts rather than relying on string comparison; 'false' is truthy in PHP and this is a classic production bug. Store structured data as json and read it back as an array, but treat the returned array as read-only — mutating it won't persist.

Testing. In phpunit.xml, point the package cache at the array driver so tests don't leak state between runs; add a small helper or trait that resets the resolved-settings cache in setUp. Then assert both the default and the override path for any setting-driven behaviour — the inheritance edge is where regressions live.

Queues and jobs. A bound scope does not survive serialization into a queued job. Re-resolve and re-bind inside handle() from an explicit $tenantId on the job payload. Never rely on the ambient scope from the dispatching request.

Gotchas and Tips

Falsy values are the #1 inheritance bug. If the implementation uses null/missing-row to mean "inherit", then a deliberately stored false or 0 must not be treated as absent. Verify with a test: set a global true, override with false, and assert get() returns false (not the global true). If the package gets this wrong, you'll need a sentinel value or a has() check in your wrapper.

Cache invalidation across the hierarchy. Changing a global value must invalidate every descendant's resolved value. Confirm the package keys its cache by the full scope chain (or flushes broadly on write). Test it: warm the tenant cache, change the global, read again. If it doesn't invalidate, wrap writes in your service with an explicit flush.

Choose the cache store deliberately. Use a shared store (Redis, database, memcached) in multi-node deployments. array is per-process and will make settings appear to "randomly revert" after a load balancer hop — a very confusing bug to diagnose.

Octane and long-lived workers. With Octane or queue workers, container singletons and in-memory resolution caches persist between requests. Register the settings scope in octane:flush/scoped() bindings, or reset it in your middleware's terminate step, otherwise tenant A can see tenant B's values.

config:cache and dynamic scope registration. If you register scopes at runtime (from a database table of tenants, for example), remember that config is frozen when cached. Register scopes in a service provider that runs at boot, not in config files that read the DB.

Don't clear the wrong cache. php artisan cache:clear only clears the default store. If settings use a separate store, optimize:clear and cache:clear won't touch it — use the package's own clear command or flush the specific store in deploy scripts.

Watch for N+1 on bulk pages. A settings list page that calls get() in a loop per key per scope will hammer the cache/DB. Use all()/group() and filter in PHP.

Transactions and stale reads. A write inside an open transaction can repopulate the cache with a value that gets rolled back. Either write settings outside the transaction or invalidate the cache in DB::afterCommit().

Migration/index details. The unique index on (scope, scope_id, key) is what makes overrides deterministic; if you customise the table name or add columns (e.g. updated_by), re-check that index and the package's updateOrCreate-style writes still match on the right columns.

Extension points to lean on. Custom cast classes (register a value object / enum caster), the settings model's events for audit trails, and a cache key prefix in config if you share a Redis instance across apps. Prefer these over forking.

Maturity risk. With two stars and no dependents, treat this as a package you own. Pin an exact version in composer.json, and write a thin internal facade (App\Settings) over it so you can swap the implementation later. Keep a handful of feature tests asserting inheritance, casting, and invalidation — they double as the contract you'd need if you ever replace it.

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.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings