Failed Conditions
Push — master ( 656579...2742cd )
by Marco
11:55
created

MasterSlaveConnection::connect()   C

Complexity

Conditions 15
Paths 18

Size

Total Lines 50
Code Lines 25

Duplication

Lines 4
Ratio 8 %

Code Coverage

Tests 0
CRAP Score 240

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 4
loc 50
rs 5.3624
ccs 0
cts 35
cp 0
cc 15
eloc 25
nc 18
nop 1
crap 240

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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\Configuration;
25
use Doctrine\Common\EventManager;
26
use Doctrine\DBAL\Event\ConnectionEventArgs;
27
use Doctrine\DBAL\Events;
28
29
/**
30
 * Master-Slave Connection
31
 *
32
 * Connection can be used with master-slave setups.
33
 *
34
 * Important for the understanding of this connection should be how and when
35
 * it picks the slave or master.
36
 *
37
 * 1. Slave if master was never picked before and ONLY if 'getWrappedConnection'
38
 *    or 'executeQuery' is used.
39
 * 2. Master picked when 'exec', 'executeUpdate', 'insert', 'delete', 'update', 'createSavepoint',
40
 *    'releaseSavepoint', 'beginTransaction', 'rollback', 'commit', 'query' or
41
 *    'prepare' is called.
42
 * 3. If master was picked once during the lifetime of the connection it will always get picked afterwards.
43
 * 4. One slave connection is randomly picked ONCE during a request.
44
 *
45
 * ATTENTION: You can write to the slave with this connection if you execute a write query without
46
 * opening up a transaction. For example:
47
 *
48
 *      $conn = DriverManager::getConnection(...);
49
 *      $conn->executeQuery("DELETE FROM table");
50
 *
51
 * Be aware that Connection#executeQuery is a method specifically for READ
52
 * operations only.
53
 *
54
 * This connection is limited to slave operations using the
55
 * Connection#executeQuery operation only, because it wouldn't be compatible
56
 * with the ORM or SchemaManager code otherwise. Both use all the other
57
 * operations in a context where writes could happen to a slave, which makes
58
 * this restricted approach necessary.
59
 *
60
 * You can manually connect to the master at any time by calling:
61
 *
62
 *      $conn->connect('master');
63
 *
64
 * Instantiation through the DriverManager looks like:
65
 *
66
 * @example
67
 *
68
 * $conn = DriverManager::getConnection(array(
69
 *    'wrapperClass' => 'Doctrine\DBAL\Connections\MasterSlaveConnection',
70
 *    'driver' => 'pdo_mysql',
71
 *    'master' => array('user' => '', 'password' => '', 'host' => '', 'dbname' => ''),
72
 *    'slaves' => array(
73
 *        array('user' => 'slave1', 'password', 'host' => '', 'dbname' => ''),
74
 *        array('user' => 'slave2', 'password', 'host' => '', 'dbname' => ''),
75
 *    )
76
 * ));
77
 *
78
 * You can also pass 'driverOptions' and any other documented option to each of this drivers to pass additional information.
79
 *
80
 * @author Lars Strojny <[email protected]>
81
 * @author Benjamin Eberlei <[email protected]>
82
 */
