Completed
Push — master ( c7757e...39cb21 )
by Luís
16s
created

DBAL/Connections/MasterSlaveConnection.php (6 issues)

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 = isset($params['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
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
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 = isset($params['driverOptions']) ? $params['driverOptions'] : [];
206
207
        $connectionParams = $this->chooseConnectionConfiguration($connectionName, $params);
208
209
        $user = isset($connectionParams['user']) ? $connectionParams['user'] : null;
210
        $password = isset($connectionParams['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
        return $params['slaves'][array_rand($params['slaves'])];
228
    }
229
230
    /**
231
     * {@inheritDoc}
232
     */
233
    public function executeUpdate($query, array $params = [], array $types = [])
234
    {
235
        $this->connect('master');
236
237
        return parent::executeUpdate($query, $params, $types);
238
    }
239
240
    /**
241
     * {@inheritDoc}
242
     */
243
    public function beginTransaction()
244
    {
245
        $this->connect('master');
246
247
        parent::beginTransaction();
248
    }
249
250
    /**
251
     * {@inheritDoc}
252
     */
253
    public function commit()
254
    {
255
        $this->connect('master');
256
257
        parent::commit();
258
    }
259
260
    /**
261
     * {@inheritDoc}
262
     */
263
    public function rollBack()
264
    {
265
        $this->connect('master');
266
267
        return parent::rollBack();
0 ignored issues
show
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...
268
    }
269
270
    /**
271
     * {@inheritDoc}
272
     */
273
    public function delete($tableName, array $identifier, array $types = [])
274
    {
275
        $this->connect('master');
276
277
        return parent::delete($tableName, $identifier, $types);
278
    }
279
280
    /**
281
     * {@inheritDoc}
282
     */
283
    public function close()
284
    {
285
        unset($this->connections['master']);
286
        unset($this->connections['slave']);
287
288
        parent::close();
289
290
        $this->_conn = null;
291
        $this->connections = ['master' => null, 'slave' => null];
292
    }
293
294
    /**
295
     * {@inheritDoc}
296
     */
297
    public function update($tableName, array $data, array $identifier, array $types = [])
298
    {
299
        $this->connect('master');
300
301
        return parent::update($tableName, $data, $identifier, $types);
302
    }
303
304
    /**
305
     * {@inheritDoc}
306
     */
307
    public function insert($tableName, array $data, array $types = [])
308
    {
309
        $this->connect('master');
310
311
        return parent::insert($tableName, $data, $types);
312
    }
313
314
    /**
315
     * {@inheritDoc}
316
     */
317
    public function exec($statement)
318
    {
319
        $this->connect('master');
320
321
        return parent::exec($statement);
322
    }
323
324
    /**
325
     * {@inheritDoc}
326
     */
327
    public function createSavepoint($savepoint)
328
    {
329
        $this->connect('master');
330
331
        parent::createSavepoint($savepoint);
332
    }
333
334
    /**
335
     * {@inheritDoc}
336
     */
337
    public function releaseSavepoint($savepoint)
338
    {
339
        $this->connect('master');
340
341
        parent::releaseSavepoint($savepoint);
342
    }
343
344
    /**
345
     * {@inheritDoc}
346
     */
347
    public function rollbackSavepoint($savepoint)
348
    {
349
        $this->connect('master');
350
351
        parent::rollbackSavepoint($savepoint);
352
    }
353
354
    /**
355
     * {@inheritDoc}
356
     */
357
    public function query()
358
    {
359
        $this->connect('master');
360
361
        $args = func_get_args();
362
363
        $logger = $this->getConfiguration()->getSQLLogger();
364
        if ($logger) {
365
            $logger->startQuery($args[0]);
366
        }
367
        
368
        $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

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