Passed
Pull Request — develop (#3453)
by Evgeniy
19:01
created

MasterSlaveConnection::connectTo()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 6
dl 0
loc 12
ccs 7
cts 7
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 1
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\ResultStatement;
11
use Doctrine\DBAL\Driver\Statement;
12
use Doctrine\DBAL\Event\ConnectionEventArgs;
13
use Doctrine\DBAL\Events;
14
use InvalidArgumentException;
15
use function array_rand;
16
use function assert;
17
use function count;
18
19
/**
20
 * Master-Slave Connection
21
 *
22
 * Connection can be used with master-slave setups.
23
 *
24
 * Important for the understanding of this connection should be how and when
25
 * it picks the slave or master.
26
 *
27
 * 1. Slave if master was never picked before and ONLY if 'getWrappedConnection'
28
 *    or 'executeQuery' is used.
29
 * 2. Master picked when 'exec', 'executeUpdate', 'insert', 'delete', 'update', 'createSavepoint',
30
 *    'releaseSavepoint', 'beginTransaction', 'rollback', 'commit', 'query' or
31
 *    'prepare' is called.
32
 * 3. If master was picked once during the lifetime of the connection it will always get picked afterwards.
33
 * 4. One slave connection is randomly picked ONCE during a request.
34
 *
35
 * ATTENTION: You can write to the slave with this connection if you execute a write query without
36
 * opening up a transaction. For example:
37
 *
38
 *      $conn = DriverManager::getConnection(...);
39
 *      $conn->executeQuery("DELETE FROM table");
40
 *
41
 * Be aware that Connection#executeQuery is a method specifically for READ
42
 * operations only.
43
 *
44
 * This connection is limited to slave operations using the
45
 * Connection#executeQuery operation only, because it wouldn't be compatible
46
 * with the ORM or SchemaManager code otherwise. Both use all the other
47
 * operations in a context where writes could happen to a slave, which makes
48
 * this restricted approach necessary.
49
 *
50
 * You can manually connect to the master at any time by calling:
51
 *
52
 *      $conn->connect('master');
53
 *
54
 * Instantiation through the DriverManager looks like:
55
 *
56
 * @example
57
 *
58
 * $conn = DriverManager::getConnection(array(
59
 *    'wrapperClass' => 'Doctrine\DBAL\Connections\MasterSlaveConnection',
60
 *    'driver' => 'pdo_mysql',
61
 *    'master' => array('user' => '', 'password' => '', 'host' => '', 'dbname' => ''),
62
 *    'slaves' => array(
63
 *        array('user' => 'slave1', 'password', 'host' => '', 'dbname' => ''),
64
 *        array('user' => 'slave2', 'password', 'host' => '', 'dbname' => ''),
65
 *    )
66
 * ));
67
 *
68
 * You can also pass 'driverOptions' and any other documented option to each of this drivers to pass additional information.
69
 */
70
class MasterSlaveConnection extends Connection
71
{
72
    /**
73
     * Master and slave connection (one of the randomly picked slaves).
74
     *
75
     * @var DriverConnection[]|null[]
76
     */
77
    protected $connections = ['master' => null, 'slave' => null];
78
79
    /**
80
     * You can keep the slave connection and then switch back to it
81
     * during the request if you know what you are doing.
82
     *
83
     * @var bool
84
     */
85
    protected $keepSlave = false;
86
87
    /**
88
     * Creates Master Slave Connection.
89
     *
90
     * @param mixed[] $params
91
     *
92
     * @throws InvalidArgumentException
93
     */
94 155
    public function __construct(array $params, Driver $driver, ?Configuration $config = null, ?EventManager $eventManager = null)
95
    {
96 155
        if (! isset($params['slaves'], $params['master'])) {
97
            throw new InvalidArgumentException('master or slaves configuration missing');
98
        }
99 155
        if (count($params['slaves']) === 0) {
100
            throw new InvalidArgumentException('You have to configure at least one slaves.');
101
        }
102
103 155
        $params['master']['driver'] = $params['driver'];
104 155
        foreach ($params['slaves'] as $slaveKey => $slave) {
105 155
            $params['slaves'][$slaveKey]['driver'] = $params['driver'];
106
        }
107
108 155
        $this->keepSlave = (bool) ($params['keepSlave'] ?? false);
109
110 155
        parent::__construct($params, $driver, $config, $eventManager);
111 155
    }
112
113
    /**
114
     * Checks if the connection is currently towards the master or not.
115
     *
116
     * @return bool
117
     */
118 126
    public function isConnectedToMaster()
119
    {
120 126
        return $this->_conn !== null && $this->_conn === $this->connections['master'];
121
    }
122
123
    /**
124
     * {@inheritDoc}
125
     */
126 126
    public function connect($connectionName = null)
127
    {
128 126
        $requestedConnectionChange = ($connectionName !== null);
129 126
        $connectionName            = $connectionName ?: 'slave';
130
131 126
        if ($connectionName !== 'slave' && $connectionName !== 'master') {
132
            throw new InvalidArgumentException('Invalid option to connect(), only master or slave allowed.');
133
        }
134
135
        // If we have a connection open, and this is not an explicit connection
136
        // change request, then abort right here, because we are already done.
137
        // This prevents writes to the slave in case of "keepSlave" option enabled.
138 126
        if ($this->_conn !== null && ! $requestedConnectionChange) {
139 56
            return false;
140
        }
141
142 126
        $forceMasterAsSlave = false;
143
144 126
        if ($this->getTransactionNestingLevel() > 0) {
145 14
            $connectionName     = 'master';
146 14
            $forceMasterAsSlave = true;
147
        }
148
149 126
        if (isset($this->connections[$connectionName])) {
150 42
            $this->_conn = $this->connections[$connectionName];
151
152 42
            if ($forceMasterAsSlave && ! $this->keepSlave) {
153
                $this->connections['slave'] = $this->_conn;
154
            }
155
156 42
            return false;
157
        }
158
159 126
        if ($connectionName === 'master') {
160 98
            $this->connections['master'] = $this->_conn = $this->connectTo($connectionName);
161
162
            // Set slave connection to master to avoid invalid reads
163 98
            if (! $this->keepSlave) {
164 98
                $this->connections['slave'] = $this->connections['master'];
165
            }
166
        } else {
167 84
            $this->connections['slave'] = $this->_conn = $this->connectTo($connectionName);
168
        }
169
170 126
        if ($this->_eventManager->hasListeners(Events::postConnect)) {
171
            $eventArgs = new ConnectionEventArgs($this);
172
            $this->_eventManager->dispatchEvent(Events::postConnect, $eventArgs);
173
        }
174
175 126
        return true;
176
    }
177
178
    /**
179
     * Connects to a specific connection.
180
     *
181
     * @param string $connectionName
182
     *
183
     * @return DriverConnection
184
     */
185 126
    protected function connectTo($connectionName)
186
    {
187 126
        $params = $this->getParams();
188
189 126
        $driverOptions = $params['driverOptions'] ?? [];
190
191 126
        $connectionParams = $this->chooseConnectionConfiguration($connectionName, $params);
192
193 126
        $user     = $connectionParams['user'] ?? null;
194 126
        $password = $connectionParams['password'] ?? null;
195
196 126
        return $this->_driver->connect($connectionParams, $user, $password, $driverOptions);
197
    }
198
199
    /**
200
     * @param string  $connectionName
201
     * @param mixed[] $params
202
     *
203
     * @return mixed
204
     */
205 126
    protected function chooseConnectionConfiguration($connectionName, $params)
206
    {
207 126
        if ($connectionName === 'master') {
208 98
            return $params['master'];
209
        }
210
211 84
        $config = $params['slaves'][array_rand($params['slaves'])];
212
213 84
        if (! isset($config['charset']) && isset($params['master']['charset'])) {
214 14
            $config['charset'] = $params['master']['charset'];
215
        }
216
217 84
        return $config;
218
    }
219
220
    /**
221
     * {@inheritDoc}
222
     */
223 42
    public function executeUpdate(string $query, array $params = [], array $types = []) : int
224
    {
225 42
        $this->connect('master');
226
227 42
        return parent::executeUpdate($query, $params, $types);
228
    }
229
230
    /**
231
     * {@inheritDoc}
232
     */
233 14
    public function beginTransaction()
234
    {
235 14
        $this->connect('master');
236
237 14
        return parent::beginTransaction();
238
    }
239
240
    /**
241
     * {@inheritDoc}
242
     */
243 14
    public function commit()
244
    {
245 14
        $this->connect('master');
246
247 14
        return parent::commit();
248
    }
249
250
    /**
251
     * {@inheritDoc}
252
     */
253
    public function rollBack()
254
    {
255
        $this->connect('master');
256
257
        return parent::rollBack();
0 ignored issues
show
Bug introduced by
Are you sure the usage of parent::rollBack() targeting Doctrine\DBAL\Connection::rollBack() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
258
    }
259
260
    /**
261
     * {@inheritDoc}
262
     */
263
    public function delete($tableName, array $identifier, array $types = [])
264
    {
265
        $this->connect('master');
266
267
        return parent::delete($tableName, $identifier, $types);
268
    }
269
270
    /**
271
     * {@inheritDoc}
272
     */
273 14
    public function close()
274
    {
275 14
        unset($this->connections['master'], $this->connections['slave']);
276
277 14
        parent::close();
278
279 14
        $this->_conn       = null;
280 14
        $this->connections = ['master' => null, 'slave' => null];
281 14
    }
282
283
    /**
284
     * {@inheritDoc}
285
     */
286
    public function update($tableName, array $data, array $identifier, array $types = [])
287
    {
288
        $this->connect('master');
289
290
        return parent::update($tableName, $data, $identifier, $types);
291
    }
292
293
    /**
294
     * {@inheritDoc}
295
     */
296 42
    public function insert($tableName, array $data, array $types = [])
297
    {
298 42
        $this->connect('master');
299
300 42
        return parent::insert($tableName, $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($savepoint)
317
    {
318
        $this->connect('master');
319
320
        parent::createSavepoint($savepoint);
321
    }
322
323
    /**
324
     * {@inheritDoc}
325
     */
326
    public function releaseSavepoint($savepoint)
327
    {
328
        $this->connect('master');
329
330
        parent::releaseSavepoint($savepoint);
331
    }
332
333
    /**
334
     * {@inheritDoc}
335
     */
336
    public function rollbackSavepoint($savepoint)
337
    {
338
        $this->connect('master');
339
340
        parent::rollbackSavepoint($savepoint);
341
    }
342
343
    /**
344
     * {@inheritDoc}
345
     */
346 28
    public function query(string $sql) : ResultStatement
347
    {
348 28
        $this->connect('master');
349 28
        assert($this->_conn instanceof DriverConnection);
350
351 28
        $logger = $this->getConfiguration()->getSQLLogger();
352 28
        $logger->startQuery($sql);
353
354 28
        $statement = $this->_conn->query($sql);
355
356 28
        $statement->setFetchMode($this->defaultFetchMode);
357
358 28
        $logger->stopQuery();
359
360 28
        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