1<?php 2 3namespace Doctrine\DBAL\Connections; 4 5use Doctrine\Common\EventManager; 6use Doctrine\DBAL\Configuration; 7use Doctrine\DBAL\Connection; 8use Doctrine\DBAL\Driver; 9use Doctrine\DBAL\Driver\Connection as DriverConnection; 10use Doctrine\DBAL\Event\ConnectionEventArgs; 11use Doctrine\DBAL\Events; 12use InvalidArgumentException; 13 14use function array_rand; 15use function assert; 16use function count; 17use function func_get_args; 18 19/** 20 * Master-Slave Connection 21 * 22 * Connection can be used with master-slave setups. 23 * 24 * Important for the understanding of this connection should be how and when 25 * it picks the slave or master. 26 * 27 * 1. Slave if master was never picked before and ONLY if 'getWrappedConnection' 28 * or 'executeQuery' is used. 29 * 2. Master picked when 'exec', 'executeUpdate', 'insert', 'delete', 'update', 'createSavepoint', 30 * 'releaseSavepoint', 'beginTransaction', 'rollback', 'commit', 'query' or 31 * 'prepare' is called. 32 * 3. If master was picked once during the lifetime of the connection it will always get picked afterwards. 33 * 4. One slave connection is randomly picked ONCE during a request. 34 * 35 * ATTENTION: You can write to the slave with this connection if you execute a write query without 36 * opening up a transaction. For example: 37 * 38 * $conn = DriverManager::getConnection(...); 39 * $conn->executeQuery("DELETE FROM table"); 40 * 41 * Be aware that Connection#executeQuery is a method specifically for READ 42 * operations only. 43 * 44 * This connection is limited to slave operations using the 45 * Connection#executeQuery operation only, because it wouldn't be compatible 46 * with the ORM or SchemaManager code otherwise. Both use all the other 47 * operations in a context where writes could happen to a slave, which makes 48 * this restricted approach necessary. 49 * 50 * You can manually connect to the master at any time by calling: 51 * 52 * $conn->connect('master'); 53 * 54 * Instantiation through the DriverManager looks like: 55 * 56 * @example 57 * 58 * $conn = DriverManager::getConnection(array( 59 * 'wrapperClass' => 'Doctrine\DBAL\Connections\MasterSlaveConnection', 60 * 'driver' => 'pdo_mysql', 61 * 'master' => array('user' => '', 'password' => '', 'host' => '', 'dbname' => ''), 62 * 'slaves' => array( 63 * array('user' => 'slave1', 'password', 'host' => '', 'dbname' => ''), 64 * array('user' => 'slave2', 'password', 'host' => '', 'dbname' => ''), 65 * ) 66 * )); 67 * 68 * You can also pass 'driverOptions' and any other documented option to each of this drivers 69 * to pass additional information. 70 */ 71class MasterSlaveConnection extends Connection 72{ 73 /** 74 * Master and slave connection (one of the randomly picked slaves). 75 * 76 * @var DriverConnection[]|null[] 77 */ 78 protected $connections = ['master' => null, 'slave' => null]; 79 80 /** 81 * You can keep the slave connection and then switch back to it 82 * during the request if you know what you are doing. 83 * 84 * @var bool 85 */ 86 protected $keepSlave = false; 87 88 /** 89 * Creates Master Slave Connection. 90 * 91 * @param mixed[] $params 92 * 93 * @throws InvalidArgumentException 94 */ 95 public function __construct( 96 array $params, 97 Driver $driver, 98 ?Configuration $config = null, 99 ?EventManager $eventManager = null 100 ) { 101 if (! isset($params['slaves'], $params['master'])) { 102 throw new InvalidArgumentException('master or slaves configuration missing'); 103 } 104 105 if (count($params['slaves']) === 0) { 106 throw new InvalidArgumentException('You have to configure at least one slaves.'); 107 } 108 109 $params['master']['driver'] = $params['driver']; 110 foreach ($params['slaves'] as $slaveKey => $slave) { 111 $params['slaves'][$slaveKey]['driver'] = $params['driver']; 112 } 113 114 $this->keepSlave = (bool) ($params['keepSlave'] ?? false); 115 116 parent::__construct($params, $driver, $config, $eventManager); 117 } 118 119 /** 120 * Checks if the connection is currently towards the master or not. 121 * 122 * @return bool 123 */ 124 public function isConnectedToMaster() 125 { 126 return $this->_conn !== null && $this->_conn === $this->connections['master']; 127 } 128 129 /** 130 * @param string|null $connectionName 131 * 132 * @return bool 133 */ 134 public function connect($connectionName = null) 135 { 136 $requestedConnectionChange = ($connectionName !== null); 137 $connectionName = $connectionName ?: 'slave'; 138 139 if ($connectionName !== 'slave' && $connectionName !== 'master') { 140 throw new InvalidArgumentException('Invalid option to connect(), only master or slave allowed.'); 141 } 142 143 // If we have a connection open, and this is not an explicit connection 144 // change request, then abort right here, because we are already done. 145 // This prevents writes to the slave in case of "keepSlave" option enabled. 146 if ($this->_conn !== null && ! $requestedConnectionChange) { 147 return false; 148 } 149 150 $forceMasterAsSlave = false; 151 152 if ($this->getTransactionNestingLevel() > 0) { 153 $connectionName = 'master'; 154 $forceMasterAsSlave = true; 155 } 156 157 if (isset($this->connections[$connectionName])) { 158 $this->_conn = $this->connections[$connectionName]; 159 160 if ($forceMasterAsSlave && ! $this->keepSlave) { 161 $this->connections['slave'] = $this->_conn; 162 } 163 164 return false; 165 } 166 167 if ($connectionName === 'master') { 168 $this->connections['master'] = $this->_conn = $this->connectTo($connectionName); 169 170 // Set slave connection to master to avoid invalid reads 171 if (! $this->keepSlave) { 172 $this->connections['slave'] = $this->connections['master']; 173 } 174 } else { 175 $this->connections['slave'] = $this->_conn = $this->connectTo($connectionName); 176 } 177 178 if ($this->_eventManager->hasListeners(Events::postConnect)) { 179 $eventArgs = new ConnectionEventArgs($this); 180 $this->_eventManager->dispatchEvent(Events::postConnect, $eventArgs); 181 } 182 183 return true; 184 } 185 186 /** 187 * Connects to a specific connection. 188 * 189 * @param string $connectionName 190 * 191 * @return DriverConnection 192 */ 193 protected function connectTo($connectionName) 194 { 195 $params = $this->getParams(); 196 197 $driverOptions = $params['driverOptions'] ?? []; 198 199 $connectionParams = $this->chooseConnectionConfiguration($connectionName, $params); 200 201 $user = $connectionParams['user'] ?? null; 202 $password = $connectionParams['password'] ?? null; 203 204 return $this->_driver->connect($connectionParams, $user, $password, $driverOptions); 205 } 206 207 /** 208 * @param string $connectionName 209 * @param mixed[] $params 210 * 211 * @return mixed 212 */ 213 protected function chooseConnectionConfiguration($connectionName, $params) 214 { 215 if ($connectionName === 'master') { 216 return $params['master']; 217 } 218 219 $config = $params['slaves'][array_rand($params['slaves'])]; 220 221 if (! isset($config['charset']) && isset($params['master']['charset'])) { 222 $config['charset'] = $params['master']['charset']; 223 } 224 225 return $config; 226 } 227 228 /** 229 * {@inheritDoc} 230 */ 231 public function executeUpdate($sql, array $params = [], array $types = []) 232 { 233 $this->connect('master'); 234 235 return parent::executeUpdate($sql, $params, $types); 236 } 237 238 /** 239 * {@inheritDoc} 240 */ 241 public function beginTransaction() 242 { 243 $this->connect('master'); 244 245 return parent::beginTransaction(); 246 } 247 248 /** 249 * {@inheritDoc} 250 */ 251 public function commit() 252 { 253 $this->connect('master'); 254 255 return parent::commit(); 256 } 257 258 /** 259 * {@inheritDoc} 260 */ 261 public function rollBack() 262 { 263 $this->connect('master'); 264 265 return parent::rollBack(); 266 } 267 268 /** 269 * {@inheritDoc} 270 */ 271 public function delete($table, array $identifier, array $types = []) 272 { 273 $this->connect('master'); 274 275 return parent::delete($table, $identifier, $types); 276 } 277 278 /** 279 * {@inheritDoc} 280 */ 281 public function close() 282 { 283 unset($this->connections['master'], $this->connections['slave']); 284 285 parent::close(); 286 287 $this->_conn = null; 288 $this->connections = ['master' => null, 'slave' => null]; 289 } 290 291 /** 292 * {@inheritDoc} 293 */ 294 public function update($table, array $data, array $identifier, array $types = []) 295 { 296 $this->connect('master'); 297 298 return parent::update($table, $data, $identifier, $types); 299 } 300 301 /** 302 * {@inheritDoc} 303 */ 304 public function insert($table, array $data, array $types = []) 305 { 306 $this->connect('master'); 307 308 return parent::insert($table, $data, $types); 309 } 310 311 /** 312 * {@inheritDoc} 313 */ 314 public function exec($sql) 315 { 316 $this->connect('master'); 317 318 return parent::exec($sql); 319 } 320 321 /** 322 * {@inheritDoc} 323 */ 324 public function createSavepoint($savepoint) 325 { 326 $this->connect('master'); 327 328 parent::createSavepoint($savepoint); 329 } 330 331 /** 332 * {@inheritDoc} 333 */ 334 public function releaseSavepoint($savepoint) 335 { 336 $this->connect('master'); 337 338 parent::releaseSavepoint($savepoint); 339 } 340 341 /** 342 * {@inheritDoc} 343 */ 344 public function rollbackSavepoint($savepoint) 345 { 346 $this->connect('master'); 347 348 parent::rollbackSavepoint($savepoint); 349 } 350 351 /** 352 * {@inheritDoc} 353 */ 354 public function query() 355 { 356 $this->connect('master'); 357 assert($this->_conn instanceof DriverConnection); 358 359 $args = func_get_args(); 360 361 $logger = $this->getConfiguration()->getSQLLogger(); 362 if ($logger) { 363 $logger->startQuery($args[0]); 364 } 365 366 $statement = $this->_conn->query(...$args); 367 368 $statement->setFetchMode($this->defaultFetchMode); 369 370 if ($logger) { 371 $logger->stopQuery(); 372 } 373 374 return $statement; 375 } 376 377 /** 378 * {@inheritDoc} 379 */ 380 public function prepare($sql) 381 { 382 $this->connect('master'); 383 384 return parent::prepare($sql); 385 } 386} 387