Subversion-Projekte lars-tiefland.laravel_shop

Revision

Zur aktuellen Revision | Details | 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;
18
 
19
use function array_rand;
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
 * @example
63
 *
64
 * $conn = DriverManager::getConnection(array(
65
 *    'wrapperClass' => 'Doctrine\DBAL\Connections\PrimaryReadReplicaConnection',
66
 *    'driver' => 'pdo_mysql',
67
 *    'primary' => array('user' => '', 'password' => '', 'host' => '', 'dbname' => ''),
68
 *    'replica' => array(
69
 *        array('user' => 'replica1', 'password', 'host' => '', 'dbname' => ''),
70
 *        array('user' => 'replica2', 'password', 'host' => '', 'dbname' => ''),
71
 *    )
72
 * ));
73
 *
74
 * You can also pass 'driverOptions' and any other documented option to each of this drivers
75
 * to pass additional information.
76
 */
77
class PrimaryReadReplicaConnection extends Connection
78
{
79
    /**
80
     * Primary and Replica connection (one of the randomly picked replicas).
81
     *
82
     * @var DriverConnection[]|null[]
83
     */
84
    protected $connections = ['primary' => null, 'replica' => null];
85
 
86
    /**
87
     * You can keep the replica connection and then switch back to it
88
     * during the request if you know what you are doing.
89
     *
90
     * @var bool
91
     */
92
    protected $keepReplica = false;
93
 
94
    /**
95
     * Creates Primary Replica Connection.
96
     *
97
     * @internal The connection can be only instantiated by the driver manager.
98
     *
99
     * @param array<string,mixed> $params
100
     * @psalm-param Params $params
101
     *
102
     * @throws Exception
103
     * @throws InvalidArgumentException
104
     */
105
    public function __construct(
106
        array $params,
107
        Driver $driver,
108
        ?Configuration $config = null,
109
        ?EventManager $eventManager = null
110
    ) {
111
        if (! isset($params['replica'], $params['primary'])) {
112
            throw new InvalidArgumentException('primary or replica configuration missing');
113
        }
114
 
115
        if (count($params['replica']) === 0) {
116
            throw new InvalidArgumentException('You have to configure at least one replica.');
117
        }
118
 
119
        if (isset($params['driver'])) {
120
            $params['primary']['driver'] = $params['driver'];
121
 
122
            foreach ($params['replica'] as $replicaKey => $replica) {
123
                $params['replica'][$replicaKey]['driver'] = $params['driver'];
124
            }
125
        }
126
 
127
        $this->keepReplica = (bool) ($params['keepReplica'] ?? false);
128
 
129
        parent::__construct($params, $driver, $config, $eventManager);
130
    }
131
 
132
    /**
133
     * Checks if the connection is currently towards the primary or not.
134
     */
135
    public function isConnectedToPrimary(): bool
136
    {
137
        return $this->_conn !== null && $this->_conn === $this->connections['primary'];
138
    }
139
 
140
    /**
141
     * @param string|null $connectionName
142
     *
143
     * @return bool
144
     */
145
    public function connect($connectionName = null)
146
    {
147
        if ($connectionName !== null) {
148
            throw new InvalidArgumentException(
149
                'Passing a connection name as first argument is not supported anymore.'
150
                    . ' Use ensureConnectedToPrimary()/ensureConnectedToReplica() instead.',
151
            );
152
        }
153
 
154
        return $this->performConnect();
155
    }
156
 
157
    protected function performConnect(?string $connectionName = null): bool
158
    {
159
        $requestedConnectionChange = ($connectionName !== null);
160
        $connectionName            = $connectionName ?? 'replica';
161
 
162
        if ($connectionName !== 'replica' && $connectionName !== 'primary') {
163
            throw new InvalidArgumentException('Invalid option to connect(), only primary or replica allowed.');
164
        }
165
 
166
        // If we have a connection open, and this is not an explicit connection
167
        // change request, then abort right here, because we are already done.
168
        // This prevents writes to the replica in case of "keepReplica" option enabled.
169
        if ($this->_conn !== null && ! $requestedConnectionChange) {
170
            return false;
171
        }
172
 
173
        $forcePrimaryAsReplica = false;
174
 
175
        if ($this->getTransactionNestingLevel() > 0) {
176
            $connectionName        = 'primary';
177
            $forcePrimaryAsReplica = true;
178
        }
179
 
180
        if (isset($this->connections[$connectionName])) {
181
            $this->_conn = $this->connections[$connectionName];
182
 
183
            if ($forcePrimaryAsReplica && ! $this->keepReplica) {
184
                $this->connections['replica'] = $this->_conn;
185
            }
186
 
187
            return false;
188
        }
189
 
190
        if ($connectionName === 'primary') {
191
            $this->connections['primary'] = $this->_conn = $this->connectTo($connectionName);
192
 
193
            // Set replica connection to primary to avoid invalid reads
194
            if (! $this->keepReplica) {
195
                $this->connections['replica'] = $this->connections['primary'];
196
            }
197
        } else {
198
            $this->connections['replica'] = $this->_conn = $this->connectTo($connectionName);
199
        }
200
 
201
        if ($this->_eventManager->hasListeners(Events::postConnect)) {
202
            Deprecation::trigger(
203
                'doctrine/dbal',
204
                'https://github.com/doctrine/dbal/issues/5784',
205
                'Subscribing to %s events is deprecated. Implement a middleware instead.',
206
                Events::postConnect,
207
            );
208
 
209
            $eventArgs = new ConnectionEventArgs($this);
210
            $this->_eventManager->dispatchEvent(Events::postConnect, $eventArgs);
211
        }
212
 
213
        return true;
214
    }
215
 
216
    /**
217
     * Connects to the primary node of the database cluster.
218
     *
219
     * All following statements after this will be executed against the primary node.
220
     */
221
    public function ensureConnectedToPrimary(): bool
222
    {
223
        return $this->performConnect('primary');
224
    }
225
 
226
    /**
227
     * Connects to a replica node of the database cluster.
228
     *
229
     * All following statements after this will be executed against the replica node,
230
     * unless the keepReplica option is set to false and a primary connection
231
     * was already opened.
232
     */
233
    public function ensureConnectedToReplica(): bool
234
    {
235
        return $this->performConnect('replica');
236
    }
237
 
238
    /**
239
     * Connects to a specific connection.
240
     *
241
     * @param string $connectionName
242
     *
243
     * @return DriverConnection
244
     *
245
     * @throws Exception
246
     */
247
    protected function connectTo($connectionName)
248
    {
249
        $params = $this->getParams();
250
 
251
        $connectionParams = $this->chooseConnectionConfiguration($connectionName, $params);
252
 
253
        try {
254
            return $this->_driver->connect($connectionParams);
255
        } catch (DriverException $e) {
256
            throw $this->convertException($e);
257
        }
258
    }
259
 
260
    /**
261
     * @param string  $connectionName
262
     * @param mixed[] $params
263
     *
264
     * @return mixed
265
     */
266
    protected function chooseConnectionConfiguration($connectionName, $params)
267
    {
268
        if ($connectionName === 'primary') {
269
            return $params['primary'];
270
        }
271
 
272
        $config = $params['replica'][array_rand($params['replica'])];
273
 
274
        if (! isset($config['charset']) && isset($params['primary']['charset'])) {
275
            $config['charset'] = $params['primary']['charset'];
276
        }
277
 
278
        return $config;
279
    }
280
 
281
    /**
282
     * {@inheritDoc}
283
     */
284
    public function executeStatement($sql, array $params = [], array $types = [])
285
    {
286
        $this->ensureConnectedToPrimary();
287
 
288
        return parent::executeStatement($sql, $params, $types);
289
    }
290
 
291
    /**
292
     * {@inheritDoc}
293
     */
294
    public function beginTransaction()
295
    {
296
        $this->ensureConnectedToPrimary();
297
 
298
        return parent::beginTransaction();
299
    }
300
 
301
    /**
302
     * {@inheritDoc}
303
     */
304
    public function commit()
305
    {
306
        $this->ensureConnectedToPrimary();
307
 
308
        return parent::commit();
309
    }
310
 
311
    /**
312
     * {@inheritDoc}
313
     */
314
    public function rollBack()
315
    {
316
        $this->ensureConnectedToPrimary();
317
 
318
        return parent::rollBack();
319
    }
320
 
321
    /**
322
     * {@inheritDoc}
323
     */
324
    public function close()
325
    {
326
        unset($this->connections['primary'], $this->connections['replica']);
327
 
328
        parent::close();
329
 
330
        $this->_conn       = null;
331
        $this->connections = ['primary' => null, 'replica' => null];
332
    }
333
 
334
    /**
335
     * {@inheritDoc}
336
     */
337
    public function createSavepoint($savepoint)
338
    {
339
        $this->ensureConnectedToPrimary();
340
 
341
        parent::createSavepoint($savepoint);
342
    }
343
 
344
    /**
345
     * {@inheritDoc}
346
     */
347
    public function releaseSavepoint($savepoint)
348
    {
349
        $this->ensureConnectedToPrimary();
350
 
351
        parent::releaseSavepoint($savepoint);
352
    }
353
 
354
    /**
355
     * {@inheritDoc}
356
     */
357
    public function rollbackSavepoint($savepoint)
358
    {
359
        $this->ensureConnectedToPrimary();
360
 
361
        parent::rollbackSavepoint($savepoint);
362
    }
363
 
364
    public function prepare(string $sql): Statement
365
    {
366
        $this->ensureConnectedToPrimary();
367
 
368
        return parent::prepare($sql);
369
    }
370
}