<?php
namespace Jarlssen\Doctrine2\DBAL;
use Doctrine\DBAL\Connection,
Doctrine\DBAL\Driver,
Doctrine\ORM\Configuration,
Doctrine\Common\EventManager,
Doctrine\DBAL\Events;
class MasterSlaveConnection extends Connection
{
/**
* Master connection
*
* @var Doctrine\DBAL\Driver\Connection
*/
protected $_masterConn;
/**
* Slave connection
*
* @var Doctrine\DBAL\Driver\Connection
*/
protected $_slaveConn;
public function __construct(
array $params,
Driver $driver,
Configuration $config = null,
EventManager $eventManager = null
)
{
if (!isset($params['slaves']) or !isset($params['master'])) {
throw new \InvalidArgumentsException('master or slaves configuration missing');
}
$params['master']['driver'] = $params['driver'];
foreach ($params['slaves'] as &$slave) {
$slave['driver'] = $params['driver'];
}
parent::__construct($params, $driver, $config, $eventManager);
}
public function connect($connectionName = 'slave')
{
$forceMasterAsSlave = false;
if ($this->getTransactionNestingLevel() > 0) {
$connectionName = 'master';
$forceMasterAsSlave = true;
}
$connectionProperty = '_' . $connectionName . 'Conn';
if ($this->{$connectionProperty}) {
if ($forceMasterAsSlave) {
$this->_slaveConn = $this->_conn = $this->_masterConn;
} else {
$this->_conn = $this->{$connectionProperty};
}
return false;
}
if ($connectionName === 'master') {
/** Set master and slave connection to master to avoid invalid reads */
$this->_masterConn = $this->_slaveConn = $this->_conn = $this->_connectTo($connectionName);
} else {
$this->_slaveConn = $this->_conn = $this->_connectTo($connectionName);
}
if ($this->_eventManager->hasListeners(Events::postConnect)) {
$eventArgs = new Event\ConnectionEventArgs($this);
$this->_eventManager->dispatchEvent(Events::postConnect, $eventArgs);
}
return true;
}
protected function _connectTo($connectionName)
{
$params = $this->getParams();
$driverOptions = isset($params['driverOptions']) ? $params['driverOptions'] : array();
$connectionParams = $this->_chooseConnectionConfiguration($connectionName, $params);
$user = isset($connectionParams['user']) ? $connectionParams['user'] : null;
$password = isset($connectionParams['password']) ? $connectionParams['password'] : null;
return $this->_driver->connect($connectionParams, $user, $password, $driverOptions);
}
protected function _chooseConnectionConfiguration($connectionName, $params)
{
if ($connectionName === 'master') {
return $params['master'];
}
return $params['slaves'][array_rand($params['slaves'])];
}
public function executeUpdate($query, array $params = array(), array $types = array())
{
try {
if (strpos(strtolower($query), 'delete from member_profile') === 0) {
throw new \Exception($query);
}
$this->connect('master');
return parent::executeUpdate($query, $params, $types);
} catch(\Exception $e) {
$errorLog = 'error_'.'log';
$errorLog(__METHOD__);
$errorLog($e->getMessage());
$errorLog($e->getTraceAsString());
return false;
}
}
public function beginTransaction()
{
$this->connect('master');
return parent::beginTransaction();
}
public function commit()
{
$this->connect('master');
return parent::commit();
}
public function rollback()
{
$this->connect('master');
return parent::rollback();
}
public function delete($tableName, array $identifier)
{
$this->connect('master');
return parent::delete($tableName, $identifier);
}
public function update($tableName, array $data, array $identifier)
{
$this->connect('master');
return parent::update($tableName, $data, $identifier);
}
public function insert($tableName, array $data)
{
$this->connect('master');
return parent::insert($tableName, $data);
}
public function exec($statement)
{
$this->connect('master');
return parent::exec($statement);
}
public function getWrappedConnection()
{
$this->connect('master');
return $this->_conn;
}
}
I have done some working code, I extended \Doctrine\DBAL\Connection:
From the controllers/ service layer:
$em->getConnection()->setConnection('write'); try { $em->persist($user); } catch (Exception $e) { echo $e->getMessage(); } $em->getConnection()->setConnection('read_1'); ... $em->getConnection()->setConnection('read_2');Obviously all the statements related with a unit of work will be associated with only one connection.
_________________________________________
When setting the event manager:
$connectionOptions = array( 'driver' => $doctrineConfig['conn']['driv'], 'user' => $doctrineConfig['conn']['user'], 'password' => $doctrineConfig['conn']['pass'], 'dbname' => $doctrineConfig['conn']['dbname'], 'host' => $doctrineConfig['conn']['host'], 'wrapperClass' =>'\Fishpond\Doctrine\DBAL\Connections\Multiple' ); $connectionOptions['multiple_connections']['read'] = array( "driver" => "pdo_mysql", "user" = "password" => "dbname"=> "host" => ); $connectionOptions['multiple_connections']['write'] = array( "driver" => "pdo_mysql", "user" => "password" => "dbname"=> "host" => ); $em = \Doctrine\ORM\EntityManager::create($connectionOptions, $config);_________________________________________
The new connection class itself:
..........
........
The class is working so far (need much more testing), but the idea is if is not selected a connection it will work as it is now. Also, an 'automatic' selection of the connection could happen in an event listener, where we could define rules about which connection should be used for different conditions/entities.