Subversion-Projekte lars-tiefland.laravel_shop

Revision

Revision 148 | Details | Vergleich mit vorheriger | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
148 lars 1
<?php
2
 
3
namespace Doctrine\DBAL\Connections;
4
 
5
use Doctrine\Common\EventManager;
6
use Doctrine\DBAL\Configuration;
7
use Doctrine\DBAL\Connection;
8
use Doctrine\DBAL\Driver;
9
use Doctrine\DBAL\Driver\Connection as DriverConnection;
10
use Doctrine\DBAL\Driver\Exception as DriverException;
11
use Doctrine\DBAL\DriverManager;
12
use Doctrine\DBAL\Event\ConnectionEventArgs;
13
use Doctrine\DBAL\Events;
14
use Doctrine\DBAL\Exception;
15
use Doctrine\DBAL\Statement;
16
use Doctrine\Deprecations\Deprecation;
17
use InvalidArgumentException;
621 lars 18
use SensitiveParameter;
148 lars 19
 
20
use function array_rand;
21
use function count;
22
 
23
/**
24
 * Primary-Replica Connection
25
 *
26
 * Connection can be used with primary-replica setups.
27
 *
28
 * Important for the understanding of this connection should be how and when
29
 * it picks the replica or primary.
30
 *
31
 * 1. Replica if primary was never picked before and ONLY if 'getWrappedConnection'
32
 *    or 'executeQuery' is used.
33
 * 2. Primary picked when 'executeStatement', 'insert', 'delete', 'update', 'createSavepoint',
34
 *    'releaseSavepoint', 'beginTransaction', 'rollback', 'commit' or 'prepare' is called.
35
 * 3. If Primary was picked once during the lifetime of the connection it will always get picked afterwards.
36
 * 4. One replica connection is randomly picked ONCE during a request.
37
 *
38
 * ATTENTION: You can write to the replica with this connection if you execute a write query without
39
 * opening up a transaction. For example:
40
 *
41
 *      $conn = DriverManager::getConnection(...);
42
 *      $conn->executeQuery("DELETE FROM table");
43
 *
44
 * Be aware that Connection#executeQuery is a method specifically for READ
45
 * operations only.
46
 *
47
 * Use Connection#executeStatement for any SQL statement that changes/updates
48
 * state in the database (UPDATE, INSERT, DELETE or DDL statements).
49
 *
50
 * This connection is limited to replica operations using the
51
 * Connection#executeQuery operation only, because it wouldn't be compatible
52
 * with the ORM or SchemaManager code otherwise. Both use all the other
53
 * operations in a context where writes could happen to a replica, which makes
54
 * this restricted approach necessary.
55
 *
56
 * You can manually connect to the primary at any time by calling:
57
 *
58
 *      $conn->ensureConnectedToPrimary();
59
 *
60
 * Instantiation through the DriverManager looks like:
61
 *
62
 * @psalm-import-type Params 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 DriverConnection[]|null[]
84
     */
85
    protected $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
     * @var bool
92
     */
93
    protected $keepReplica = false;
94
 
95
    /**
96
     * Creates Primary Replica Connection.
97
     *
98
     * @internal The connection can be only instantiated by the driver manager.
99
     *
100
     * @param array<string,mixed> $params
101
     * @psalm-param Params $params
102
     *
103
     * @throws Exception
104
     * @throws InvalidArgumentException
105
     */
106
    public function __construct(
107
        array $params,
108
        Driver $driver,
109
        ?Configuration $config = null,
110
        ?EventManager $eventManager = null
111
    ) {
112
        if (! isset($params['replica'], $params['primary'])) {
113
            throw new InvalidArgumentException('primary or replica configuration missing');
114
        }
115
 
116
        if (count($params['replica']) === 0) {
117
            throw new InvalidArgumentException('You have to configure at least one replica.');
118
        }
119
 
120
        if (isset($params['driver'])) {
121
            $params['primary']['driver'] = $params['driver'];
122
 
123
            foreach ($params['replica'] as $replicaKey => $replica) {
124
                $params['replica'][$replicaKey]['driver'] = $params['driver'];
125
            }
126
        }
127
 
128
        $this->keepReplica = (bool) ($params['keepReplica'] ?? false);
129
 
130
        parent::__construct($params, $driver, $config, $eventManager);
131
    }
132
 
133
    /**
134
     * Checks if the connection is currently towards the primary or not.
135
     */
136
    public function isConnectedToPrimary(): bool
137
    {
138
        return $this->_conn !== null && $this->_conn === $this->connections['primary'];
139
    }
140
 
141
    /**
142
     * @param string|null $connectionName
143
     *
144
     * @return bool
145
     */
146
    public function connect($connectionName = null)
147
    {
148
        if ($connectionName !== null) {
149
            throw new InvalidArgumentException(
150
                'Passing a connection name as first argument is not supported anymore.'
151
                    . ' Use ensureConnectedToPrimary()/ensureConnectedToReplica() instead.',
152
            );
153
        }
154
 
155
        return $this->performConnect();
156
    }
157
 
158
    protected function performConnect(?string $connectionName = null): bool
