Passed
Push — fix-tests ( 6dd538...727178 )
by Alexander
195:41 queued 192:18
created

Transaction::begin()   A

Complexity

Conditions 6
Paths 5

Size

Total Lines 29
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 6.0052

Importance

Changes 0
Metric Value
cc 6
eloc 19
nc 5
nop 1
dl 0
loc 29
ccs 18
cts 19
cp 0.9474
crap 6.0052
rs 9.0111
c 0
b 0
f 0
1
<?php
2
/**
3
 * @link http://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license http://www.yiiframework.com/license/
6
 */
7
8
namespace yii\db;
9
10
use Yii;
11
use yii\base\InvalidConfigException;
12
use yii\base\NotSupportedException;
13
14
/**
15
 * Transaction represents a DB transaction.
16
 *
17
 * It is usually created by calling [[Connection::beginTransaction()]].
18
 *
19
 * The following code is a typical example of using transactions (note that some
20
 * 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 (\Exception $e) {
30
 *     $transaction->rollBack();
31
 *     throw $e;
32
 * } catch (\Throwable $e) {
33
 *     $transaction->rollBack();
34
 *     throw $e;
35
 * }
36
 * ```
37
 *
38
 * > Note: in the above code we have two catch-blocks for compatibility
39
 * > with PHP 5.x and PHP 7.x. `\Exception` implements the [`\Throwable` interface](https://secure.php.net/manual/en/class.throwable.php)
40
 * > since PHP 7.0, so you can skip the part with `\Exception` if your app uses only PHP 7.0 and higher.
41
 *
42
 * @property bool $isActive Whether this transaction is active. Only an active transaction can [[commit()]] or
43
 * [[rollBack()]]. This property is read-only.
44
 * @property string $isolationLevel The transaction isolation level to use for this transaction. This can be
45
 * one of [[READ_UNCOMMITTED]], [[READ_COMMITTED]], [[REPEATABLE_READ]] and [[SERIALIZABLE]] but also a string
46
 * containing DBMS specific syntax to be used after `SET TRANSACTION ISOLATION LEVEL`. This property is
47
 * write-only.
48
 * @property int $level The current nesting level of the transaction. This property is read-only.
49
 *
50
 * @author Qiang Xue <[email protected]>
51
 * @since 2.0
52
 */
