sherlockode/configuration-bundle
A Symfony bundle for database-backed application configuration. Define typed settings, manage them through an admin-friendly interface, and retrieve values easily in services, controllers, and templates, with Doctrine persistence and caching support.
First, the honest framing. sherlockode/configuration-bundle is a Symfony bundle, not a Laravel package. There is no service provider, no facade, no config/configuration.php publish tag. As a Laravel developer you have three real options, in order of cost:
config() + Eloquent (or drop in spatie/laravel-settings, which is the Laravel-native equivalent and should be your default unless you specifically need this bundle's type system).If you are evaluating it for a Symfony service in a mixed estate, the minimal path is:
composer require sherlockode/configuration-bundle
config/bundles.php.doctrine:schema:update in dev).Where to look first, in this order:
ConfigurationManager method names — the public surface is deliberately tiny (read one, read all, write one) and you should verify signatures rather than assume them.array/json values are stored before you write any query or migration around them.First use case: the "site name / support email / feature flag" trio. Declare three entries in code, let the DB hold the values, inject ConfigurationManager into a controller and a Twig extension, then build one trivial admin form that writes them back. That exercises the whole loop — schema in code, value in DB, read at runtime — in under an hour.
Schema in code, values in the database. The bundle's core discipline is that definitions (code, stable, versioned) are separate from values (DB, mutable, admin-editable). Keep that boundary religiously: never let an admin create a brand-new configuration key from a form. New keys ship as code; only values are editable.
Wrap the manager behind your own interface on day one. The single highest-value decision. Do not let ConfigurationManager leak into controllers, jobs, or Blade/Twig:
interface AppSettings
{
public function siteName(): string;
public function supportEmail(): string;
public function newCheckoutEnabled(): bool;
}
final class BundleBackedSettings implements AppSettings
{
public function __construct(private ConfigurationManager $manager) {}
public function newCheckoutEnabled(): bool
{
return (bool) $this->manager->get('checkout.new_enabled');
}
}
Fifteen minutes of work, and it is the difference between "we can swap this out" and "this bundle is now load-bearing in 40 call sites." It also gives you a seam to fake in tests.
Cache-aside is not optional. Configuration reads land on the request path. Left raw, every flag check is a DB round-trip — and a page render might do a dozen. Decorate the manager:
final class CachedConfigurationManager implements ConfigurationManagerInterface
{
public function get(string $code): mixed
{
return $this->cache->rememberForever(
"cfg:{$code}",
fn () => $this->inner->get($code)
);
}
public function set(string $code, mixed $value): void
{
$this->inner->set($code, $value);
$this->cache->forget("cfg:{$code}");
}
}
Two things to get right: invalidate on write (the setter above, not a scheduled job), and use a shared cache store if you run more than one app server — a per-node APCu cache means an admin edit takes effect on one node and silently not on the others. Alternatively version the whole namespace (cfg:v{int}:...) and bump the version on any write; cruder, but it survives multi-node without a taggable store.
Bulk-load the whole set once per request. If you are going to touch more than a couple of keys, read everything in one call and serve from an in-memory array. It turns N queries into one, and the configuration set is small enough that this is always the right trade.
Admin write path. You get no admin UI from this bundle — you build it. The pattern that ages well:
$request->validate()-style ad hoc rules.Migration pattern from static config. Read-through fallback while transitioning: if the DB has no value for a code, fall back to the old config()/YAML/env source, and log which path was hit. Run both for a release cycle, then delete the fallback. Never migrate everything at once, and never migrate secrets — DB config tables are visible in admin panels and DB backups.
Multi-tenancy is not built in. The bundle is a flat key/value store. Tenant-scoped settings require either a custom type/entity extension or a per-tenant key prefix (tenant.{id}.site_name) with a decorator that injects the tenant ID into every lookup. The decorator approach is far cheaper; take it unless you have genuinely large per-tenant config volumes.
Custom types are the intended extension point — register a type service that knows how to validate and (de)serialize your format (a money value, a timezone, a JSON schema). That is how you keep validation in code while values stay editable.
If you take the sidecar route, expose GET /config returning the full resolved set as JSON with an ETag; have Laravel poll it into its own cache on a short TTL and read locally. That keeps configuration off the Laravel request path entirely and makes the Symfony boundary explicit and replaceable.
composer update --dry-run resolution check so a constraint-only release cannot surprise you on deploy.array types are serialized before you write any query against the table, and before you ever hand-edit a value in SQL. JSON is queryable and diffable; PHP serialize() is neither.set(), some expect you to. Getting this wrong produces values that appear saved in the request and vanish afterwards, or unexpected flushes inside a larger transaction.How can I help you explore Laravel packages today?