lst/settings-bundle
Laravel settings bundle for centralizing application configuration. Store and retrieve settings through a simple API, keep values organized, and manage options without scattering config across your codebase. A lightweight foundation for consistent runtime settings.
First, the reality check that shapes everything else. lst/settings-bundle is a Symfony bundle (Bundle class, DI extension, Doctrine ORM entities/repositories). It has no ServiceProvider, no facade, no publishable config/settings.php. In a Laravel app you cannot composer require it into a working state — the value you extract day-to-day is the pattern, not the package. Treat it as a reference implementation to read once and reimplement (~1–2 days).
Read the source in this order (small surface area):
*Bundle.php — what services get registered.DependencyInjection/*Extension.php — config tree and defaults.Entity/Setting.php — the schema (key, value, type).Repository/SettingRepository.php / manager service — read/write and type-coercion semantics. This is the part worth copying.First use case: an admin-editable runtime setting (a feature flag or a threshold) that ops can change without a deploy.
Minimal Laravel reimplementation:
// migration: settings table
$table->string('key')->unique();
$table->text('value')->nullable();
$table->string('type')->default('string'); // string|int|bool|float|array|json
$table->string('group')->default('default');
$table->timestamps();
// app/Services/SettingsManager.php — public API mirroring the bundle
public function get(string $key, mixed $default = null): mixed;
public function getBool(string $key, bool $default = false): bool;
public function set(string $key, mixed $value, string $type = 'string'): void;
public function forget(string $key): void;
Bind it in a SettingsServiceProvider, resolve lazily, and seed defaults so no code path depends on a missing row.
Typed getters with fallback defaults — the single feature that justifies the bundle's existence. Copy the semantics: getInt, getBool, getFloat, getJson/getArray, each accepting a default so callers never do ?? + cast gymnastics.
Manager behind an interface, injected — never a global helper. The bundle's DI wiring is the point (in Symfony). In Laravel, bind SettingsManager (an interface) in the container and inject it. That keeps the swap window small if you ever replace it.
$this->app->singleton(SettingsManager::class, fn ($app) =>
new CachedSettings(new DatabaseSettings($app['db']), $app['cache'])
);
Cache decorator with write-through invalidation — non-negotiable. Settings are read on nearly every request; a naive DB read per request is a latency and load problem. Wrap the DB manager in a cached decorator that forgets the key on write.
class CachedSettings implements SettingsManager {
public function get(string $key, mixed $default = null): mixed {
return Cache::rememberForever("settings.$key",
fn () => $this->inner->get($key, $default));
}
public function set(string $key, mixed $value, $type = 'string'): void {
$this->inner->set($key, $value, $type);
Cache::forget("settings.$key");
}
}
Group / namespace column — the bundle is a single flat global table. Add a group column (or prefix keys) now if you ever expect per-tenant, per-environment, or per-feature scoping. Retrofitting scope into a flat global table is painful.
Seed defaults via migration/seeder — so behavior is defined by data, not by code falling back to null. This also makes the admin panel self-documenting.
Admin surface via Artisan or Filament/Nova resource — settings without a UI become a DBA-only artifact and rot. Add an audit column (updated_by) and a timestamp while you're there.
Reading the same table from a Symfony service (interop only). If a sibling Symfony app owns the table and Laravel reads it, mirror the exact column names and type strings the bundle uses, and read the source to confirm how it serializes arrays/JSON. Do not let bundle types leak into your domain code.
Test the coercion edges — nulls, empty strings, "0", "false", JSON arrays, and mixed-case booleans. This is the highest-risk silent-bug area.
composer require will not give you a working settings API here — plan to port or reimplement from day one.register(). If you eagerly load settings when the container boots, an unreachable DB makes the whole app unbootable. Resolve lazily on first access; fail soft to defaults where safe."false", "0", "", and null mean for each type. The bundle's semantics are the contract to copy — don't invent your own mid-project.doctrine:schema:update-style tooling. If you mirror the entity, confirm JSON/boolean column types behave across MySQL/Postgres/SQLite.group column or a feature./integration. prefix convention from the start.How can I help you explore Laravel packages today?