summaryrefslogtreecommitdiff
path: root/vendor/doctrine/dbal/src/Connections/PrimaryReadReplicaConnection.php
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/doctrine/dbal/src/Connections/PrimaryReadReplicaConnection.php')
-rw-r--r--vendor/doctrine/dbal/src/Connections/PrimaryReadReplicaConnection.php327
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
3declare(strict_types=1);
4
5namespace Doctrine\DBAL\Connections;
6
7use Doctrine\DBAL\Configuration;
8use Doctrine\DBAL\Connection;
9use Doctrine\DBAL\Driver;
10use Doctrine\DBAL\Driver\Connection as DriverConnection;
11use Doctrine\DBAL\Driver\Exception as DriverException;
12use Doctrine\DBAL\DriverManager;
13use Doctrine\DBAL\Exception;
14use Doctrine\DBAL\Statement;
15use InvalidArgumentException;
16use SensitiveParameter;
17
18use function array_rand;
19use function assert;
20use 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 */
78class 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}