Failed Conditions
Push — master ( 7bbed5...cf98cc )
by Sergei
84:08 queued 81:00
created

MasterSlaveConnection   A

Complexity

Total Complexity 42

Size/Duplication

Total Lines 307
Duplicated Lines 0 %

Test Coverage

Coverage 60.55%

Importance

Changes 0
Metric Value
wmc 42
eloc 86
dl 0
loc 307
ccs 66
cts 109
cp 0.6055
rs 9.0399
c 0
b 0
f 0

19 Methods

Rating   Name   Duplication   Size   Complexity  
A connectTo() 0 12 1
A commit() 0 5 1
A beginTransaction() 0 5 1
A insert() 0 5 1
A close() 0 8 1
A delete() 0 5 1
A releaseSavepoint() 0 5 1
A rollbackSavepoint() 0 5 1
A chooseConnectionConfiguration() 0 13 4
A executeUpdate() 0 5 1
A exec() 0 5 1
A isConnectedToMaster() 0 3 2
C connect() 0 50 15
A createSavepoint() 0 5 1
A prepare() 0 5 1
A query() 0 18 3
A __construct() 0 17 4
A rollBack() 0 5 1
A update() 0 5 1

How to fix   Complexity   

Complex Class

Complex classes like MasterSlaveConnection often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use MasterSlaveConnection, and based on these observations, apply Extract Interface, too.

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

377
        /** @scrutinizer ignore-call */ 
378
        $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...
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

377
        /** @scrutinizer ignore-call */ 
378
        $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...
378
379
        if ($logger) {
380
            $logger->stopQuery();
381
        }
382
383
        return $statement;
384
    }
385
386
    /**
387
     * {@inheritDoc}
388
     */
389
    public function prepare($statement)
390
    {
391
        $this->connect('master');
392
393
        return parent::prepare($statement);
394
    }
395
}
396