Passed
Pull Request — master (#215)
by Wilmer
14:22
created

Transaction::getDb()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 0
cts 2
cp 0
crap 2
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Transaction;
6
7
use Psr\Log\LogLevel;
8
use Throwable;
9
use Yiisoft\Db\Connection\Connection;
10
use Yiisoft\Db\Exception\Exception;
11
use Yiisoft\Db\Exception\InvalidConfigException;
12
use Yiisoft\Db\Exception\NotSupportedException;
13
use Yiisoft\Db\Factory\LoggerFactory;
0 ignored issues
show
Bug introduced by
The type Yiisoft\Db\Factory\LoggerFactory was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
14
15
/**
16
 * Transaction represents a DB transaction.
17
 *
18
 * It is usually created by calling {@see Connection::beginTransaction()}.
19
 *
20
 * The following code is a typical example of using transactions (note that some DBMS may not support transactions):
21
 *
22
 * ```php
23
 * $transaction = $connection->beginTransaction();
24
 * try {
25
 *     $connection->createCommand($sql1)->execute();
26
 *     $connection->createCommand($sql2)->execute();
27
 *     //.... other SQL executions
28
 *     $transaction->commit();
29
 * } catch (\Throwable $e) {
30
 *     $transaction->rollBack();
31
 *     throw $e;
32
 * }
33
 * ```
34
 *
35
 * @property bool $isActive Whether this transaction is active. Only an active transaction can {@see commit()} or
36
 * {@see rollBack()}. This property is read-only.
37
 * @property string $isolationLevel The transaction isolation level to use for this transaction. This can be one of
38
 * {@see READ_UNCOMMITTED}, {@see READ_COMMITTED}, {@see REPEATABLE_READ} and {@see SERIALIZABLE} but also a string
39
 * containing DBMS specific syntax to be used after `SET TRANSACTION ISOLATION LEVEL`. This property is write-only.
40
 * @property int $level The current nesting level of the transaction. This property is read-only.
41
 */
42
class Transaction
43
{
44
    /**
45
     * A constant representing the transaction isolation level `READ UNCOMMITTED`.
46
     *
47
     * {@see http://en.wikipedia.org/wiki/Isolation_%28database_systems%29#Isolation_levels}
48
     */
49
    public const READ_UNCOMMITTED = 'READ UNCOMMITTED';
50
51
    /**
52
     * A constant representing the transaction isolation level `READ COMMITTED`.
53
     *
54
     * {@see http://en.wikipedia.org/wiki/Isolation_%28database_systems%29#Isolation_levels}
55
     */
56
    public const READ_COMMITTED = 'READ COMMITTED';
57
58
    /**
59
     * A constant representing the transaction isolation level `REPEATABLE READ`.
60
     *
61
     * {@see http://en.wikipedia.org/wiki/Isolation_%28database_systems%29#Isolation_levels}
62
     */
63
    public const REPEATABLE_READ = 'REPEATABLE READ';
64
65
    /**
66
     * A constant representing the transaction isolation level `SERIALIZABLE`.
67
     *
68
     * {@see http://en.wikipedia.org/wiki/Isolation_%28database_systems%29#Isolation_levels}
69
     */
70
    public const SERIALIZABLE = 'SERIALIZABLE';
71
72
    private int $level = 0;
73
    private Connection $db;
74
75 40
    public function __construct(Connection $db)
76
    {
77 40
        $this->db = $db;
78 40
    }
79 40
80
    /**
81
     * Returns a value indicating whether this transaction is active.
82
     *
83
     * @return bool whether this transaction is active. Only an active transaction can {@see commit()} or
84
     * {@see rollBack()}.
85
     */
86
    public function isActive(): bool
87 40
    {
88
        return $this->level > 0 && $this->db && $this->db->isActive();
89 40
    }
90
91
    /**
92
     * Begins a transaction.
93
     *
94
     * @param string|null $isolationLevel The {@see isolation level}[] to use for this transaction.
95
     * This can be one of {@see READ_UNCOMMITTED}, {@see READ_COMMITTED}, {@see REPEATABLE_READ} and {@see SERIALIZABLE}
96
     * but also a string containing DBMS specific syntax to be used after `SET TRANSACTION ISOLATION LEVEL`.
97
     *
98
     * If not specified (`null`) the isolation level will not be set explicitly and the DBMS default will be used.
99
     *
100
     * > Note: This setting does not work for PostgreSQL, where setting the isolation level before the transaction has
101
     * no effect. You have to call {@see setIsolationLevel()} in this case after the transaction has started.
102
     *
103
     * > Note: Some DBMS allow setting of the isolation level only for the whole connection so subsequent transactions
104
     * may get the same isolation level even if you did not specify any. When using this feature you may need to set the
105
     * isolation level for all transactions explicitly to avoid conflicting settings.
106
     * At the time of this writing affected DBMS are MSSQL and SQLite.
107
     *
108
     * [isolation level]: http://en.wikipedia.org/wiki/Isolation_%28database_systems%29#Isolation_levels
109
     *
110
     * @throws InvalidConfigException if {@see db} is `null`
111
     * @throws NotSupportedException if the DBMS does not support nested transactions
112
     * @throws Exception|Throwable if DB connection fails
113
     */
114
    public function begin(?string $isolationLevel = null): void
115 40
    {
116
        $logger = $this->db->getLogger();
117 40
118
        if ($this->db === null) {
119
            throw new InvalidConfigException('Transaction::db must be set.');
120
        }
121 40
122
        $this->db->open();
123 40
124 40
        if ($this->level === 0) {
125 9
            if ($isolationLevel !== null) {
126
                $this->db->getSchema()->setTransactionIsolationLevel($isolationLevel);
127
            }
128 40
129 40
            $logger->log(
130 40
                LogLevel::DEBUG,
131 40
                'Begin transaction' . ($isolationLevel ? ' with isolation level ' . $isolationLevel : '')
132
                . ' ' . __METHOD__
133
            );
134 40
135 40
            $this->db->getPDO()->beginTransaction();
136
            $this->level = 1;
137 40
138
            return;
139
        }
140 10
141
        $schema = $this->db->getSchema();
142 10
143 5
        if ($schema->supportsSavepoint()) {
144 5
            $logger->log(
145 5
                LogLevel::DEBUG,
146
                'Set savepoint ' . $this->level . ' ' . __METHOD__
147
            );
148 5
149
            $schema->createSavepoint('LEVEL' . $this->level);
150 5
        } else {
151 5
            $logger->log(
152 5
                LogLevel::DEBUG,
153
                'Transaction not started: nested transaction not supported ' . __METHOD__
154
            );
155 5
156
            throw new NotSupportedException('Transaction not started: nested transaction not supported.');
157
        }
158 5
159 5
        $this->level++;
160
    }
161
162
    /**
163
     * Commits a transaction.
164
     *
165
     * @throws Exception|Throwable if the transaction is not active
166 24
     */
167
    public function commit(): void
168 24
    {
169
        $logger = $this->db->getLogger();
170
171
        if (!$this->isActive()) {
172 24
            throw new Exception('Failed to commit transaction: transaction was inactive.');
173 24
        }
174 24
175 24
        $this->level--;
176 24
        if ($this->level === 0) {
177
            $logger->log(
178
                LogLevel::DEBUG,
179 24
                'Commit transaction ' . __METHOD__
180
            );
181 24
182
            $this->db->getPDO()->commit();
183
184
            return;
185
        }
186
187
        $schema = $this->db->getSchema();
188
        if ($schema->supportsSavepoint()) {
189
            $logger->log(
190
                LogLevel::DEBUG,
191
                'Release savepoint ' . $this->level . ' ' . __METHOD__
192
            );
193
            $schema->releaseSavepoint('LEVEL' . $this->level);
194
        } else {
195
            $logger->log(
196
                LogLevel::INFO,
197
                'Transaction not committed: nested transaction not supported ' . __METHOD__
198
            );
199
        }
200
    }
201
202 21
    /**
203
     * Rolls back a transaction.
204 21
     */
205
    public function rollBack(): void
206
    {
207
        $logger = $this->db->getLogger();
208
209
        if (!$this->isActive()) {
210
            /**
211
             * do nothing if transaction is not active: this could be the transaction is committed but the event handler
212 21
             * to "commitTransaction" throw an exception
213 21
             */
214 16
            return;
215 16
        }
216 16
217
        $this->level--;
218
        if ($this->level === 0) {
219 16
            $logger->log(
220
                LogLevel::INFO,
221 16
                'Roll back transaction ' . __METHOD__
222
            );
223
224 5
            $this->db->getPDO()->rollBack();
225 5
226 5
            return;
227 5
        }
228 5
229
        $schema = $this->db->getSchema();
230
        if ($schema->supportsSavepoint()) {
231 5
            $logger->log(
232
                LogLevel::DEBUG,
233
                'Roll back to savepoint ' . $this->level . ' ' . __METHOD__
234
            );
235
236
            $schema->rollBackSavepoint('LEVEL' . $this->level);
237
        } else {
238 5
            $logger->log(
239
                LogLevel::INFO,
240
                'Transaction not rolled back: nested transaction not supported ' . __METHOD__
241
            );
242
        }
243
    }
244
245
    /**
246
     * Sets the transaction isolation level for this transaction.
247
     *
248
     * This method can be used to set the isolation level while the transaction is already active.
249
     * However this is not supported by all DBMS so you might rather specify the isolation level directly when calling
250
     * {@see begin()}.
251
     *
252
     * @param string $level The transaction isolation level to use for this transaction.
253
     * This can be one of {@see READ_UNCOMMITTED}, {@see READ_COMMITTED}, {@see REPEATABLE_READ} and {@see SERIALIZABLE}
254
     * but also a string containing DBMS specific syntax to be used after `SET TRANSACTION ISOLATION LEVEL`.
255 1
     *
256
     * @throws Exception|Throwable if the transaction is not active.
257 1
     *
258
     * {@see http://en.wikipedia.org/wiki/Isolation_%28database_systems%29#Isolation_levels}
259
     */
260
    public function setIsolationLevel(string $level): void
261 1
    {
262 1
        $logger = $this->db->getLogger();
263 1
264
        if (!$this->isActive()) {
265
            throw new Exception('Failed to set isolation level: transaction was inactive.');
266 1
        }
267 1
268
        $logger->log(
269
            LogLevel::DEBUG,
270
            'Setting transaction isolation level to ' . $this->level . ' ' . __METHOD__
271
        );
272 25
273
        $this->db->getSchema()->setTransactionIsolationLevel($level);
274 25
    }
275
276
    /**
277
     * @return int the nesting level of the transaction. 0 means the outermost level.
278
     */
279
    public function getLevel(): int
280
    {
281
        return $this->level;
282
    }
283
}
284