Passed
Push — master ( 9607e5...058f8b )
by Wilmer
09:15
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\LoggerInterface;
8
use Psr\Log\LogLevel;
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 = null;
73
    private ?LoggerInterface $logger = null;
74
75 40
    public function __construct(?Connection $db, ?LoggerInterface $logger)
76
    {
77 40
        $this->db = $db;
78 40
        $this->logger = $logger;
79 40
    }
80
81
    /**
82
     * Returns a value indicating whether this transaction is active.
83
     *
84
     * @return bool whether this transaction is active. Only an active transaction can {@see commit()} or
85
     * {@see rollBack()}.
86
     */
87 40
    public function isActive(): bool
88
    {
89 40
        return $this->level > 0 && $this->db && $this->db->isActive();
90
    }
91
92
    /**
93
     * Begins a transaction.
94
     *
95
     * @param string|null $isolationLevel The {@see isolation level}[] to use for this transaction.
96
     * This can be one of {@see READ_UNCOMMITTED}, {@see READ_COMMITTED}, {@see REPEATABLE_READ} and {@see SERIALIZABLE}
97
     * but also a string containing DBMS specific syntax to be used after `SET TRANSACTION ISOLATION LEVEL`.
98
     *
99
     * If not specified (`null`) the isolation level will not be set explicitly and the DBMS default will be used.
100
     *
101
     * > Note: This setting does not work for PostgreSQL, where setting the isolation level before the transaction has
102
     * no effect. You have to call {@see setIsolationLevel()} in this case after the transaction has started.
103
     *
104
     * > Note: Some DBMS allow setting of the isolation level only for the whole connection so subsequent transactions
105
     * may get the same isolation level even if you did not specify any. When using this feature you may need to set the
106
     * isolation level for all transactions explicitly to avoid conflicting settings.
107
     * At the time of this writing affected DBMS are MSSQL and SQLite.
108
     *
109
     * [isolation level]: http://en.wikipedia.org/wiki/Isolation_%28database_systems%29#Isolation_levels
110
     *
111
     * @throws InvalidConfigException if {@see db} is `null`
112
     * @throws NotSupportedException if the DBMS does not support nested transactions
113
     * @throws Exception if DB connection fails
114
     */
115 40
    public function begin(?string $isolationLevel = null): void
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
            $this->logger->log(
0 ignored issues
show
Bug introduced by
The method log() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

128
            $this->logger->/** @scrutinizer ignore-call */ 
129
                           log(

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
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
            $this->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
            $this->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 if the transaction is not active
165
     */
166 24
    public function commit(): void
167
    {
168 24
        if (!$this->isActive()) {
169
            throw new Exception('Failed to commit transaction: transaction was inactive.');
170
        }
171
172 24
        $this->level--;
173 24
        if ($this->level === 0) {
174 24
            $this->logger->log(
175 24
                LogLevel::DEBUG,
176 24
                'Commit transaction ' . __METHOD__
177
            );
178
179 24
            $this->db->getPDO()->commit();
180
181 24
            return;
182
        }
183
184
        $schema = $this->db->getSchema();
185
        if ($schema->supportsSavepoint()) {
186
            $this->logger->log(
187
                LogLevel::DEBUG,
188
                'Release savepoint ' . $this->level . ' ' . __METHOD__
189
            );
190
            $schema->releaseSavepoint('LEVEL' . $this->level);
191
        } else {
192
            $this->logger->log(
193
                LogLevel::INFO,
194
                'Transaction not committed: nested transaction not supported ' . __METHOD__
195
            );
196
        }
197
    }
198
199
    /**
200
     * Rolls back a transaction.
201
     */
202 21
    public function rollBack(): void
203
    {
204 21
        if (!$this->isActive()) {
205
            /**
206
             * do nothing if transaction is not active: this could be the transaction is committed but the event handler
207
             * to "commitTransaction" throw an exception
208
             */
209
            return;
210
        }
211
212 21
        $this->level--;
213 21
        if ($this->level === 0) {
214 16
            $this->logger->log(
215 16
                LogLevel::INFO,
216 16
                'Roll back transaction ' . __METHOD__
217
            );
218
219 16
            $this->db->getPDO()->rollBack();
220
221 16
            return;
222
        }
223
224 5
        $schema = $this->db->getSchema();
225 5
        if ($schema->supportsSavepoint()) {
226 5
            $this->logger->log(
227 5
                LogLevel::DEBUG,
228 5
                'Roll back to savepoint ' . $this->level . ' ' . __METHOD__
229
            );
230
231 5
            $schema->rollBackSavepoint('LEVEL' . $this->level);
232
        } else {
233
            $this->logger->log(
234
                LogLevel::INFO,
235
                'Transaction not rolled back: nested transaction not supported ' . __METHOD__
236
            );
237
        }
238 5
    }
239
240
    /**
241
     * Sets the transaction isolation level for this transaction.
242
     *
243
     * This method can be used to set the isolation level while the transaction is already active.
244
     * However this is not supported by all DBMS so you might rather specify the isolation level directly when calling
245
     * {@see begin()}.
246
     *
247
     * @param string $level The transaction isolation level to use for this transaction.
248
     * This can be one of {@see READ_UNCOMMITTED}, {@see READ_COMMITTED}, {@see REPEATABLE_READ} and {@see SERIALIZABLE}
249
     * but also a string containing DBMS specific syntax to be used after `SET TRANSACTION ISOLATION LEVEL`.
250
     *
251
     * @throws Exception if the transaction is not active.
252
     *
253
     * {@see http://en.wikipedia.org/wiki/Isolation_%28database_systems%29#Isolation_levels}
254
     */
255 1
    public function setIsolationLevel(string $level): void
256
    {
257 1
        if (!$this->isActive()) {
258
            throw new Exception('Failed to set isolation level: transaction was inactive.');
259
        }
260
261 1
        $this->logger->log(
262 1
            LogLevel::DEBUG,
263 1
            'Setting transaction isolation level to ' . $this->level . ' ' . __METHOD__
264
        );
265
266 1
        $this->db->getSchema()->setTransactionIsolationLevel($level);
267 1
    }
268
269
    /**
270
     * @return int the nesting level of the transaction. 0 means the outermost level.
271
     */
272 25
    public function getLevel(): int
273
    {
274 25
        return $this->level;
275
    }
276
277
    /**
278
     * @return Connection|null the database connection that this transaction is associated with.
279
     */
280
    public function getDb(): ?Connection
281
    {
282
        return $this->db;
283
    }
284
}
285