Passed
Pull Request — master (#671)
by Wilmer
14:35 queued 12:26
created

AbstractPdoTransaction::rollBack()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 26
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 14
nc 4
nop 0
dl 0
loc 26
rs 9.7998
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Driver\Pdo;
6
7
use Psr\Log\LoggerAwareTrait;
8
use Psr\Log\LogLevel;
9
use Throwable;
10
use Yiisoft\Db\Exception\Exception;
11
use Yiisoft\Db\Exception\InvalidConfigException;
12
use Yiisoft\Db\Exception\NotSupportedException;
13
use Yiisoft\Db\Transaction\TransactionInterface;
14
15
/**
16
 * Represents a DB transaction.
17
 *
18
 * A transaction is a set of SQL statements that must either all succeed or all fail.
19
 *
20
 * It's usually created by calling {@see \Yiisoft\Db\Connection\AbstractConnectionAbstractConnection::beginTransaction()}.
21
 *
22
 * The following code is a typical example of using transactions (note that some DBMS may not support transactions):
23
 *
24
 * ```php
25
 * $transaction = $connection->beginTransaction();
26
 * try {
27
 *     $connection->createCommand($sql1)->execute();
28
 *     $connection->createCommand($sql2)->execute();
29
 *     // ... other SQL executions
30
 *     $transaction->commit();
31
 * } catch (\Throwable $e) {
32
 *     $transaction->rollBack();
33
 *     throw $e;
34
 * }
35
 * ```
36
 */
37
abstract class AbstractPdoTransaction implements TransactionInterface
38
{
39
    use LoggerAwareTrait;
40
41
    /**
42
     * @var int The nesting level of the transaction.
43
     */
44
    private int $level = 0;
45
46
    public function __construct(protected PdoConnectionInterface $db)
47
    {
48
    }
49
50
    public function begin(string $isolationLevel = null): void
51
    {
52
        $this->db->open();
53
54
        if ($this->level === 0) {
55
            if ($isolationLevel !== null) {
56
                $this->setTransactionIsolationLevel($isolationLevel);
57
            }
58
59
            $this->logger?->log(
60
                LogLevel::DEBUG,
61
                'Begin transaction' . ($isolationLevel ? ' with isolation level ' . $isolationLevel : '')
62
                . ' ' . __METHOD__
63
            );
64
65
            $this->db->getPDO()?->beginTransaction();
66
            $this->level = 1;
67
68
            return;
69
        }
70
71
        if ($this->db->isSavepointEnabled()) {
72
            $this->logger?->log(LogLevel::DEBUG, 'Set savepoint ' . $this->level . ' ' . __METHOD__);
73
74
            $this->createSavepoint('LEVEL' . $this->level);
75
        } else {
76
            $this->logger?->log(
77
                LogLevel::DEBUG,
78
                'Transaction not started: nested transaction not supported ' . __METHOD__
79
            );
80
81
            throw new NotSupportedException('Transaction not started: nested transaction not supported.');
82
        }
83
84
        $this->level++;
85
    }
86
87
    public function commit(): void
88
    {
89
        if (!$this->isActive()) {
90
            throw new Exception('Failed to commit transaction: transaction was inactive.');
91
        }
92
93
        $this->level--;
94
95
        if ($this->level === 0) {
96
            $this->logger?->log(LogLevel::DEBUG, 'Commit transaction ' . __METHOD__);
97
            $this->db->getPDO()?->commit();
98
99
            return;
100
        }
101
102
        if ($this->db->isSavepointEnabled()) {
103
            $this->logger?->log(LogLevel::DEBUG, 'Release savepoint ' . $this->level . ' ' . __METHOD__);
104
            $this->releaseSavepoint('LEVEL' . $this->level);
105
        } else {
106
            $this->logger?->log(
107
                LogLevel::INFO,
108
                'Transaction not committed: nested transaction not supported ' . __METHOD__
109
            );
110
        }
111
    }
112
113
    public function getLevel(): int
114
    {
115
        return $this->level;
116
    }
117
118
    public function isActive(): bool
119
    {
120
        /** Extra check pdo->inTransaction {@link https://github.com/yiisoft/yii2/pull/18407/} */
121
        return $this->level > 0 && $this->db->isActive() && $this->db->getPDO()?->inTransaction();
122
    }
123
124
    public function rollBack(): void
125
    {
126
        if (!$this->isActive()) {
127
            /**
128
             * Do nothing if a transaction isn't active: this could be the transaction is committed but the event
129
             * handler to "commitTransaction" throw an exception
130
             */
131
            return;
132
        }
133
134
        $this->level--;
135
136
        if ($this->level === 0) {
137
            $this->logger?->log(LogLevel::INFO, 'Roll back transaction ' . __METHOD__);
138
            $this->db->getPDO()?->rollBack();
139
140
            return;
141
        }
142
143
        if ($this->db->isSavepointEnabled()) {
144
            $this->logger?->log(LogLevel::DEBUG, 'Roll back to savepoint ' . $this->level . ' ' . __METHOD__);
145
            $this->rollBackSavepoint('LEVEL' . $this->level);
146
        } else {
147
            $this->logger?->log(
148
                LogLevel::INFO,
149
                'Transaction not rolled back: nested transaction not supported ' . __METHOD__
150
            );
151
        }
152
    }
153
154
    public function setIsolationLevel(string $level): void
155
    {
156
        if (!$this->isActive()) {
157
            throw new Exception('Failed to set isolation level: transaction was inactive.');
158
        }
159
160
        $this->logger?->log(
161
            LogLevel::DEBUG,
162
            'Setting transaction isolation level to ' . $this->level . ' ' . __METHOD__
163
        );
164
        $this->setTransactionIsolationLevel($level);
165
    }
166
167
    public function createSavepoint(string $name): void
168
    {
169
        $this->db->createCommand("SAVEPOINT $name")->execute();
170
    }
171
172
    public function rollBackSavepoint(string $name): void
173
    {
174
        $this->db->createCommand("ROLLBACK TO SAVEPOINT $name")->execute();
175
    }
176
177
    public function releaseSavepoint(string $name): void
178
    {
179
        $this->db->createCommand("RELEASE SAVEPOINT $name")->execute();
180
    }
181
182
    /**
183
     * Sets the transaction isolation level.
184
     *
185
     * @throws Exception
186
     * @throws InvalidConfigException
187
     * @throws Throwable
188
     */
189
    protected function setTransactionIsolationLevel(string $level): void
190
    {
191
        $this->db->createCommand("SET TRANSACTION ISOLATION LEVEL $level")->execute();
192
    }
193
}
194