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

DBAL/Connections/MasterSlaveConnection.php (3 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)) {
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)
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);
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