Passed
Push — master ( a8d37b...dabdd0 )
by Wilmer
10:00
created

Transaction::isActive()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 3

Importance

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