Passed
Branch dev (f56f10)
by Wilmer
04:41 queued 01:34
created

Connection::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 0

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 0
nc 1
nop 1
dl 0
loc 2
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Connection;
6
7
use Psr\Log\LogLevel;
8
use Throwable;
9
use Yiisoft\Cache\Dependency\Dependency;
10
use Yiisoft\Db\AwareTrait\LoggerAwareTrait;
11
use Yiisoft\Db\AwareTrait\ProfilerAwareTrait;
12
use Yiisoft\Db\Cache\QueryCache;
13
use Yiisoft\Db\Exception\Exception;
14
use Yiisoft\Db\Schema\TableSchema;
15
use Yiisoft\Db\Transaction\TransactionInterface;
16
17
abstract class Connection implements ConnectionInterface
18
{
19
    use LoggerAwareTrait;
20
    use ProfilerAwareTrait;
21
22
    protected array $masters = [];
23
    protected array $slaves = [];
24
    protected ?ConnectionInterface $master = null;
25
    protected ?ConnectionInterface $slave = null;
26
    protected ?TransactionInterface $transaction = null;
27
    private ?bool $emulatePrepare = null;
28
    private bool $enableSavepoint = true;
29
    private bool $enableSlaves = true;
30
    private int $serverRetryInterval = 600;
31
    private bool $shuffleMasters = true;
32
    private string $tablePrefix = '';
33
34
    public function __construct(private QueryCache $queryCache)
35
    {
36
    }
37
38
    public function areSlavesEnabled(): bool
39
    {
40
        return $this->enableSlaves;
41
    }
42
43
    public function beginTransaction(string $isolationLevel = null): TransactionInterface
44
    {
45
        $this->open();
46
        $this->transaction = $this->getTransaction();
47
48
        if ($this->transaction === null) {
49
            $this->transaction = $this->createTransaction();
50
        }
51
52
        if ($this->logger !== null) {
53
            $this->transaction->setLogger($this->logger);
54
        }
55
56
        $this->transaction->begin($isolationLevel);
57
58
        return $this->transaction;
59
    }
60
61
    public function cache(callable $callable, int $duration = null, Dependency $dependency = null): mixed
62
    {
63
        $this->queryCache->setInfo(
64
            [$duration ?? $this->queryCache->getDuration(), $dependency]
65
        );
66
        $result = $callable($this);
67
        $this->queryCache->removeLastInfo();
68
        return $result;
69
    }
70
71
    public function getEmulatePrepare(): ?bool
72
    {
73
        return $this->emulatePrepare;
74
    }
75
76
    public function getLastInsertID(string $sequenceName = ''): string
77
    {
78
        return $this->getSchema()->getLastInsertID($sequenceName);
79
    }
80
81
    public function getMaster(): ?self
82
    {
83
        if ($this->master === null) {
84
            $this->master = $this->shuffleMasters
85
                ? $this->openFromPool($this->masters)
86
                : $this->openFromPoolSequentially($this->masters);
87
        }
88
89
        return $this->master;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->master could return the type Yiisoft\Db\Connection\ConnectionInterface which includes types incompatible with the type-hinted return Yiisoft\Db\Connection\Connection|null. Consider adding an additional type-check to rule them out.
Loading history...
90
    }
91
92
    public function getSlave(bool $fallbackToMaster = true): ?self
93
    {
94
        if (!$this->enableSlaves) {
95
            return $fallbackToMaster ? $this : null;
96
        }
97
98
        if ($this->slave === null) {
99
            $this->slave = $this->openFromPool($this->slaves);
100
        }
101
102
        return $this->slave === null && $fallbackToMaster ? $this : $this->slave;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->slave === ... ? $this : $this->slave could return the type Yiisoft\Db\Connection\ConnectionInterface which includes types incompatible with the type-hinted return Yiisoft\Db\Connection\Connection|null. Consider adding an additional type-check to rule them out.
Loading history...
103
    }
104
105
    public function getTablePrefix(): string
106
    {
107
        return $this->tablePrefix;
108
    }
109
110
    public function getTableSchema(string $name, bool $refresh = false): ?TableSchema
111
    {
112
        return $this->getSchema()->getTableSchema($name, $refresh);
113
    }
114
115
    public function getTransaction(): ?TransactionInterface
116
    {
117
        return $this->transaction && $this->transaction->isActive() ? $this->transaction : null;
118
    }
119
120
    public function isSavepointEnabled(): bool
121
    {
122
        return $this->enableSavepoint;
123
    }
124
125
    public function noCache(callable $callable): mixed
126
    {
127
        $queryCache = $this->queryCache;
128
        $queryCache->setInfo(false);
129
        $result = $callable($this);
130
        $queryCache->removeLastInfo();
131
        return $result;
132
    }
133
134
    public function setEmulatePrepare(bool $value): void
135
    {
136
        $this->emulatePrepare = $value;
137
    }
138
139
    public function setEnableSavepoint(bool $value): void
140
    {
141
        $this->enableSavepoint = $value;
142
    }
143
144
    public function setEnableSlaves(bool $value): void
145
    {
146
        $this->enableSlaves = $value;
147
    }
148
149
    public function setMaster(string $key, ConnectionInterface $master): void
150
    {
151
        $this->masters[$key] = $master;
152
    }
153
154
    public function setServerRetryInterval(int $value): void
155
    {
156
        $this->serverRetryInterval = $value;
157
    }
158
159
    public function setShuffleMasters(bool $value): void
160
    {
161
        $this->shuffleMasters = $value;
162
    }
163
164
    public function setSlave(string $key, ConnectionInterface $slave): void
165
    {
166
        $this->slaves[$key] = $slave;
167
    }
168
169
    public function setTablePrefix(string $value): void
170
    {
171
        $this->tablePrefix = $value;
172
    }
173
174
    public function transaction(callable $callback, string $isolationLevel = null): mixed
175
    {
176
        $transaction = $this->beginTransaction($isolationLevel);
177
178
        $level = $transaction->getLevel();
179
180
        try {
181
            $result = $callback($this);
182
183
            if ($transaction->isActive() && $transaction->getLevel() === $level) {
184
                $transaction->commit();
185
            }
186
        } catch (Throwable $e) {
187
            $this->rollbackTransactionOnLevel($transaction, $level);
188
189
            throw $e;
190
        }
191
192
        return $result;
193
    }
194
195
    public function useMaster(callable $callback): mixed
196
    {
197
        if ($this->enableSlaves) {
198
            $this->enableSlaves = false;
199
200
            try {
201
                $result = $callback($this);
202
            } catch (Throwable $e) {
203
                $this->enableSlaves = true;
204
205
                throw $e;
206
            }
207
            $this->enableSlaves = true;
208
        } else {
209
            $result = $callback($this);
210
        }
211
212
        return $result;
213
    }
214
215
    /**
216
     * Opens the connection to a server in the pool.
217
     *
218
     * This method implements the load balancing among the given list of the servers.
219
     *
220
     * Connections will be tried in random order.
221
     *
222
     * @param array $pool The list of connection configurations in the server pool.
223
     *
224
     * @return static|null The opened DB connection, or `null` if no server is available.
225
     */
226
    protected function openFromPool(array $pool): ?self
227
    {
228
        shuffle($pool);
229
        return $this->openFromPoolSequentially($pool);
230
    }
231
232
    /**
233
     * Opens the connection to a server in the pool.
234
     *
235
     * This method implements the load balancing among the given list of the servers.
236
     *
237
     * Connections will be tried in sequential order.
238
     *
239
     * @param array $pool
240
     *
241
     * @return static|null The opened DB connection, or `null` if no server is available.
242
     */
243
    protected function openFromPoolSequentially(array $pool): ?self
244
    {
245
        if (!$pool) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $pool of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
246
            return null;
247
        }
248
249
        foreach ($pool as $poolConnection) {
250
            $key = [__METHOD__, $poolConnection->getDriver()->getDsn()];
251
252
            if (
253
                $this->getSchema()->getSchemaCache()->isEnabled() &&
254
                $this->getSchema()->getSchemaCache()->getOrSet($key, null, $this->serverRetryInterval)
255
            ) {
256
                /** should not try this dead server now */
257
                continue;
258
            }
259
260
            try {
261
                $poolConnection->open();
262
263
                return $poolConnection;
264
            } catch (Exception $e) {
265
                $this->logger?->log(
266
                    LogLevel::WARNING,
267
                    "Connection ({$poolConnection->getDriver()->getDsn()}) failed: " . $e->getMessage() . ' ' . __METHOD__
268
                );
269
270
                if ($this->getSchema()->getSchemaCache()->isEnabled()) {
271
                    /** mark this server as dead and only retry it after the specified interval */
272
                    $this->getSchema()->getSchemaCache()->set($key, 1, $this->serverRetryInterval);
273
                }
274
275
                return null;
276
            }
277
        }
278
279
        return null;
280
    }
281
282
    /**
283
     * Rolls back given {@see TransactionInterface} object if it's still active and level match. In some cases rollback
284
     * can fail, so this method is fail-safe. Exceptions thrown from rollback will be caught and just logged with
285
     * {@see logger->log()}.
286
     *
287
     * @param TransactionInterface $transaction TransactionInterface object given from {@see beginTransaction()}.
288
     * @param int $level TransactionInterface level just after {@see beginTransaction()} call.
289
     */
290
    private function rollbackTransactionOnLevel(TransactionInterface $transaction, int $level): void
291
    {
292
        if ($transaction->isActive() && $transaction->getLevel() === $level) {
293
            /**
294
             * {@see https://github.com/yiisoft/yii2/pull/13347}
295
             */
296
            try {
297
                $transaction->rollBack();
298
            } catch (\Exception $e) {
299
                if ($this->logger !== null) {
300
                    $this->logger->log(LogLevel::ERROR, $e, [__METHOD__]);
301
                    /** hide this exception to be able to continue throwing original exception outside */
302
                }
303
            }
304
        }
305
    }
306
}
307