53
class Transaction extends \yii\base\BaseObject
54
{
55
    /**
56
     * A constant representing the transaction isolation level `READ UNCOMMITTED`.
57
     * @see http://en.wikipedia.org/wiki/Isolation_%28database_systems%29#Isolation_levels
58
     */
59
    const READ_UNCOMMITTED = 'READ UNCOMMITTED';
60
    /**
61
     * A constant representing the transaction isolation level `READ COMMITTED`.
62
     * @see http://en.wikipedia.org/wiki/Isolation_%28database_systems%29#Isolation_levels
63
     */
64
    const READ_COMMITTED = 'READ COMMITTED';
65
    /**
66
     * A constant representing the transaction isolation level `REPEATABLE READ`.
67
     * @see http://en.wikipedia.org/wiki/Isolation_%28database_systems%29#Isolation_levels
68
     */
69
    const REPEATABLE_READ = 'REPEATABLE READ';
70
    /**
71
     * A constant representing the transaction isolation level `SERIALIZABLE`.
72
     * @see http://en.wikipedia.org/wiki/Isolation_%28database_systems%29#Isolation_levels
73
     */
74
    const SERIALIZABLE = 'SERIALIZABLE';
75
76
    /**
77
     * @var Connection the database connection that this transaction is associated with.
78
     */
79
    public $db;
80
81
    /**
82
     * @var int the nesting level of the transaction. 0 means the outermost level.
83
     */
84
    private $_level = 0;
85
86
87
    /**
88
     * Returns a value indicating whether this transaction is active.
89
     * @return bool whether this transaction is active. Only an active transaction
90
     * can [[commit()]] or [[rollBack()]].
91
     */
92 39
    public function getIsActive()
93
    {
94 39
        return $this->_level > 0 && $this->db && $this->db->isActive;
95
    }
96
97
    /**
98
     * Begins a transaction.
99
     * @param string|null $isolationLevel The [isolation level][] to use for this transaction.
100
     * This can be one of [[READ_UNCOMMITTED]], [[READ_COMMITTED]], [[REPEATABLE_READ]] and [[SERIALIZABLE]] but
101
     * also a string containing DBMS specific syntax to be used after `SET TRANSACTION ISOLATION LEVEL`.
102
     * If not specified (`null`) the isolation level will not be set explicitly and the DBMS default will be used.
103
     *
104
     * > Note: This setting does not work for PostgreSQL, where setting the isolation level before the transaction
105
     * has no effect. You have to call [[setIsolationLevel()]] in this case after the transaction has started.
106
     *
107
     * > Note: Some DBMS allow setting of the isolation level only for the whole connection so subsequent transactions
108
     * may get the same isolation level even if you did not specify any. When using this feature
109
     * you may need to set the isolation level for all transactions explicitly to avoid conflicting settings.
110
     * At the time of this writing affected DBMS are MSSQL and SQLite.
111
     *
112
     * [isolation level]: http://en.wikipedia.org/wiki/Isolation_%28database_systems%29#Isolation_levels
113
     *
114
     * Starting from version 2.0.16, this method throws exception when beginning nested transaction and underlying DBMS
115
     * does not support savepoints.
116
     * @throws InvalidConfigException if [[db]] is `null`
117
     * @throws NotSupportedException if the DBMS does not support nested transactions
118
     * @throws Exception if DB connection fails
119
     */
120 39
    public function begin($isolationLevel = null)
121
    {
122 39
        if ($this->db === null) {
123
            throw new InvalidConfigException('Transaction::db must be set.');
124
        }
125 39
        $this->db->open();
126
127 39
        if ($this->_level === 0) {
128 39
            if ($isolationLevel !== null) {
129 7
                $this->db->getSchema()->setTransactionIsolationLevel($isolationLevel);
130
            }
131 39
            Yii::debug('Begin transaction' . ($isolationLevel ? ' with isolation level ' . $isolationLevel : ''), __METHOD__);
132
133 39
            $this->db->trigger(Connection::EVENT_BEGIN_TRANSACTION);
134 39
            $this->db->pdo->beginTransaction();
135 39
            $this->_level = 1;
136
137 39
            return;
138
        }
139
140 8
        $schema = $this->db->getSchema();
141 8
        if ($schema->supportsSavepoint()) {
142 4
            Yii::debug('Set savepoint ' . $this->_level, __METHOD__);
143 4
            $schema->createSavepoint('LEVEL' . $this->_level);
144
        } else {
145 4
            Yii::info('Transaction not started: nested transaction not supported', __METHOD__);
146 4
            throw new NotSupportedException('Transaction not started: nested transaction not supported.');
147
        }
148 4
        $this->_level++;
149 4
    }
150
151
    /**
152
     * Commits a transaction.
153
     * @throws Exception if the transaction is not active
154
     */
155 23
    public function commit()
156
    {
157 23
        if (!$this->getIsActive()) {
158
            throw new Exception('Failed to commit transaction: transaction was inactive.');
159
        }
160
161 23
        $this->_level--;
162 23
        if ($this->_level === 0) {
163 23
            Yii::debug('Commit transaction', __METHOD__);
164 23
            $this->db->pdo->commit();
165 23
            $this->db->trigger(Connection::EVENT_COMMIT_TRANSACTION);
166 23
            return;
167
        }
168
169
        $schema = $this->db->getSchema();
170
        if ($schema->supportsSavepoint()) {
171
            Yii::debug('Release savepoint ' . $this->_level, __METHOD__);
172
            $schema->releaseSavepoint('LEVEL' . $this->_level);
173
        } else {
174
            Yii::info('Transaction not committed: nested transaction not supported', __METHOD__);
175
        }
176
    }
177
178
    /**
179
     * Rolls back a transaction.
180
     */
181 20
    public function rollBack()
182
    {
183 20
        if (!$this->getIsActive()) {
184
            // do nothing if transaction is not active: this could be the transaction is committed
185
            // but the event handler to "commitTransaction" throw an exception
186
            return;
187
        }
188
189 20
        $this->_level--;
190 20
        if ($this->_level === 0) {
191 16
            Yii::debug('Roll back transaction', __METHOD__);
192 16
            $this->db->pdo->rollBack();
193 16
            $this->db->trigger(Connection::EVENT_ROLLBACK_TRANSACTION);
194 16
            return;
195
        }
196
197 4
        $schema = $this->db->getSchema();
198 4
        if ($schema->supportsSavepoint()) {
199 4
            Yii::debug('Roll back to savepoint ' . $this->_level, __METHOD__);
200 4
            $schema->rollBackSavepoint('LEVEL' . $this->_level);
201
        } else {
202
            Yii::info('Transaction not rolled back: nested transaction not supported', __METHOD__);
203
        }
204 4
    }
205
206
    /**
207
     * Sets the transaction isolation level for this transaction.
208
     *
209
     * This method can be used to set the isolation level while the transaction is already active.
210
     * However this is not supported by all DBMS so you might rather specify the isolation level directly
211
     * when calling [[begin()]].
212
     * @param string $level The transaction isolation level to use for this transaction.
213
     * This can be one of [[READ_UNCOMMITTED]], [[READ_COMMITTED]], [[REPEATABLE_READ]] and [[SERIALIZABLE]] but
214
     * also a string containing DBMS specific syntax to be used after `SET TRANSACTION ISOLATION LEVEL`.
215
     * @throws Exception if the transaction is not active
216
     * @see http://en.wikipedia.org/wiki/Isolation_%28database_systems%29#Isolation_levels
217
     */
218 1
    public function setIsolationLevel($level)
219
    {
220 1
        if (!$this->getIsActive()) {
221
            throw new Exception('Failed to set isolation level: transaction was inactive.');
222
        }
223 1
        Yii::debug('Setting transaction isolation level to ' . $level, __METHOD__);
224 1
        $this->db->getSchema()->setTransactionIsolationLevel($level);
225 1
    }
226
227
    /**
228
     * @return int The current nesting level of the transaction.
229
     * @since 2.0.8
230
     */
231 23
    public function getLevel()
232
    {
233 23
        return $this->_level;
234
    }
235
}
236