Failed Conditions
Pull Request — develop (#3367)
by Benjamin
10:59
created

MasterSlaveConnection::commit()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

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

352
        /** @scrutinizer ignore-call */ 
353
        $statement = $this->_conn->query($sql);

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...
353
354
        $logger->stopQuery();
355
356
        return $statement;
357
    }
358
359
    /**
360
     * {@inheritDoc}
361
     */
362
    public function prepare(string $sql) : Statement
363
    {
364
        $this->connect('master');
365
366
        return parent::prepare($sql);
367
    }
368
}
369