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