diff options
Diffstat (limited to 'vendor/doctrine/dbal/src/Connections/PrimaryReadReplicaConnection.php')
| -rw-r--r-- | vendor/doctrine/dbal/src/Connections/PrimaryReadReplicaConnection.php | 327 |
1 files changed, 327 insertions, 0 deletions
diff --git a/vendor/doctrine/dbal/src/Connections/PrimaryReadReplicaConnection.php b/vendor/doctrine/dbal/src/Connections/PrimaryReadReplicaConnection.php new file mode 100644 index 0000000..1d9c1a9 --- /dev/null +++ b/vendor/doctrine/dbal/src/Connections/PrimaryReadReplicaConnection.php | |||
| @@ -0,0 +1,327 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\DBAL\Connections; | ||
| 6 | |||
| 7 | use Doctrine\DBAL\Configuration; | ||
| 8 | use Doctrine\DBAL\Connection; | ||
| 9 | use Doctrine\DBAL\Driver; | ||
| 10 | use Doctrine\DBAL\Driver\Connection as DriverConnection; | ||
| 11 | use Doctrine\DBAL\Driver\Exception as DriverException; | ||
| 12 | use Doctrine\DBAL\DriverManager; | ||
| 13 | use Doctrine\DBAL\Exception; | ||
| 14 | use Doctrine\DBAL\Statement; | ||
| 15 | use InvalidArgumentException; | ||
| 16 | use SensitiveParameter; | ||
| 17 | |||
| 18 | use function array_rand; | ||
| 19 | use function assert; | ||
| 20 | use function count; | ||
| 21 | |||
| 22 | /** | ||
| 23 | * Primary-Replica Connection | ||
| 24 | * | ||
| 25 | * Connection can be used with primary-replica setups. | ||
| 26 | * | ||
| 27 | * Important for the understanding of this connection should be how and when | ||
| 28 | * it picks the replica or primary. | ||
| 29 | * | ||
| 30 | * 1. Replica if primary was never picked before and ONLY if 'getWrappedConnection' | ||
| 31 | * or 'executeQuery' is used. | ||
| 32 | * 2. Primary picked when 'executeStatement', 'insert', 'delete', 'update', 'createSavepoint', | ||
| 33 | * 'releaseSavepoint', 'beginTransaction', 'rollback', 'commit' or 'prepare' is called. | ||
| 34 | * 3. If Primary was picked once during the lifetime of the connection it will always get picked afterwards. | ||
| 35 | * 4. One replica connection is randomly picked ONCE during a request. | ||
| 36 | * | ||
| 37 | * ATTENTION: You can write to the replica with this connection if you execute a write query without | ||
| 38 | * opening up a transaction. For example: | ||
| 39 | * | ||
| 40 | * $conn = DriverManager::getConnection(...); | ||
| 41 | * $conn->executeQuery("DELETE FROM table"); | ||
| 42 | * | ||
| 43 | * Be aware that Connection#executeQuery is a method specifically for READ | ||
| 44 | * operations only. | ||
| 45 | * | ||
| 46 | * Use Connection#executeStatement for any SQL statement that changes/updates | ||
| 47 | * state in the database (UPDATE, INSERT, DELETE or DDL statements). | ||
| 48 | * | ||
| 49 | * This connection is limited to replica operations using the | ||
| 50 | * Connection#executeQuery operation only, because it wouldn't be compatible | ||
| 51 | * with the ORM or SchemaManager code otherwise. Both use all the other | ||
| 52 | * operations in a context where writes could happen to a replica, which makes | ||
| 53 | * this restricted approach necessary. | ||
| 54 | * | ||
| 55 | * You can manually connect to the primary at any time by calling: | ||
| 56 | * | ||
| 57 | * $conn->ensureConnectedToPrimary(); | ||
| 58 | * | ||
| 59 | * Instantiation through the DriverManager looks like: | ||
| 60 | * | ||
| 61 | * @psalm-import-type Params from DriverManager | ||
| 62 | * @psalm-import-type OverrideParams from DriverManager | ||
| 63 | * @example | ||
| 64 | * | ||
| 65 | * $conn = DriverManager::getConnection(array( | ||
| 66 | * 'wrapperClass' => 'Doctrine\DBAL\Connections\PrimaryReadReplicaConnection', | ||
| 67 | * 'driver' => 'pdo_mysql', | ||
| 68 | * 'primary' => array('user' => '', 'password' => '', 'host' => '', 'dbname' => ''), | ||
| 69 | * 'replica' => array( | ||
| 70 | * array('user' => 'replica1', 'password' => '', 'host' => '', 'dbname' => ''), | ||
| 71 | * array('user' => 'replica2', 'password' => '', 'host' => '', 'dbname' => ''), | ||
| 72 | * ) | ||
| 73 | * )); | ||
| 74 | * | ||
| 75 | * You can also pass 'driverOptions' and any other documented option to each of this drivers | ||
| 76 | * to pass additional information. | ||
| 77 | */ | ||
| 78 | class PrimaryReadReplicaConnection extends Connection | ||
| 79 | { | ||
| 80 | /** | ||
| 81 | * Primary and Replica connection (one of the randomly picked replicas). | ||
| 82 | * | ||
| 83 | * @var array<string, DriverConnection|null> | ||
| 84 | */ | ||
| 85 | protected array $connections = ['primary' => null, 'replica' => null]; | ||
| 86 | |||
| 87 | /** | ||
| 88 | * You can keep the replica connection and then switch back to it | ||
| 89 | * during the request if you know what you are doing. | ||
| 90 | */ | ||
| 91 | protected bool $keepReplica = false; | ||
| 92 | |||
| 93 | /** | ||
| 94 | * Creates Primary Replica Connection. | ||
| 95 | * | ||
| 96 | * @internal The connection can be only instantiated by the driver manager. | ||
| 97 | * | ||
| 98 | * @param array<string, mixed> $params | ||
| 99 | * @psalm-param Params $params | ||
| 100 | */ | ||
| 101 | public function __construct(array $params, Driver $driver, ?Configuration $config = null) | ||
| 102 | { | ||
| 103 | if (! isset($params['replica'], $params['primary'])) { | ||
| 104 | throw new InvalidArgumentException('primary or replica configuration missing'); | ||
| 105 | } | ||
| 106 | |||
| 107 | if (count($params['replica']) === 0) { | ||
| 108 | throw new InvalidArgumentException('You have to configure at least one replica.'); | ||
| 109 | } | ||
| 110 | |||
| 111 | if (isset($params['driver'])) { | ||
| 112 | $params['primary']['driver'] = $params['driver']; | ||
| 113 | |||
| 114 | foreach ($params['replica'] as $replicaKey => $replica) { | ||
| 115 | $params['replica'][$replicaKey]['driver'] = $params['driver']; | ||
| 116 | } | ||
| 117 | } | ||
| 118 | |||
| 119 | $this->keepReplica = ! empty($params['keepReplica']); | ||
| 120 | |||
| 121 | parent::__construct($params, $driver, $config); | ||
| 122 | } | ||
| 123 | |||
| 124 | /** | ||
| 125 | * Checks if the connection is currently towards the primary or not. | ||
| 126 | */ | ||
| 127 | public function isConnectedToPrimary(): bool | ||
| 128 | { | ||
| 129 | return $this->_conn !== null && $this->_conn === $this->connections['primary']; | ||
| 130 | } | ||
| 131 | |||
| 132 | public function connect(?string $connectionName = null): DriverConnection | ||
| 133 | { | ||
| 134 | if ($connectionName !== null) { | ||
| 135 | throw new InvalidArgumentException( | ||
| 136 | 'Passing a connection name as first argument is not supported anymore.' | ||
| 137 | . ' Use ensureConnectedToPrimary()/ensureConnectedToReplica() instead.', | ||
| 138 | ); | ||
| 139 | } | ||
| 140 | |||
| 141 | return $this->performConnect(); | ||
| 142 | } | ||
| 143 | |||
| 144 | protected function performConnect(?string $connectionName = null): DriverConnection | ||
| 145 | { | ||
| 146 | $requestedConnectionChange = ($connectionName !== null); | ||
| 147 | $connectionName ??= 'replica'; | ||
| 148 | |||
| 149 | if ($connectionName !== 'replica' && $connectionName !== 'primary') { | ||
| 150 | throw new InvalidArgumentException('Invalid option to connect(), only primary or replica allowed.'); | ||
| 151 | } | ||
| 152 | |||
| 153 | // If we have a connection open, and this is not an explicit connection | ||
| 154 | // change request, then abort right here, because we are already done. | ||
| 155 | // This prevents writes to the replica in case of "keepReplica" option enabled. | ||
| 156 | if ($this->_conn !== null && ! $requestedConnectionChange) { | ||
| 157 | return $this->_conn; | ||
| 158 | } | ||
| 159 | |||
| 160 | $forcePrimaryAsReplica = false; | ||
| 161 | |||
| 162 | if ($this->getTransactionNestingLevel() > 0) { | ||
| 163 | $connectionName = 'primary'; | ||
| 164 | $forcePrimaryAsReplica = true; | ||
| 165 | } | ||
| 166 | |||
| 167 | if (isset($this->connections[$connectionName])) { | ||
| 168 | $this->_conn = $this->connections[$connectionName]; | ||
| 169 | |||
| 170 | if ($forcePrimaryAsReplica && ! $this->keepReplica) { | ||
| 171 | $this->connections['replica'] = $this->_conn; | ||
| 172 | } | ||
| 173 | |||
| 174 | return $this->_conn; | ||
| 175 | } | ||
| 176 | |||
| 177 | if ($connectionName === 'primary') { | ||
| 178 | $this->connections['primary'] = $this->_conn = $this->connectTo($connectionName); | ||
| 179 | |||
| 180 | // Set replica connection to primary to avoid invalid reads | ||
| 181 | if (! $this->keepReplica) { | ||
| 182 | $this->connections['replica'] = $this->connections['primary']; | ||
| 183 | } | ||
| 184 | } else { | ||
| 185 | $this->connections['replica'] = $this->_conn = $this->connectTo($connectionName); | ||
| 186 | } | ||
| 187 | |||
| 188 | return $this->_conn; | ||
| 189 | } | ||
| 190 | |||
| 191 | /** | ||
| 192 | * Connects to the primary node of the database cluster. | ||
| 193 | * | ||
| 194 | * All following statements after this will be executed against the primary node. | ||
| 195 | */ | ||
| 196 | public function ensureConnectedToPrimary(): void | ||
| 197 | { | ||
| 198 | $this->performConnect('primary'); | ||
| 199 | } | ||
| 200 | |||
| 201 | /** | ||
| 202 | * Connects to a replica node of the database cluster. | ||
| 203 | * | ||
| 204 | * All following statements after this will be executed against the replica node, | ||
| 205 | * unless the keepReplica option is set to false and a primary connection | ||
| 206 | * was already opened. | ||
| 207 | */ | ||
| 208 | public function ensureConnectedToReplica(): void | ||
| 209 | { | ||
| 210 | $this->performConnect('replica'); | ||
| 211 | } | ||
| 212 | |||
| 213 | /** | ||
| 214 | * Connects to a specific connection. | ||
| 215 | * | ||
| 216 | * @throws Exception | ||
| 217 | */ | ||
| 218 | protected function connectTo(string $connectionName): DriverConnection | ||
| 219 | { | ||
| 220 | $params = $this->getParams(); | ||
| 221 | assert(isset($params['primary'])); | ||
| 222 | |||
| 223 | if ($connectionName === 'primary') { | ||
| 224 | $connectionParams = $params['primary']; | ||
| 225 | } else { | ||
| 226 | assert(isset($params['replica'])); | ||
| 227 | $connectionParams = $this->chooseReplicaConnectionParameters($params['primary'], $params['replica']); | ||
| 228 | } | ||
| 229 | |||
| 230 | try { | ||
| 231 | return $this->driver->connect($connectionParams); | ||
| 232 | } catch (DriverException $e) { | ||
| 233 | throw $this->convertException($e); | ||
| 234 | } | ||
| 235 | } | ||
| 236 | |||
| 237 | /** | ||
| 238 | * @param OverrideParams $primary | ||
| 239 | * @param array<OverrideParams> $replicas | ||
| 240 | * | ||
| 241 | * @return array<string, mixed> | ||
| 242 | * @psalm-return OverrideParams | ||
| 243 | */ | ||
| 244 | protected function chooseReplicaConnectionParameters( | ||
| 245 | #[SensitiveParameter] | ||
| 246 | array $primary, | ||
| 247 | #[SensitiveParameter] | ||
| 248 | array $replicas, | ||
| 249 | ): array { | ||
| 250 | $params = $replicas[array_rand($replicas)]; | ||
| 251 | |||
| 252 | if (! isset($params['charset']) && isset($primary['charset'])) { | ||
| 253 | $params['charset'] = $primary['charset']; | ||
| 254 | } | ||
| 255 | |||
| 256 | return $params; | ||
| 257 | } | ||
| 258 | |||
| 259 | /** | ||
| 260 | * {@inheritDoc} | ||
| 261 | */ | ||
| 262 | public function executeStatement(string $sql, array $params = [], array $types = []): int|string | ||
| 263 | { | ||
| 264 | $this->ensureConnectedToPrimary(); | ||
| 265 | |||
| 266 | return parent::executeStatement($sql, $params, $types); | ||
| 267 | } | ||
| 268 | |||
| 269 | public function beginTransaction(): void | ||
| 270 | { | ||
| 271 | $this->ensureConnectedToPrimary(); | ||
| 272 | |||
| 273 | parent::beginTransaction(); | ||
| 274 | } | ||
| 275 | |||
| 276 | public function commit(): void | ||
| 277 | { | ||
| 278 | $this->ensureConnectedToPrimary(); | ||
| 279 | |||
| 280 | parent::commit(); | ||
| 281 | } | ||
| 282 | |||
| 283 | public function rollBack(): void | ||
| 284 | { | ||
| 285 | $this->ensureConnectedToPrimary(); | ||
| 286 | |||
| 287 | parent::rollBack(); | ||
| 288 | } | ||
| 289 | |||
| 290 | public function close(): void | ||
| 291 | { | ||
| 292 | unset($this->connections['primary'], $this->connections['replica']); | ||
| 293 | |||
| 294 | parent::close(); | ||
| 295 | |||
| 296 | $this->_conn = null; | ||
| 297 | $this->connections = ['primary' => null, 'replica' => null]; | ||
| 298 | } | ||
| 299 | |||
| 300 | public function createSavepoint(string $savepoint): void | ||
| 301 | { | ||
| 302 | $this->ensureConnectedToPrimary(); | ||
| 303 | |||
| 304 | parent::createSavepoint($savepoint); | ||
| 305 | } | ||
| 306 | |||
| 307 | public function releaseSavepoint(string $savepoint): void | ||
| 308 | { | ||
| 309 | $this->ensureConnectedToPrimary(); | ||
| 310 | |||
| 311 | parent::releaseSavepoint($savepoint); | ||
| 312 | } | ||
| 313 | |||
| 314 | public function rollbackSavepoint(string $savepoint): void | ||
| 315 | { | ||
| 316 | $this->ensureConnectedToPrimary(); | ||
| 317 | |||
| 318 | parent::rollbackSavepoint($savepoint); | ||
| 319 | } | ||
| 320 | |||
| 321 | public function prepare(string $sql): Statement | ||
| 322 | { | ||
| 323 | $this->ensureConnectedToPrimary(); | ||
| 324 | |||
| 325 | return parent::prepare($sql); | ||
| 326 | } | ||
| 327 | } | ||