159
    {
160
        $requestedConnectionChange = ($connectionName !== null);
161
        $connectionName            = $connectionName ?? 'replica';
162
 
163
        if ($connectionName !== 'replica' && $connectionName !== 'primary') {
164
            throw new InvalidArgumentException('Invalid option to connect(), only primary or replica allowed.');
165
        }
166
 
167
        // If we have a connection open, and this is not an explicit connection
168
        // change request, then abort right here, because we are already done.
169
        // This prevents writes to the replica in case of "keepReplica" option enabled.
170
        if ($this->_conn !== null && ! $requestedConnectionChange) {
171
            return false;
172
        }
173
 
174
        $forcePrimaryAsReplica = false;
175
 
176
        if ($this->getTransactionNestingLevel() > 0) {
177
            $connectionName        = 'primary';
178
            $forcePrimaryAsReplica = true;
179
        }
180
 
181
        if (isset($this->connections[$connectionName])) {
182
            $this->_conn = $this->connections[$connectionName];
183
 
184
            if ($forcePrimaryAsReplica && ! $this->keepReplica) {
185
                $this->connections['replica'] = $this->_conn;
186
            }
187
 
188
            return false;
189
        }
190
 
191
        if ($connectionName === 'primary') {
192
            $this->connections['primary'] = $this->_conn = $this->connectTo($connectionName);
193
 
194
            // Set replica connection to primary to avoid invalid reads
195
            if (! $this->keepReplica) {
196
                $this->connections['replica'] = $this->connections['primary'];
197
            }
198
        } else {
199
            $this->connections['replica'] = $this->_conn = $this->connectTo($connectionName);
200
        }
201
 
202
        if ($this->_eventManager->hasListeners(Events::postConnect)) {
203
            Deprecation::trigger(
204
                'doctrine/dbal',
205
                'https://github.com/doctrine/dbal/issues/5784',
206
                'Subscribing to %s events is deprecated. Implement a middleware instead.',
207
                Events::postConnect,
208
            );
209
 
210
            $eventArgs = new ConnectionEventArgs($this);
211
            $this->_eventManager->dispatchEvent(Events::postConnect, $eventArgs);
212
        }
213
 
214
        return true;
215
    }
216
 
217
    /**
218
     * Connects to the primary node of the database cluster.
219
     *
220
     * All following statements after this will be executed against the primary node.
221
     */
222
    public function ensureConnectedToPrimary(): bool
223
    {
224
        return $this->performConnect('primary');
225
    }
226
 
227
    /**
228
     * Connects to a replica node of the database cluster.
229
     *
230
     * All following statements after this will be executed against the replica node,
231
     * unless the keepReplica option is set to false and a primary connection
232
     * was already opened.
233
     */
234
    public function ensureConnectedToReplica(): bool
235
    {
236
        return $this->performConnect('replica');
237
    }
238
 
239
    /**
240
     * Connects to a specific connection.
241
     *
242
     * @param string $connectionName
243
     *
244
     * @return DriverConnection
245
     *
246
     * @throws Exception
247
     */
248
    protected function connectTo($connectionName)
249
    {
250
        $params = $this->getParams();
251
 
252
        $connectionParams = $this->chooseConnectionConfiguration($connectionName, $params);
253
 
254
        try {
255
            return $this->_driver->connect($connectionParams);
256
        } catch (DriverException $e) {
257
            throw $this->convertException($e);
258
        }
259
    }
260
 
261
    /**
262
     * @param string  $connectionName
263
     * @param mixed[] $params
264
     *
265
     * @return mixed
266
     */
621 lars 267
    protected function chooseConnectionConfiguration(
268
        $connectionName,
269
        #[SensitiveParameter]
270
        $params
271
    ) {
148 lars 272
        if ($connectionName === 'primary') {
273
            return $params['primary'];
274
        }
275
 
276
        $config = $params['replica'][array_rand($params['replica'])];
277
 
278
        if (! isset($config['charset']) && isset($params['primary']['charset'])) {
279
            $config['charset'] = $params['primary']['charset'];
280
        }
281
 
282
        return $config;
283
    }
284
 
285
    /**
286
     * {@inheritDoc}
287
     */
288
    public function executeStatement($sql, array $params = [], array $types = [])
289
    {
290
        $this->ensureConnectedToPrimary();
291
 
292
        return parent::executeStatement($sql, $params, $types);
293
    }
294
 
295
    /**
296
     * {@inheritDoc}
297
     */
298
    public function beginTransaction()
299
    {
300
        $this->ensureConnectedToPrimary();
301
 
302
        return parent::beginTransaction();
303
    }
304
 
305
    /**
306
     * {@inheritDoc}
307
     */
308
    public function commit()
309
    {
310
        $this->ensureConnectedToPrimary();
311
 
312
        return parent::commit();
313
    }
314
 
315
    /**
316
     * {@inheritDoc}
317
     */
318
    public function rollBack()
319
    {
320
        $this->ensureConnectedToPrimary();
321
 
322
        return parent::rollBack();
323
    }
324
 
325
    /**
326
     * {@inheritDoc}
327
     */
328
    public function close()
329
    {
330
        unset($this->connections['primary'], $this->connections['replica']);
331
 
332
        parent::close();
333
 
334
        $this->_conn       = null;
335
        $this->connections = ['primary' => null, 'replica' => null];
336
    }
337
 
338
    /**
339
     * {@inheritDoc}
340
     */
341
    public function createSavepoint($savepoint)
342
    {
343
        $this->ensureConnectedToPrimary();
344
 
345
        parent::createSavepoint($savepoint);
346
    }
347
 
348
    /**
349
     * {@inheritDoc}
350
     */
351
    public function releaseSavepoint($savepoint)
352
    {
353
        $this->ensureConnectedToPrimary();
354
 
355
        parent::releaseSavepoint($savepoint);
356
    }
357
 
358
    /**
359
     * {@inheritDoc}
360
     */
361
    public function rollbackSavepoint($savepoint)
362
    {
363
        $this->ensureConnectedToPrimary();
364
 
365
        parent::rollbackSavepoint($savepoint);
366
    }
367
 
368
    public function prepare(string $sql): Statement
369
    {
370
        $this->ensureConnectedToPrimary();
371
 
372
        return parent::prepare($sql);
373
    }
374
}