Completed
Push — master ( 5dd66e...5b5c2c )
by Marco
114:19 queued 111:15
created

DBAL/Connections/MasterSlaveConnection.php (1 issue)

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 assert;
15
use function count;
16
use function func_get_args;
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 153
    public function __construct(array $params, Driver $driver, ?Configuration $config = null, ?EventManager $eventManager = null)
94
    {
95 153
        if (! isset($params['slaves'], $params['master'])) {
96
            throw new InvalidArgumentException('master or slaves configuration missing');
97
        }
98 153
        if (count($params['slaves']) === 0) {
99
            throw new InvalidArgumentException('You have to configure at least one slaves.');
100
        }
101
102 153
        $params['master']['driver'] = $params['driver'];
103 153
        foreach ($params['slaves'] as $slaveKey => $slave) {
104 153
            $params['slaves'][$slaveKey]['driver'] = $params['driver'];
105
        }
106
107 153
        $this->keepSlave = (bool) ($params['keepSlave'] ?? false);
108
109 153
        parent::__construct($params, $driver, $config, $eventManager);
110 153
    }
111
112
    /**
113
     * Checks if the connection is currently towards the master or not.
114
     *
115
     * @return bool
116
     */
117 126
    public function isConnectedToMaster()
118
    {
119 126
        return $this->_conn !== null && $this->_conn === $this->connections['master'];
120
    }
121
122
    /**
123
     * {@inheritDoc}
124
     */
125 126
    public function connect($connectionName = null)
126
    {
127 126
        $requestedConnectionChange = ($connectionName !== null);
128 126
        $connectionName            = $connectionName ?: 'slave';
129
130 126
        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 126
        if ($this->_conn !== null && ! $requestedConnectionChange) {
138 126
            return false;
139
        }
140
141 126
        $forceMasterAsSlave = false;
142
143 126
        if ($this->getTransactionNestingLevel() > 0) {
144 70
            $connectionName     = 'master';
145 70
            $forceMasterAsSlave = true;
146
        }
147
148 126
        if (isset($this->connections[$connectionName])) {
149 84
            $this->_conn = $this->connections[$connectionName];
150
151 84
            if ($forceMasterAsSlave && ! $this->keepSlave) {
152
                $this->connections['slave'] = $this->_conn;
153
            }
154
155 84
            return false;
156
        }
157
158 126
        if ($connectionName === 'master') {
159 112
            $this->connections['master'] = $this->_conn = $this->connectTo($connectionName);
160
161
            // Set slave connection to master to avoid invalid reads
162 112
            if (! $this->keepSlave) {
163 112
                $this->connections['slave'] = $this->connections['master'];
164
            }
165
        } else {
166 126
            $this->connections['slave'] = $this->_conn = $this->connectTo($connectionName);
167
        }
168
169 126
        if ($this->_eventManager->hasListeners(Events::postConnect)) {
170
            $eventArgs = new ConnectionEventArgs($this);
171
            $this->_eventManager->dispatchEvent(Events::postConnect, $eventArgs);
172
        }
173
174 126
        return true;
175
    }
176
177
    /**
178
     * Connects to a specific connection.
179
     *
180
     * @param string $connectionName
181
     *
182
     * @return DriverConnection
183
     */
184 126
    protected function connectTo($connectionName)
185
    {
186 126
        $params = $this->getParams();
187
188 126
        $driverOptions = $params['driverOptions'] ?? [];
189
190 126
        $connectionParams = $this->chooseConnectionConfiguration($connectionName, $params);
191
192 126
        $user     = $connectionParams['user'] ?? null;
193 126
        $password = $connectionParams['password'] ?? null;
194
195 126
        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 126
    protected function chooseConnectionConfiguration($connectionName, $params)
205
    {
206 126
        if ($connectionName === 'master') {
207 112
            return $params['master'];
208
        }
209
210 126
        $config = $params['slaves'][array_rand($params['slaves'])];
211
212 126
        if (! isset($config['charset']) && isset($params['master']['charset'])) {
213 126
            $config['charset'] = $params['master']['charset'];
214
        }
215
216 126
        return $config;
217
    }
218
219
    /**
220
     * {@inheritDoc}
221
     */
222 84
    public function executeUpdate($query, array $params = [], array $types = [])
223
    {
224 84
        $this->connect('master');
225
226 84
        return parent::executeUpdate($query, $params, $types);
227
    }
228
229
    /**
230
     * {@inheritDoc}
231
     */
232 70
    public function beginTransaction()
233
    {
234 70
        $this->connect('master');
235
236 70
        return parent::beginTransaction();
237
    }
238
239
    /**
240
     * {@inheritDoc}
241
     */
242 70
    public function commit()
243
    {
244 70
        $this->connect('master');
245
246 70
        return parent::commit();
247
    }
248
249
    /**
250
     * {@inheritDoc}
251
     */
252
    public function rollBack()
253
    {
254
        $this->connect('master');
255
256
        return 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 42
    public function close()
273
    {
274 42
        unset($this->connections['master'], $this->connections['slave']);
275
276 42
        parent::close();
277
278 42
        $this->_conn       = null;
279 42
        $this->connections = ['master' => null, 'slave' => null];
280 42
    }
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 84
    public function insert($tableName, array $data, array $types = [])
296
    {
297 84
        $this->connect('master');
298
299 84
        return parent::insert($tableName, $data, $types);
300
    }
301
302
    /**
303
     * {@inheritDoc}
304
     */
305
    public function exec($statement)
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 28
    public function query()
346
    {
347 28
        $this->connect('master');
348 28
        assert($this->_conn instanceof DriverConnection);
349
350 28
        $args = func_get_args();
351
352 28
        $logger = $this->getConfiguration()->getSQLLogger();
353 28
        if ($logger) {
354
            $logger->startQuery($args[0]);
355
        }
356
357 28
        $statement = $this->_conn->query(...$args);
0 ignored issues
show
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

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