Completed
Pull Request — 2.11.x (#2448)
by
unknown
12:14
created

MasterSlaveConnection::setConnector()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

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

362
        /** @scrutinizer ignore-call */ 
363
        $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...
363
364 24
        $statement->setFetchMode($this->defaultFetchMode);
365
366 24
        if ($logger) {
367
            $logger->stopQuery();
368
        }
369
370 24
        return $statement;
371
    }
372
373
    /**
374
     * {@inheritDoc}
375
     */
376
    public function prepare($statement)
377
    {
378
        $this->connect('master');
379
380
        return parent::prepare($statement);
381
    }
382
}
383