83
class MasterSlaveConnection extends Connection
84
{
85
    /**
86
     * Master and slave connection (one of the randomly picked slaves).
87
     *
88
     * @var \Doctrine\DBAL\Driver\Connection[]
89
     */
90
    protected $connections = ['master' => null, 'slave' => null];
91
92
    /**
93
     * You can keep the slave connection and then switch back to it
94
     * during the request if you know what you are doing.
95
     *
96
     * @var boolean
97
     */
98
    protected $keepSlave = false;
99
100
    /**
101
     * Creates Master Slave Connection.
102
     *
103
     * @param array                              $params
104
     * @param \Doctrine\DBAL\Driver              $driver
105
     * @param \Doctrine\DBAL\Configuration|null  $config
106
     * @param \Doctrine\Common\EventManager|null $eventManager
107
     *
108
     * @throws \InvalidArgumentException
109
     */
110
    public function __construct(array $params, Driver $driver, Configuration $config = null, EventManager $eventManager = null)
111
    {
112
        if ( !isset($params['slaves']) || !isset($params['master'])) {
113
            throw new \InvalidArgumentException('master or slaves configuration missing');
114
        }
115
        if (count($params['slaves']) == 0) {
116
            throw new \InvalidArgumentException('You have to configure at least one slaves.');
117
        }
118
119
        $params['master']['driver'] = $params['driver'];
120
        foreach ($params['slaves'] as $slaveKey => $slave) {
121
            $params['slaves'][$slaveKey]['driver'] = $params['driver'];
122
        }
123
124
        $this->keepSlave = (bool) ($params['keepSlave'] ?? false);
125
126
        parent::__construct($params, $driver, $config, $eventManager);
127
    }
128
129
    /**
130
     * Checks if the connection is currently towards the master or not.
131
     *
132
     * @return boolean
133
     */
134
    public function isConnectedToMaster()
135
    {
136
        return $this->_conn !== null && $this->_conn === $this->connections['master'];
137
    }
138
139
    /**
140
     * {@inheritDoc}
141
     */
142
    public function connect($connectionName = null)
143
    {
144
        $requestedConnectionChange = ($connectionName !== null);
145
        $connectionName            = $connectionName ?: 'slave';
146
147
        if ($connectionName !== 'slave' && $connectionName !== 'master') {
148
            throw new \InvalidArgumentException("Invalid option to connect(), only master or slave allowed.");
149
        }
150
151
        // If we have a connection open, and this is not an explicit connection
152
        // change request, then abort right here, because we are already done.
153
        // This prevents writes to the slave in case of "keepSlave" option enabled.
154
        if (isset($this->_conn) && $this->_conn && !$requestedConnectionChange) {
155
            return false;
156
        }
157
158
        $forceMasterAsSlave = false;
159
160
        if ($this->getTransactionNestingLevel() > 0) {
161
            $connectionName     = 'master';
162
            $forceMasterAsSlave = true;
163
        }
164
165
        if (isset($this->connections[$connectionName]) && $this->connections[$connectionName]) {
166
            $this->_conn = $this->connections[$connectionName];
167
168
            if ($forceMasterAsSlave && ! $this->keepSlave) {
169
                $this->connections['slave'] = $this->_conn;
170
            }
171
172
            return false;
173
        }
174
175
        if ($connectionName === 'master') {
176
            $this->connections['master'] = $this->_conn = $this->connectTo($connectionName);
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->connectTo($connectionName) of type Doctrine\DBAL\Driver is incompatible with the declared type Doctrine\DBAL\Driver\Connection of property $_conn.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
177
178
            // Set slave connection to master to avoid invalid reads
179
            if ( ! $this->keepSlave) {
180
                $this->connections['slave'] = $this->connections['master'];
181
            }
182
        } else {
183
            $this->connections['slave'] = $this->_conn = $this->connectTo($connectionName);
184
        }
185
186 View Code Duplication
        if ($this->_eventManager->hasListeners(Events::postConnect)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
187
            $eventArgs = new ConnectionEventArgs($this);
188
            $this->_eventManager->dispatchEvent(Events::postConnect, $eventArgs);
189
        }
190
191
        return true;
192
    }
193
194
    /**
195
     * Connects to a specific connection.
196
     *
197
     * @param string $connectionName
198
     *
199
     * @return \Doctrine\DBAL\Driver
200
     */
201 View Code Duplication
    protected function connectTo($connectionName)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
202
    {
203
        $params = $this->getParams();
204
205
        $driverOptions = $params['driverOptions'] ?? [];
206
207
        $connectionParams = $this->chooseConnectionConfiguration($connectionName, $params);
208
209
        $user = $connectionParams['user'] ?? null;
210
        $password = $connectionParams['password'] ?? null;
211
212
        return $this->_driver->connect($connectionParams, $user, $password, $driverOptions);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->_driver->c...ssword, $driverOptions) returns the type Doctrine\DBAL\Driver\Connection which is incompatible with the documented return type Doctrine\DBAL\Driver.
Loading history...
213
    }
