MasterSlavesConnection::exec()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
1
<?php
2
3
namespace Ez\DbLinker\Driver\Connection;
4
5
use Doctrine\DBAL\Driver\Connection;
6
use Ez\DbLinker\ExtendedServer;
7
use Ez\DbLinker\Master;
8
use Ez\DbLinker\Slave;
9
10
class MasterSlavesConnection implements Connection
11
{
12
    /**
13
     * configuration
14
     *
15
     * @var Master
16
     */
17
    private $master;
18
19
    /**
20
     * configuration
21
     *
22
     * @var Slave[]
23
     */
24
    private $slaves;
25
26
    /**
27
     * @var \Ez\DbLinker\Cache
28
     */
29
    private $cache;
30
31
    /**
32
     * @var bool
33
     */
34
    private $forceMaster = false;
35
36
    /**
37
     * @var int
38
     */
39
    private $maxSlaveDelay = 30;
40
41
    /**
42
     * @var int
43
     */
44
    private $slaveStatusCacheTtl = 10;
45
46
    /**
47
     * @var ExtendedServer
48
     */
49
    private $lastConnection;
50
51
    public function __construct(array $master, array $slaves, $cache = null)
52
    {
53
        $this->master = new Master($master);
54
        $this->slaves = $this->filteredSlaves($slaves);
55
        $this->cache = $cache;
56
    }
57
58
    public function disableCache(): void
59
    {
60
        $this->cache->disableCache();
61
    }
62
63
    /**
64
     * @param array $slaves
65
     * @return Slave[]
66
     */
67
    private function filteredSlaves(array $slaves): array
68
    {
69
        // keep slave with weight > 0
70
        return array_filter(array_map(function ($slave) {
71
            if ((int) $slave['weight'] > 0) {
72
                return new Slave($slave);
73
            }
74
            return null;
75
        }, $slaves));
76
    }
77
78
    /**
79
     * @param bool|null $forced
80
     * @return Connection
81
     * @throws \Doctrine\DBAL\DBALException
82
     */
83
    public function masterConnection(bool $forced = null): Connection
84
    {
85
        if ($forced !== null) {
86
            $this->forceMaster = $forced;
87
        }
88
        $this->lastConnection = $this->master;
89
        return $this->master->connection();
90
    }
91
92
    /**
93
     * @return Connection
94
     * @throws \Doctrine\DBAL\DBALException
95
     */
96
    public function slaveConnection(): ?Connection
97
    {
98
        if (empty($this->slaves)) {
99
            return $this->masterConnection();
100
        }
101
102
        $this->forceMaster = false;
103
        $x = null;
104
105
        // if we have a connected slave
106
        foreach ($this->slaves as $slave) {
107
            if ($slave->isConnected()) {
108
                $x = $slave;
109
            }
110
        }
111
        if (!$x) {
112
            $x = $this->randomSlave($this->slaves);
113
        }
114
115
        $this->lastConnection = $x;
116
        return $x->connection();
117
    }
118
119
    /**
120
     * @param Slave[] $slaves
121
     * @return Slave|null
122
     */
123
    private function randomSlave(array $slaves): ?Slave
124
    {
125
        $weights = [];
126
        foreach ($slaves as $slaveIndex => $slave) {
127
            if (!$this->isSlaveOK($slave)) {
128
                continue;
129
            }
130
            $weights = array_merge($weights, array_fill(0, $slave->getWeight(), $slaveIndex));
131
        }
132
        if (!\count($weights)) {
133
            return null;
134
        }
135
        return $slaves[$weights[array_rand($weights)]];
136
    }
137
138
    /**
139
     * @return ExtendedServer|null
140
     */
141
    public function getLastConnection(): ?ExtendedServer
142
    {
143
        return $this->lastConnection;
144
    }
145
146
    /**
147
     * @return Slave[]|null
148
     */
149
    public function slaves(): ?array
150
    {
151
        return $this->slaves;
152
    }
153
154
    /**
155
     * Prepares a statement for execution and returns a Statement object.
156
     *
157
     * @param string $prepareString
158
     *
159
     * @return \Doctrine\DBAL\Driver\Statement
160
     * @throws \Doctrine\DBAL\DBALException
161
     */
162
    public function prepare($prepareString)
163
    {
164
        $cnx = null;
0 ignored issues
show
Unused Code introduced by
$cnx is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
165
        $caller = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]['function'];
166
167
        if ($this->forceMaster || $caller === 'executeUpdate') {
168
            $cnx = $this->masterConnection();
169
        } else if ($caller === 'executeQuery') {
170
            if (preg_match('/\b(DELETE|UPDATE|INSERT|REPLACE)\b/i', $prepareString)) {
171
                $cnx = $this->masterConnection();
172
                error_log('should call executeUpdate()');
173
            } else {
174
                $cnx = $this->slaveConnection();
175
            }
176
        } else if (preg_match('/\b(DELETE|UPDATE|INSERT|REPLACE)\b/i', $prepareString)) {
177
            $cnx = $this->masterConnection();
178
        } else {
179
            $cnx = $this->slaveConnection();
180
        }
181
182
        return $cnx->prepare($prepareString);
183
    }
