Completed
Push — master ( 1b1935...af38e2 )
by Sergei
17:55 queued 17:50
created

chooseConnectionConfiguration()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 13
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 4

Importance

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

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

355
        /** @scrutinizer ignore-call */ 
356
        $statement = $this->_conn->query(...$args);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
Unused Code introduced by
The call to Doctrine\DBAL\Driver\Connection::query() has too many arguments starting with $args. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

355
        /** @scrutinizer ignore-call */ 
356
        $statement = $this->_conn->query(...$args);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
356
357 28
        $statement->setFetchMode($this->defaultFetchMode);
358
359 28
        if ($logger) {
360
            $logger->stopQuery();
361
        }
362
363 28
        return $statement;
364
    }
365
366
    /**
367
     * {@inheritDoc}
368
     */
369
    public function prepare($statement)
370
    {
371
        $this->connect('master');
372
373
        return parent::prepare($statement);
374
    }
375
}
376