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 | } | ||