Completed
Push — master ( 1a9812...b70610 )
by Sergei
19s queued 14s
created

MasterSlaveConnection::query()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 15
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 8
dl 0
loc 15
ccs 9
cts 9
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\DBAL\Connections;
6
7
use Doctrine\Common\EventManager;
8
use Doctrine\DBAL\Configuration;
9
use Doctrine\DBAL\Connection;
10
use Doctrine\DBAL\Driver;
11
use Doctrine\DBAL\Driver\Connection as DriverConnection;
12
use Doctrine\DBAL\Driver\ResultStatement;
13
use Doctrine\DBAL\Driver\Statement;
14
use Doctrine\DBAL\Event\ConnectionEventArgs;
15
use Doctrine\DBAL\Events;
16
use InvalidArgumentException;
17
use function array_rand;
18
use function assert;
19
use function count;
20
21
/**
22
 * Master-Slave Connection
23
 *
24
 * Connection can be used with master-slave setups.
25
 *
26
 * Important for the understanding of this connection should be how and when
27
 * it picks the slave or master.
28
 *
29
 * 1. Slave if master was never picked before and ONLY if 'getWrappedConnection'
30
 *    or 'executeQuery' is used.
31
 * 2. Master picked when 'exec', 'executeUpdate', 'insert', 'delete', 'update', 'createSavepoint',
32
 *    'releaseSavepoint', 'beginTransaction', 'rollback', 'commit', 'query' or
33
 *    'prepare' is called.
34
 * 3. If master was picked once during the lifetime of the connection it will always get picked afterwards.
35
 * 4. One slave connection is randomly picked ONCE during a request.
36
 *
37
 * ATTENTION: You can write to the slave 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
 * This connection is limited to slave operations using the
47
 * Connection#executeQuery operation only, because it wouldn't be compatible
48
 * with the ORM or SchemaManager code otherwise. Both use all the other
49
 * operations in a context where writes could happen to a slave, which makes
50
 * this restricted approach necessary.
51
 *
52
 * You can manually connect to the master at any time by calling:
53
 *
54
 *      $conn->connect('master');
55
 *
56
 * Instantiation through the DriverManager looks like:
57
 *
58
 * @example
59
 *
60
 * $conn = DriverManager::getConnection(array(
61
 *    'wrapperClass' => 'Doctrine\DBAL\Connections\MasterSlaveConnection',
62
 *    'driver' => 'pdo_mysql',
63
 *    'master' => array('user' => '', 'password' => '', 'host' => '', 'dbname' => ''),
64
 *    'slaves' => array(
65
 *        array('user' => 'slave1', 'password', 'host' => '', 'dbname' => ''),
66
 *        array('user' => 'slave2', 'password', 'host' => '', 'dbname' => ''),
67
 *    )
68
 * ));
69
 *
70
 * You can also pass 'driverOptions' and any other documented option to each of this drivers
71
 * to pass additional information.
72
 */
