DynamicDbBundle is a Symfony bundle that allows you to store database connection configurations in a database table and seamlessly fetch and instantiate connections and entity managers at runtime.
With this bundle, you can natively use $doctrine->getConnection('dynamic_name') or $doctrine->getManager('dynamic_name') just like any conventionally configured connection in your doctrine.yaml.
- Seamless Doctrine Integration: Fetches database configuration dynamically via Doctrine's
ManagerRegistry. - Inherited ORM Configuration: Dynamically boots new entity managers by automatically inheriting the configuration (metadata mapping drivers, naming strategies, custom functions, filters, etc.) of the default
EntityManager. No need to redefine entity paths or configs. - Dynamic Connection Discovery: Extends standard
ManagerRegistrylookup methods (getConnections(),getConnectionNames(),getManagers(), andgetManagerNames()) to include dynamically created connections/managers alongside static ones. - Universal Database Support: Supports all database drivers supported by Doctrine DBAL (MySQL, PostgreSQL, SQLite, Oracle, SQL Server, etc.) via structured parameter mapping or DSN URLs.
- Auto Cache Rebuilding: Automatically handles clearing the Symfony cache when your dynamic database connection entities are created, updated, or removed, ensuring any new connections are immediately discoverable.
- Runtime Secret Injection: Allows the consumer application to supply a decryption secret via Symfony's DI container, which is automatically forwarded to the connection entity via
setSecret()before the connection config is built.
Add the bundle to your project via Composer (if published):
composer require feroz/dynamic-db-bundleEnsure the bundle is registered in your config/bundles.php:
return [
// ...
Feroz\DynamicDbBundle\DynamicDbBundle::class => ['all' => true],
];Create a standard Doctrine Entity in your application that stores the database connection configurations. Crucially, this entity must implement DynamicDbConnectionInterface and provide implementations for all interface methods, including setSecret().
The setSecret() method is called automatically by DynamicDbProvider before building the connection config, allowing your entity to make the secret available to downstream logic (e.g., for custom password decryption).
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Feroz\DynamicDbBundle\Contract\DynamicDbConnectionInterface;
#[ORM\Entity]
class TenantConnection implements DynamicDbConnectionInterface
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255)]
private ?string $connectionName = null; // e.g., "tenant_1"
#[ORM\Column(length: 255)]
private ?string $dbHost = null;
#[ORM\Column(length: 255)]
private ?string $dbName = null;
#[ORM\Column(length: 255)]
private ?string $dbUser = null;
#[ORM\Column(length: 255)]
private ?string $dbPassword = null;
private ?string $secret = null;
// --- DynamicDbConnectionInterface implementation ---
public function getConnectionName(): string { return $this->connectionName; }
public function getConnectionString(): ?string { return null; } // Optional: Return DSN URL or Oracle TNS string here
public function getDatabaseDriver(): string { return 'pdo_mysql'; } // Hardcode or map a column
public function getDatabaseHost(): string { return $this->dbHost; }
public function getDatabasePort(): int|string { return 3306; }
public function getDatabaseName(): string { return $this->dbName; }
public function getDatabaseUser(): string { return $this->dbUser; }
public function getDatabasePassword(): string { return $this->dbPassword; }
/**
* Called automatically by DynamicDbProvider before the connection config is built.
* The secret is null when none is configured — implement your decryption logic here.
*/
public function setSecret(?string $secret): void
{
$this->secret = $secret;
}
}You can fetch the connection or the manager directly through Symfony's core Doctrine integration! The bundle decorates the Doctrine registry to seamlessly integrate.
namespace App\Controller;
use Doctrine\Persistence\ManagerRegistry;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class TenantController extends AbstractController
{
#[Route('/tenant/{tenantName}', name: 'tenant_dashboard')]
public function index(string $tenantName, ManagerRegistry $doctrine): Response
{
// Behind the scenes, the bundle fetches the row matching $tenantName
// and initializes the connection seamlessly!
$connection = $doctrine->getConnection($tenantName);
// Alternatively, grab the dynamic EntityManager
$entityManager = $doctrine->getManager($tenantName);
// Run queries for this specific tenant's database
$results = $connection->executeQuery('SELECT * FROM users')->fetchAllAssociative();
return $this->json($results);
}
}The bundle does not provide a built-in encryption utility. Password encryption and decryption are entirely the consumer's responsibility.
The recommended pattern is:
- Encrypt the password before persisting the entity (using any encryption library of your choice).
- Decrypt the password inside
getDatabasePassword()of your entity, using the secret injected viasetSecret().
public function setSecret(?string $secret): void
{
$this->secret = $secret;
}
public function getDatabasePassword(): string
{
if ($this->secret !== null) {
// Decrypt using your own logic / library
return MyEncryptionHelper::decrypt($this->dbPassword, $this->secret);
}
return $this->dbPassword; // Return as-is if no secret configured
}DynamicDbProvider accepts $secret as an optional constructor parameter. The recommended approach is to bind it in your application's services.yaml using a Symfony parameter (e.g. from an environment variable):
# config/services.yaml
parameters:
dynamic_db_secret: '%env(DYNAMIC_DB_SECRET)%'
services:
Feroz\DynamicDbBundle\Service\DynamicDbProvider:
arguments:
$secret: '%dynamic_db_secret%'With this configuration, DynamicDbProvider will automatically call $entity->setSecret($secret) on the fetched connection entity before building the connection config, giving the entity access to the secret for custom decryption logic.
If no secret is needed, simply omit the binding — the $secret parameter defaults to null and setSecret() will not be called.
- When you call
$doctrine->getConnection('X')or$doctrine->getManager('X'), the wrappedDynamicRegistryDecoratorintercepts the request. - If Doctrine natively doesn't know about connection
X,DynamicDbProviderkicks in. - It finds the class implementing
DynamicDbConnectionInterfacedynamically and uses the default EntityManager to fetch the entity matchingconnectionName = 'X'. - If a
$secretwas configured (via DI), it calls$entity->setSecret($secret)on the fetched entity. - The
DynamicEntityManagerFactoryboots up the new ORM connection using the database connection parameters (or parses a DSN URL using Doctrine'sDsnParser). - The new dynamic
EntityManageris instantiated by inheriting the exact configuration (metadata mappings, naming strategies, proxies, etc.) of the defaultEntityManager. - Standard methods like
$doctrine->getConnections()or$doctrine->getManagers()are decorated to dynamically include the newly instantiated connections/managers alongside the statically defined ones. - The connection is cached locally for the remainder of the request.