Symfony2, Dynamic DB Connection/Early override of Doctrine Service
Combined, these two postings helped me solve my own very similar problem. Here is my solution, maybe it is useful for someone else:
<?php
namespace Calitarus\CollaborationBundle\EventListener;
use Symfony\Component\HttpFoundation\Request;
use Doctrine\DBAL\Connection;
use Exception;
use Monolog\Logger;
class DatabaseSwitcherEventListener {
private $request;
private $connection;
private $logger;
public function __construct(Request $request, Connection $connection, Logger $logger) {
$this->request = $request;
$this->connection = $connection;
$this->logger = $logger;
}
public function onKernelRequest() {
if ($this->request->attributes->has('_site')) {
$site = $this->request->attributes->get('_site');
$connection = $this->connection;
$params = $this->connection->getParams();
$db_name = 'br_'.$this->request->attributes->get('_site');
// TODO: validate that this site exists
if ($db_name != $params['dbname']) {
$this->logger->debug('switching connection from '.$params['dbname'].' to '.$db_name);
$params['dbname'] = $db_name;
if ($connection->isConnected()) {
$connection->close();
}
$connection->__construct(
$params, $connection->getDriver(), $connection->getConfiguration(),
$connection->getEventManager()
);
try {
$connection->connect();
} catch (Exception $e) {
// log and handle exception
}
}
}
}
}
To get this to work, I set up services.yml as follows:
services:
cc.database_switcher:
class: Calitarus\CollaborationBundle\EventListener\DatabaseSwitcherEventListener
arguments: [@request, @doctrine.dbal.default_connection, @logger]
scope: request
tags:
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest }
and I have this routing configuration to get the _site parameter, which in my case is part of the URL, but you can probably get it in other ways depending on your setup:
resource: "@CCollabBundle/Controller"
type: annotation
prefix: /{_site}
defaults:
_site: default
Here is the new and improved non-reflection version
#services.yml
acme_app.dynamic_connection:
class: %acme.dynamic_doctrine_connection.class%
calls:
- [setDoctrineConnection, [@doctrine.dbal.default_connection]]
<?php
namespace Acme\Bundle\AppBundle;
use Doctrine\DBAL\Connection;
use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException;
use Exception;
class DynamicDoctrineConnection
{
/**
* @var Connection
*/
private $connection;
/**
* Sets the DB Name prefix to use when selecting the database to connect to
*
* @param Connection $connection
* @return SiteDbConnection $this
*/
public function setDoctrineConnection(Connection $connection)
{
$this->connection = $connection;
return $this;
}
public function setUpAppConnection()
{
if ($this->request->attributes->has('appId')) {
$connection = $this->connection;
$params = $this->connection->getParams();
// we also check if the current connection needs to be closed based on various things
// have left that part in for information here
// $appId changed from that in the connection?
// if ($connection->isConnected()) {
// $connection->close();
// }
// Set default DB connection using appId
//$params['host'] = $someHost;
$params['dbname'] = 'Acme_App'.$this->request->attributes->get('appId');
// Set up the parameters for the parent
$connection->__construct(
$params, $connection->getDriver(), $connection->getConfiguration(),
$connection->getEventManager()
);
try {
$connection->connect();
} catch (Exception $e) {
// log and handle exception
}
}
return $this;
}
}
In symfony 4, you can pull it off with a wrapper class:
# doctrine.yaml
doctrine:
dbal:
connections:
default:
wrapper_class: App\Service\Database\DynamicConnection
The class simply extends the original Connection:
class DynamicConnection extends \Doctrine\DBAL\Connection
{
public function changeDatabase(string $dbName)
{
$params = $this->getParams();
if ($this->isConnected())
$this->close();
if (isset($params['url'])) {
$params['url'] = preg_replace(
sprintf("/(?<=\/)%s/", preg_quote($this->getDatabase())),
$dbName,
$params['url']
);
}
if (isset($params['dbname']))
$params['dbname'] = $dbName;
parent::__construct(
$params,
$this->_driver,
$this->_config,
$this->_eventManager
);
}
}