gam6itko/doctrine-multibase-bundle
A Symfony bundle that adds multi-database support to Doctrine ORM: configure several connections and assign entities to them via attributes, letting a single application persist to and query from multiple databases.
This bundle is small and underexposed (no stars, no dependents), so treat the source as the documentation. The core idea: keep your default Doctrine connection as the "base", and at runtime repoint it at a different database instance without rebuilding the container.
Minimal steps:
composer require gam6itko/doctrine-multibase-bundleGam6itko\DoctrineMultibaseBundle\DoctrineMultibaseBundle in config/bundles.php# config/packages/doctrine.yaml
doctrine:
dbal:
default_connection: default
connections:
default:
url: '%env(DATABASE_URL)%'
reporting:
url: '%env(REPORTING_DATABASE_URL)%'
tenant_a:
url: '%env(TENANT_A_DATABASE_URL)%'
src/ for the class that decorates or extends Doctrine\DBAL\Connection, and in Resources/config/ (services.xml/yaml) for the service ID it replaces (usually doctrine.dbal.default_connection or a new multibase.connection alias). Grep for switch, selectBase, or setConnection to see the exact method name — it is not standardized.First use case to validate your integration: a console command or controller that hits $connection->getDatabase() (or SELECT DATABASE()), switches to reporting, and confirms the query now lands on the other schema. Once that works, the rest is just deciding when to switch.
Request-scoped switching via a listener. The most common pattern is a kernel.request subscriber that resolves a tenant/report target and calls the switch before anything touches the EM. Keep the decision in one place so controllers never call the switch method directly.
Wrap switches in try/finally. Because the switch mutates a shared service, always restore the base connection:
$previous = $connection->getDatabase();
try {
$connection->switchTo('reporting'); // confirm method name in src/
$rows = $connection->fetchAllAssociative($sql);
} finally {
$connection->switchTo($previous);
}
Expose this as a small MultibaseManager service (switch + restore + current()) and inject that everywhere instead of the raw connection — it makes the lifecycle explicit and testable.
Console commands. Call the switch at the top of execute() against the connection the command receives, and add a --base= option. This lets one command target any configured DB without duplicating command classes.
Messenger / long-running workers. Switch per message, not per worker boot, and restore in finally. A stale base leaking into the next message is the classic production bug for this kind of bundle.
Testing. Point a test connection at SQLite or a throwaway schema and switch in setUp(); restore in tearDown(). Keep the switch out of fixtures so you don't accidentally seed the wrong base.
close() call inside the switch method or call it yourself; verify with SELECT DATABASE() after switching.$em->getMetadataFactory()->setMetadataFor(...) or use separate EMs per base.$em->clear() on switch (or on restore) to avoid saving stale objects into the wrong database.beginTransaction() / commit() will hit different PDO objects. Assert !$connection->isTransactionActive() in your manager's switch method.doctrine:migrations:migrate defaults to default; pass --em/--conn per base or wrap in a loop that switches first.Doctrine\DBAL\Connection changed substantially between 2.x and 3.x (constructor signature, fetchAllAssociative vs fetchAll). A 0-star bundle is unlikely to cover both — check composer.json constraints before adopting, and pin DBAL accordingly.PDO::ATTR_PERSISTENT can survive a switch within one PHP-FPM process and serve stale data. Disable persistence on connections you switch.info with the previous and new base pays for itself), and rely on the Symfony profiler's Doctrine panel — it shows the connection name, which makes a mis-switch obvious.services.yaml and add the guardrails (transaction check, clear(), logging) in your decorator rather than patching vendor code.src/ and to fork/pin. Assume you own maintenance of it.How can I help you explore Laravel packages today?