smirik/config-bundle
Symfony bundle for managing application configuration from the database. Provides Doctrine-backed settings storage, a service for reading and writing config values, caching, and integration with Symfony services and templates so runtime options can change without code deployments.
Confirm the mismatch before you plan. smirik/config-bundle is Smirik\ConfigBundle — a Symfony bundle with a DependencyInjection extension, Resources/config YAML service definitions, and container-parameter config merging. There is no Laravel service provider, no facade, no config/config-bundle.php, and no package auto-discovery. In a Laravel codebase this is a reference design, not a dependency. Day-to-day leverage means porting its ideas onto Eloquent + Cache, not calling it.
Read the source in this order (worth 20 minutes, it is small):
DependencyInjection/*Extension.php — see how defaults are merged with app config.Check compatibility first if you still want to try it (e.g. in a legacy Symfony app you maintain alongside Laravel):
composer why-not smirik/config-bundle ^1.0
composer show smirik/config-bundle --all # PHP + symfony/* constraints, licence, tags
Expect PHP 5/7 and symfony/* 2.x/3.x pins. If it requires symfony/http-kernel 2.x/3.x, it is incompatible with Laravel 9+ by construction.
First use case (Laravel-native rebuild). Reproduce the capability you actually want — DB-backed, environment-aware, cached runtime settings:
// database/migrations/..._create_settings_table.php
Schema::create('settings', function (Blueprint $t) {
$t->id();
$t->string('environment')->default('production')->index();
$t->string('key');
$t->text('value')->nullable();
$t->string('type')->default('string'); // string|int|float|bool|json
$t->timestamps();
$t->unique(['environment', 'key']);
});
final class Settings
{
public function __construct(private Repository $config, private string $env) {}
public function get(string $key, mixed $default = null): mixed
{
// 1. code default 2. env override 3. DB override
$rows = Cache::rememberForever("settings:{$this->env}", fn () =>
Setting::where('environment', $this->env)->pluck('value', 'key')->all()
);
if (! array_key_exists($key, $rows)) {
return $this->config->get("settings.{$key}", $default);
}
return $this->cast($rows[$key], $this->config->get("settings.{$key}.type", 'string'));
}
}
Bind it as a singleton in AppServiceProvider; that is your "bundle registration" step.
Resolution order is the whole design. Encode it explicitly and document it: code default → env override → DB override → request/runtime override. The bundle's value is that precedence is predictable; if you rebuild it, write it down in a docblock and test it. Silent wrong values come from an ordering you cannot recite.
Back the repository, don't scatter reads. Either inject Settings where needed or implement Illuminate\Contracts\Config\Repository and swap it in the container. The latter keeps config('limits.max') working across the app:
$this->app->singleton('config', fn ($app) => new CachedConfigRepository(
$app->make('settings'), $app['config']
));
Cache aggressively, invalidate on write, never on read. Settings sit on the per-request hot path. Cache the whole environment map in one key, not per-key (avoids N round-trips). Invalidate via model events:
static::saved(fn () => Cache::forget("settings:{$this->environment}"));
static::deleted(fn () => Cache::forget("settings:{$this->environment}"));
Use a shared cache store (Redis) so all app servers see the same invalidation.
Type casting is a first-class concern. Persist type, cast on read. (bool) "false" is true; guard it:
private function cast(?string $v, string $type): mixed
{
return match ($type) {
'bool' => filter_var($v, FILTER_VALIDATE_BOOLEAN),
'int' => (int) $v,
'float' => (float) $v,
'json' => json_decode($v, true, flags: JSON_THROW_ON_ERROR),
default => $v,
};
}
Validate on write, not on read. Use a FormRequest and a schema map keyed per setting (max_upload_mb => ['integer', 'min:1']). Fail the write loudly; never let a typo (limts.max) silently fall back to default in production.
Scope it. The bundle's per-environment overrides map cleanly to an environment column. For multi-tenant, add tenant_id (nullable = global) and resolve global → tenant in the lookup. Defaults live in code; overrides live in data.
Gate the write path. Put the CRUD (Filament/Nova/Blade) behind a policy. Anyone who can write settings can change limits and copy across every tenant — treat it as admin-only and, for anything regulated, add an append-only setting_changes audit table.
Warm the cache on deploy. Add a post-deploy php artisan settings:warm step, or Cache::lock around the first rebuild so a fresh release doesn't stampede the DB.
composer require it into Laravel. Solver conflicts are near-certain; --ignore-platform-reqs installs an untested dependency graph into production — not a mitigation. If you must reuse its logic, vendor a pinned fork and expose it behind one internal interface so no app code touches Smirik\ConfigBundle\*.php artisan config:cache will not see DB values. Config files load before the DB guarantee exists. Keep DB-backed settings in the repository above, injected lazily — never read them inside config/*.php.array_merge; a misunderstood precedence yields wrong runtime values, not an exception. Write a test that asserts each layer wins where you think it does.rememberForever key that is only forgotten on one node means stale config on the others. Use a shared cache store, or version the key (settings:{env}:{version}) and bump the version on write.deleted and saved in model events, not just in your update controller — anything bypassing the controller (tinker, a migration, an import) must still bust the cache.settings:prune command that flags rows with no matching code default.settings() helper; its DI service → a container singleton; its compiler pass → a service provider register()/boot(). Nothing in the bundle is hard to reproduce.spatie/laravel-settings). Revisit the Symfony bundle only if you are genuinely operating a Symfony application — there it is at least architecturally native, but you still inherit ownership of an unmaintained fork.How can I help you explore Laravel packages today?