unifik/database-config-bundle
Unifik DatabaseConfigBundle is a Symfony bundle for storing and managing application configuration in the database instead of flat files. It provides config entities, services, and admin-friendly editing for dynamic, environment-aware settings.
Install with Composer and register the bundle in your kernel/bundles file:
composer require unifik/database-config-bundle
// config/bundles.php (Symfony 4+) — or AppKernel::registerBundles() on legacy apps
return [
// ...
Unifik\DatabaseConfigBundle\UnifikDatabaseConfigBundle::class => ['all' => true],
];
Create the config table. The bundle ships a Doctrine entity (typically Config with name/value columns). Run migrations or, for a quick local start:
bin/console doctrine:schema:update --force
In production always generate a migration instead of --force.
Seed a couple of rows so you have something to read before wiring up an admin UI:
bin/console doctrine:query:sql "INSERT INTO config (name, value) VALUES ('site.title', 'My App')"
Look here first when exploring the bundle:
Entity/Config.php — the persisted shape (column names, types, length limits).Resources/config/services.yml — the service IDs/aliases; confirm the exact manager alias with bin/console debug:container | grep -i config.DependencyInjection/ — how/if values are pushed into container parameters and whether a prefix is applied.README.md — the canonical service name and any required bundle-level config: options.First real use case: inject the config manager into a service or controller and read a value with a fallback, then write one back from an admin action:
$title = $this->configManager->get('site.title', 'Default Title');
$this->configManager->set('site.title', 'New Title');
Wrap it in your own Settings service. Don't let the bundle's manager leak across the codebase. A thin application service gives you typing, validation, and a single place to add caching:
final class Settings
{
public function __construct(private ConfigManager $config, private CacheItemPoolInterface $cache) {}
public function get(string $key, mixed $default = null): mixed
{
$item = $this->cache->getItem('settings.'.$key);
if (!$item->isHit()) {
$item->set($this->config->get($key, $default));
$this->cache->save($item);
}
return $item->get();
}
}
Store structured values as JSON, not ad-hoc strings. For arrays/objects, json_encode on write and json_decode(..., true) on read inside your wrapper. This keeps the value column simple and avoids inventing your own serialization format.
Use it for runtime-editable settings, not container topology. Good fits: site title, feature flags, mail-from address, pagination size, third-party API toggles. Poor fits: service definitions, routing, security roles — those belong in YAML/compiled config.
Admin CRUD pattern. Expose the Config entity through a simple form (name + value, optionally a "type" field) behind ROLE_ADMIN. On save, flush the entity, then invalidate the corresponding cache key and — if the bundle pushes parameters at boot — clear the Symfony cache in non-prod.
Idempotent seeding. Rather than relying on fixtures, add a console command that inserts missing keys only if absent. Run it during deployment so new code can assume defaults exist. Never overwrite operator-changed values.
Layer precedence explicitly. A common pattern is DB value ?? parameters.yml value ?? hard-coded default. Implement the fallback chain in your wrapper so callers express intent once, and document the order so nobody hunts for where a value came from.
Twig convenience. Register a small Twig extension exposing setting('site.title') that delegates to your Settings service, so templates don't inject the raw manager.
Environment separation. If the same database is shared across stages, prefix keys (prod., staging.) or use a separate table/database per env; otherwise a staging tweak silently changes production behavior.
$container->setParameter(), those values are frozen when the cache is warmed in prod. Editing a row won't change anything until cache:clear (or a dedicated warmup). Prefer reading values through a service instead of injected parameters when you need live updates.cache:clear/cache:warmup will fail on a fresh deploy before migrations have run, or when the DB is temporarily unreachable. Check the bundle's boot logic and, if needed, warm the cache after doctrine:migrations:migrate.value column is often VARCHAR(255). Truncation errors on long JSON payloads are a frequent surprise — verify the mapping and migrate to TEXT if you store larger blobs.name. Two concurrent set() calls on a new key can otherwise create duplicates. Wrap read-modify-write in a transaction or use an upsert.Settings wrapper rather than the bundle's manager. For integration tests, point the bundle at an in-memory SQLite database and load a fixture schema so tests stay fast and isolated.debug:container to confirm the real service alias (they vary between versions), and doctrine:query:sql to verify what's actually stored — value formatting bugs (e.g., "0", "false", empty string) are common when everything round-trips as a string.services.yaml to add casting, validation, or auditing. Prefer decoration over forking — it survives upgrades.How can I help you explore Laravel packages today?