184
185
    public function forceMaster(bool $force): void
186
    {
187
        $this->forceMaster = $force;
188
    }
189
190
    /**
191
     * Executes an SQL statement, returning a result set as a Statement object.
192
     *
193
     * @return \Doctrine\DBAL\Driver\Statement
194
     * @throws \Doctrine\DBAL\DBALException
195
     */
196
    public function query()
197
    {
198
        $cnx = $this->forceMaster ? $this->masterConnection() : $this->slaveConnection();
199
        return call_user_func_array([$cnx, __FUNCTION__], func_get_args());
200
    }
201
202
    /**
203
     * Quotes a string for use in a query.
204
     *
205
     * @param string $input
206
     * @param integer $type
207
     *
208
     * @return string
209
     * @throws \Doctrine\DBAL\DBALException
210
     */
211
    public function quote($input, $type = \PDO::PARAM_STR)
212
    {
213
        return $this->slaveConnection()->quote($input, $type);
214
    }
215
216
    /**
217
     * Executes an SQL statement and return the number of affected rows.
218
     *
219
     * @param string $statement
220
     *
221
     * @return integer
222
     * @throws \Doctrine\DBAL\DBALException
223
     */
224
    public function exec($statement)
225
    {
226
        return $this->masterConnection()->exec($statement);
227
    }
228
229
    /**
230
     * Returns the ID of the last inserted row or sequence value.
231
     *
232
     * @param string|null $name
233
     *
234
     * @return string
235
     * @throws \Doctrine\DBAL\DBALException
236
     */
237
    public function lastInsertId($name = null)
238
    {
239
        return $this->masterConnection()->lastInsertId($name);
240
    }
241
242
    /**
243
     * Initiates a transaction.
244
     *
245
     * @return boolean TRUE on success or FALSE on failure.
246
     * @throws \Doctrine\DBAL\DBALException
247
     */
248
    public function beginTransaction()
249
    {
250
        return $this->masterConnection(true)->beginTransaction();
251
    }
252
253
    /**
254
     * Commits a transaction.
255
     *
256
     * @return boolean TRUE on success or FALSE on failure.
257
     * @throws \Doctrine\DBAL\DBALException
258
     */
259
    public function commit()
260
    {
261
        return $this->masterConnection(false)->commit();
262
    }
263
264
    /**
265
     * Rolls back the current transaction, as initiated by beginTransaction().
266
     *
267
     * @return boolean TRUE on success or FALSE on failure.
268
     * @throws \Doctrine\DBAL\DBALException
269
     */
270
    public function rollBack()
271
    {
272
        return $this->masterConnection(false)->rollBack();
273
    }
274
275
    /**
276
     * Returns the error code associated with the last operation on the database handle.
277
     *
278
     * @return string|null The error code, or null if no operation has been run on the database handle.
279
     * @throws \Doctrine\DBAL\DBALException
280
     */
281
    public function errorCode()
282
    {
283
        return $this->lastConnection->connection()->errorCode();
284
    }
285
286
    /**
287
     * Returns extended error information associated with the last operation on the database handle.
288
     *
289
     * @return array
290
     * @throws \Doctrine\DBAL\DBALException
291
     */
292
    public function errorInfo()
293
    {
294
        return $this->lastConnection->connection()->errorInfo();
295
    }
296
297
    private function hasCache() {
298
        return $this->cache !== null;
299
    }
300
301
    private function getCacheKey(Slave $slave) {
302
        return 'Slave_' . md5(serialize($slave->dbalConfig()));
303
    }
304
305
    public function setSlaveStatus(Slave $slave, bool $running, int $delay = null): array
306
    {
307
        if ($this->hasCache()) {
308
            $this->cache->setCacheItem($this->getCacheKey($slave), ['running' => $running, 'delay' => $delay], $this->slaveStatusCacheTtl);
309
        }
310
        return ['running' => $running, 'delay' => $delay];
311
    }
312
313
    private function isSlaveOK(Slave $slave): bool {
314
        if ($this->hasCache()) {
315
            $data = $this->cache->getCacheItem($this->getCacheKey($slave));
316
            if (is_array($data)) {
317
                if ($data['runnning'] === false || $data['delay'] > $this->maxSlaveDelay) {
318
                    return false;
319
                }
320
            }
321
        }
322
        return true;
323
    }
324
}
325