gupalo/config-bundle
Symfony bundle for centralizing application configuration with typed settings, environment-specific overrides, validation, and cached container access. It simplifies managing flexible defaults and maintainable multi-environment deployments while keeping settings organized and consistent across servi...
This is a Symfony bundle, not a Laravel package — there is no service provider, no facade, and Composer auto-discovery will do nothing for you. Treat it as a reference implementation you either (a) run inside a Symfony kernel living alongside your Laravel app, or (b) port in an afternoon. The codebase is small enough that reading it is faster than reading docs, which is fortunate because there aren't any.
Minimal evaluation path:
composer require gupalo/config-bundle in a scratch Symfony 6/7 app (not your Laravel app first — you want to see it work in its native habitat).config/bundles.php, then check src/DependencyInjection/ for the extension alias and whether the config table is created via Doctrine migrations or auto-mapped entities.php bin/console doctrine:schema:update --dump-sql to see exactly what storage it expects. That schema is the contract you'll be reimplementing in Laravel.First use case: runtime-editable site settings — site name, support email, maintenance mode flag, per-environment toggles — that an admin edits without a deploy. That is the only problem this class of bundle solves well, and it's the one worth evaluating against.
For the Laravel side, decide up front which of two shapes you want:
settings table plus a ConfigRepository bound to Illuminate\Contracts\Config\Repository, loaded once at boot and merged into config(). Roughly 80 lines. Most teams end up here.Typed getters over raw arrays. The value in this bundle is not the key/value table — you can write that in a migration — it's the accessor discipline. Wrap every read behind a typed method (getString, getBool, getInt, getArray) with an explicit default. Untyped get('key') returning null, '1', or 'false' depending on how the row was inserted is the #1 source of production bugs in DB-backed config.
Cache-on-read, invalidate-on-write. The bundle's manager almost certainly layers a cache pool over the storage layer. Mirror that in Laravel:
class SettingsRepository
{
public function get(string $key, mixed $default = null): mixed
{
return Cache::rememberForever("settings.{$key}", fn () =>
DB::table('settings')->where('key', $key)->value('value') ?? $default
);
}
public function set(string $key, mixed $value): void
{
DB::table('settings')->updateOrInsert(['key' => $key], ['value' => $value]);
Cache::forget("settings.{$key}");
}
}
Cache the whole map under one key instead if your settings count is small — fewer round trips, one invalidation point.
Load-on-boot into the config repository. The pattern that keeps application code idiomatic: at AppServiceProvider::boot(), read all settings once and Config::set('settings', $map). Then call sites use config('settings.maintenance_mode'). Downside: settings changes need a cache clear or a queue-based refresh; don't do this for anything that must change mid-request.
Environment scoping. Store rows keyed by (environment, key) or (tenant_id, key) rather than maintaining separate tables per environment. Seed the table from config/ defaults in a seeder so a fresh checkout and a production DB agree on every key that exists.
Feature flags. Same table, separate namespace (feature.*). Cast to bool at the boundary and expose a single Feature::enabled('new_checkout') helper so call sites never touch the raw value. This is where DB config shines and where env vars get painful (you can't flip FEATURE_X without redeploying or restarting workers).
Admin editing. Build a simple resource form (Filament, Nova, or plain Blade) over the table with a per-key type hint in a companion config file. Whitelist editable keys — never let the admin UI write arbitrary keys, or app.key-adjacent values become editable by anyone with panel access.
Testing. Bind an in-memory implementation (array $values) of your settings interface in tests and never touch the database. This is the main reason to define an interface rather than injecting the Eloquent model directly.
extra.laravel block, no auto-discovery, no facade. Anything you get from this package in a Laravel app is either a bridge you wrote or a port you wrote. Budget accordingly.singleton and be aware that constructor-injected config is frozen for the worker's lifetime — an Octane worker will hold stale values indefinitely.Cache::forget silently does nothing on a key that doesn't exist, which is fine, but a tag-less cache means you cannot invalidate "all settings" cheaply.null value. Decide the semantics explicitly. exists() and get() are different operations; conflating them causes defaults to be skipped whenever an admin clears a field..env/a secret manager. If you need both, keep secrets in env and reference them by name.app/Support/Settings/ and pin nothing.ConfigChangedEvent or similar) — an event hook is the cleanest place to rebuild caches or broadcast to workers.How can I help you explore Laravel packages today?