73
class MasterSlaveConnection extends Connection
74
{
75
    /**
76
     * Master and slave connection (one of the randomly picked slaves).
77
     *
78
     * @var array<string, DriverConnection|null>
79
     */
80
    protected $connections = ['master' => null, 'slave' => null];
81
82
    /**
83
     * You can keep the slave connection and then switch back to it
84
     * during the request if you know what you are doing.
85
     *
86
     * @var bool
87
     */
88
    protected $keepSlave = false;
89
90
    /**
91
     * Creates Master Slave Connection.
92
     *
93
     * @param array<string, mixed> $params
94
     *
95
     * @throws InvalidArgumentException
96
     */
97 180
    public function __construct(
98
        array $params,
99
        Driver $driver,
100
        ?Configuration $config = null,
101
        ?EventManager $eventManager = null
102
    ) {
103 180
        if (! isset($params['slaves'], $params['master'])) {
104
            throw new InvalidArgumentException('master or slaves configuration missing');
105
        }
106 180
        if (count($params['slaves']) === 0) {
107
            throw new InvalidArgumentException('You have to configure at least one slaves.');
108
        }
109
110 180
        $params['master']['driver'] = $params['driver'];
111 180
        foreach ($params['slaves'] as $slaveKey => $slave) {
112 180
            $params['slaves'][$slaveKey]['driver'] = $params['driver'];
113
        }
114
115 180
        $this->keepSlave = (bool) ($params['keepSlave'] ?? false);
116
117 180
        parent::__construct($params, $driver, $config, $eventManager);
118 180
    }
119
120
    /**
121
     * Checks if the connection is currently towards the master or not.
122
     */
123 153
    public function isConnectedToMaster() : bool
124
    {
125 153
        return $this->_conn !== null && $this->_conn === $this->connections['master'];
126
    }
127
128
    /**
129
     * {@inheritDoc}
130
     */
131 153
    public function connect($connectionName = null) : void
132
    {
133 153
        $requestedConnectionChange = ($connectionName !== null);
134 153
        $connectionName            = $connectionName ?: 'slave';
135
136 153
        if ($connectionName !== 'slave' && $connectionName !== 'master') {
137
            throw new InvalidArgumentException('Invalid option to connect(), only master or slave allowed.');
138
        }
139
140
        // If we have a connection open, and this is not an explicit connection
141
        // change request, then abort right here, because we are already done.
142
        // This prevents writes to the slave in case of "keepSlave" option enabled.
143 153
        if ($this->_conn !== null && ! $requestedConnectionChange) {
144 68
            return;
145
        }
146
147 153
        $forceMasterAsSlave = false;
148
149 153
        if ($this->getTransactionNestingLevel() > 0) {
150 17
            $connectionName     = 'master';
151 17
            $forceMasterAsSlave = true;
152
        }
153
154 153
        if (isset($this->connections[$connectionName])) {
155 51
            $this->_conn = $this->connections[$connectionName];
156
157 51
            if ($forceMasterAsSlave && ! $this->keepSlave) {
158
                $this->connections['slave'] = $this->_conn;
159
            }
160
161 51
            return;
162
        }
163
164 153
        if ($connectionName === 'master') {
165 119
            $this->connections['master'] = $this->_conn = $this->connectTo($connectionName);
166
167
            // Set slave connection to master to avoid invalid reads
168 119
            if (! $this->keepSlave) {
169 119
                $this->connections['slave'] = $this->connections['master'];
170
            }
171
        } else {
172 102
            $this->connections['slave'] = $this->_conn = $this->connectTo($connectionName);
173
        }
174
175 153
        if (! $this->_eventManager->hasListeners(Events::postConnect)) {
176 153
            return;
177
        }
178
179
        $eventArgs = new ConnectionEventArgs($this);
180
        $this->_eventManager->dispatchEvent(Events::postConnect, $eventArgs);
181
    }
182
183
    /**
184
     * Connects to a specific connection.
185
     */
186 153
    protected function connectTo(string $connectionName) : DriverConnection
187
    {
188 153
        $params = $this->getParams();
189
190 153
        $driverOptions = $params['driverOptions'] ?? [];
191
192 153
        $connectionParams = $this->chooseConnectionConfiguration($connectionName, $params);
193
194 153
        $user     = $connectionParams['user'] ?? '';
195 153
        $password = $connectionParams['password'] ?? '';
196
197 153
        return $this->_driver->connect($connectionParams, $user, $password, $driverOptions);
198
    }
199
200
    /**
201
     * @param array<string, mixed> $params
202
     *
203
     * @return array<string, mixed>
204
     */
205 153
    protected function chooseConnectionConfiguration(string $connectionName, array $params) : array
206
    {
207 153
        if ($connectionName === 'master') {
208 119
            return $params['master'];
209
        }
210
211 102
        $config = $params['slaves'][array_rand($params['slaves'])];
212
213 102
        if (! isset($config['charset']) && isset($params['master']['charset'])) {
214 17
            $config['charset'] = $params['master']['charset'];
215
        }
216
217 102
        return $config;
218
    }
219
220
    /**
221
     * {@inheritDoc}
222
     */
223 51
    public function executeUpdate(string $query, array $params = [], array $types = []) : int
224
    {
225 51
        $this->connect('master');
226
227 51
        return parent::executeUpdate($query, $params, $types);
228
    }
229
230
    /**
231
     * {@inheritDoc}
232
     */
233 17
    public function beginTransaction() : void
234
    {
235 17
        $this->connect('master');
236
237 17
        parent::beginTransaction();
238 17
    }
239
240
    /**
241
     * {@inheritDoc}
242
     */
243 17
    public function commit() : void
244
    {
245 17
        $this->connect('master');
246
247 17
        parent::commit();
248 17
    }
249
250
    /**
251
     * {@inheritDoc}
252
     */
253
    public function rollBack() : void
254
    {
255
        $this->connect('master');
256
257
        parent::rollBack();
258
    }
259
260
    /**
261
     * {@inheritDoc}
262
     */
263
    public function delete(string $table, array $identifier, array $types = []) : int
264
    {
265
        $this->connect('master');
266
267
        return parent::delete($table, $identifier, $types);
268
    }
269
270
    /**
271
     * {@inheritDoc}
272
     */
273 17
    public function close() : void
274
    {
275 17
        unset($this->connections['master'], $this->connections['slave']);
276
277 17
        parent::close();
278
279 17
        $this->_conn       = null;
280 17
        $this->connections = ['master' => null, 'slave' => null];
281 17
    }
282
283
    /**
284
     * {@inheritDoc}
285
     */
286
    public function update(string $table, array $data, array $identifier, array $types = []) : int
287
    {
288
        $this->connect('master');
289
290
        return parent::update($table, $data, $identifier, $types);
291
    }
292
293
    /**
294
     * {@inheritDoc}
295
     */
296 51
    public function insert(string $table, array $data, array $types = []) : int
297
    {
298 51
        $this->connect('master');
299
300 51
        return parent::insert($table, $data, $types);
301
    }
302
303
    /**
304
     * {@inheritDoc}
305
     */
306
    public function exec(string $statement) : int
307
    {
308
        $this->connect('master');
309
310
        return parent::exec($statement);
311
    }
312
313
    /**
314
     * {@inheritDoc}
315
     */
316
    public function createSavepoint(string $savepoint) : void
317
    {
318
        $this->connect('master');
319
320
        parent::createSavepoint($savepoint);
321
    }
322
323
    /**
324
     * {@inheritDoc}
325
     */
326
    public function releaseSavepoint(string $savepoint) : void
327
    {
328
        $this->connect('master');
329
330
        parent::releaseSavepoint($savepoint);
331
    }
332
333
    /**
334
     * {@inheritDoc}
335
     */
336
    public function rollbackSavepoint(string $savepoint) : void
337
    {
338
        $this->connect('master');
339
340
        parent::rollbackSavepoint($savepoint);
341
    }
342
343
    /**
344
     * {@inheritDoc}
345
     */
346 34
    public function query(string $sql) : ResultStatement
347
    {
348 34
        $this->connect('master');
349 34
        assert($this->_conn instanceof DriverConnection);
350
351 34
        $logger = $this->getConfiguration()->getSQLLogger();
352 34
        $logger->startQuery($sql);
353
354 34
        $statement = $this->_conn->query($sql);
355
356 34
        $statement->setFetchMode($this->defaultFetchMode);
357
358 34
        $logger->stopQuery();
359
360 34
        return $statement;
361
    }
362
363
    /**
364
     * {@inheritDoc}
365
     */
366
    public function prepare(string $sql) : Statement
367
    {
368
        $this->connect('master');
369
370
        return parent::prepare($sql);
371
    }
372
}
373