I have a Core config Database, each row is an \'App\' with some basic config etc.
Once you have chosen your app, I want to connect to a database using a property of that
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
);
}
}
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;
}
}
I took a look at your service and tried to implement it, but it looks like you were missing some arguments that needed passed into your constructor. Here is an updated version that should work:
#services.yml
parameters:
acme_page.dynamic_doctrine_connection.class: Acme\Bundle\PageBundle\DynamicDoctrineConnection
services:
acme_page.dynamic_doctrine_connection:
class: %acme_page.dynamic_doctrine_connection.class%
arguments: [@request, @doctrine.dbal.client_connection, @doctrine]
scope: request
calls:
- [setContainer, [@service_container]]
tags:
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest }
//DynamicDoctrineConnection.php
<?php
namespace Acme\Bundle\PageBundle;
use Symfony\Component\HttpFoundation\Request;
use Doctrine\DBAL\Connection;
use Doctrine\Bundle\DoctrineBundle\Registry;
/**
* Creates a Doctrine connection from attributes in the Request
*/
class DynamicDoctrineConnection
{
private $request;
private $defaultConnection;
private $doctrine;
public function __construct(Request $request, Connection $defaultConnection, Registry $doctrine)
{
$this->request = $request;
$this->defaultConnection = $defaultConnection;
$this->doctrine = $doctrine;
}
public function onKernelRequest()
{
if ($this->request->attributes->has('appId')) {
$dbName = 'Acme_App_'.$this->request->attributes->get('appId');
$this->defaultConnection->close();
$reflectionConn = new \ReflectionObject($this->defaultConnection);
$reflectionParams = $reflectionConn->getProperty('_params');
$reflectionParams->setAccessible(true);
$params = $reflectionParams->getValue($this->defaultConnection);
$params['dbname'] = $dbName;
$reflectionParams->setValue($this->defaultConnection, $params);
$reflectionParams->setAccessible(false);
$this->doctrine->resetEntityManager('default');
}
}
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
Symfony 4
The cleanest way by using a decorator pattern in the service definition:
First create a custom class like App\Factory\Authentication\DatabaseConnectionFactory Then this class will instantiate with an instance of doctrine.dbal.connection_factory.
#services.xml
App\Factory\Authentication\DatabaseConnectionFactory:
decorates: doctrine.dbal.connection_factory
arguments:
$wrappedConnectionFactory: '@App\Factory\Authentication\DatabaseConnectionFactory.inner'
In our custom connection factory class mock the createConnection() function and execute the original createConnection() logic by calling the function on the wrappedConnectionFactory (= doctrine.dbal.connection_factory).
/** App\Factory\Authentication\DatabaseConnectionFactory
* @param array $params
* @param Configuration|null $config
* @param EventManager|null $eventManager
* @param array $mappingTypes
*
* @throws \DomainException
*
* @return mixed
*/
public function createConnection(array $params, Configuration $config = null, EventManager $eventManager = null, array $mappingTypes = [])
{
$params['url'] = $this->databaseConnectionUrlService->getDatabaseConnectionUrlForApiUser($this->apiUser, $params['url'] );
return $this->wrappedConnectionFactory->createConnection($params, $config, $eventManager, $mappingTypes);
}