214
215
    /**
216
     * @param string $connectionName
217
     * @param array  $params
218
     *
219
     * @return mixed
220
     */
221
    protected function chooseConnectionConfiguration($connectionName, $params)
222
    {
223
        if ($connectionName === 'master') {
224
            return $params['master'];
225
        }
226
227
        $config = $params['slaves'][array_rand($params['slaves'])];
228
229
        if ( ! isset($config['charset']) && isset($params['master']['charset'])) {
230
            $config['charset'] = $params['master']['charset'];
231
        }
232
233
        return $config;
234
    }
235
236
    /**
237
     * {@inheritDoc}
238
     */
239
    public function executeUpdate($query, array $params = [], array $types = [])
240
    {
241
        $this->connect('master');
242
243
        return parent::executeUpdate($query, $params, $types);
244
    }
245
246
    /**
247
     * {@inheritDoc}
248
     */
249
    public function beginTransaction()
250
    {
251
        $this->connect('master');
252
253
        parent::beginTransaction();
254
    }
255
256
    /**
257
     * {@inheritDoc}
258
     */
259
    public function commit()
260
    {
261
        $this->connect('master');
262
263
        parent::commit();
264
    }
265
266
    /**
267
     * {@inheritDoc}
268
     */
269
    public function rollBack()
270
    {
271
        $this->connect('master');
272
273
        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...
274
    }
275
276
    /**
277
     * {@inheritDoc}
278
     */
279
    public function delete($tableName, array $identifier, array $types = [])
280
    {
281
        $this->connect('master');
282
283
        return parent::delete($tableName, $identifier, $types);
284
    }
285
286
    /**
287
     * {@inheritDoc}
288
     */
289
    public function close()
290
    {
291
        unset($this->connections['master']);
292
        unset($this->connections['slave']);
293
294
        parent::close();
295
296
        $this->_conn = null;
297
        $this->connections = ['master' => null, 'slave' => null];
298
    }
299
300
    /**
301
     * {@inheritDoc}
302
     */
303
    public function update($tableName, array $data, array $identifier, array $types = [])
304
    {
305
        $this->connect('master');
306
307
        return parent::update($tableName, $data, $identifier, $types);
308
    }
309
310
    /**
311
     * {@inheritDoc}
312
     */
313
    public function insert($tableName, array $data, array $types = [])
314
    {
315
        $this->connect('master');
316
317
        return parent::insert($tableName, $data, $types);
318
    }
319
320
    /**
321
     * {@inheritDoc}
322
     */
323
    public function exec($statement)
324
    {
325
        $this->connect('master');
326
327
        return parent::exec($statement);
328
    }
329
330
    /**
331
     * {@inheritDoc}
332
     */
333
    public function createSavepoint($savepoint)
334
    {
335
        $this->connect('master');
336
337
        parent::createSavepoint($savepoint);
338
    }
339
340
    /**
341
     * {@inheritDoc}
342
     */
343
    public function releaseSavepoint($savepoint)
344
    {
345
        $this->connect('master');
346
347
        parent::releaseSavepoint($savepoint);
348
    }
349
350
    /**
351
     * {@inheritDoc}
352
     */
353
    public function rollbackSavepoint($savepoint)
354
    {
355
        $this->connect('master');
356
357
        parent::rollbackSavepoint($savepoint);
358
    }
359
360
    /**
361
     * {@inheritDoc}
362
     */
363
    public function query()
364
    {
365
        $this->connect('master');
366
367
        $args = func_get_args();
368
369
        $logger = $this->getConfiguration()->getSQLLogger();
370
        if ($logger) {
371
            $logger->startQuery($args[0]);
372
        }
373
374
        $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

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