Passed
Pull Request — master (#455)
by Wilmer
27:34 queued 24:54
created

AbstractTransactionPDO::isActive()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 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
 * 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
abstract class AbstractTransactionPDO implements TransactionInterface
43
{
44
    use LoggerAwareTrait;
45
46
    private int $level = 0;
47
48
    public function __construct(protected ConnectionPDOInterface $db)
49
    {
50
    }
51
52
    /**
53
     * @inheritDoc
54
     */
55
    public function begin(string $isolationLevel = null): void
56
    {
57
        $this->db->open();
58
59
        if ($this->level === 0) {
60
            if ($isolationLevel !== null) {
61
                $this->setTransactionIsolationLevel($isolationLevel);
62
            }
63
64
            $this->logger?->log(
65
                LogLevel::DEBUG,
66
                'Begin transaction' . ($isolationLevel ? ' with isolation level ' . $isolationLevel : '')
67
                . ' ' . __METHOD__
68
            );
69
70
            $this->db->getPDO()?->beginTransaction();
71
            $this->level = 1;
72
73
            return;
74
        }
75
76
        if ($this->db->isSavepointEnabled()) {
77
            $this->logger?->log(LogLevel::DEBUG, 'Set savepoint ' . $this->level . ' ' . __METHOD__);
78
79
            $this->createSavepoint('LEVEL' . $this->level);
80
        } else {
81
            $this->logger?->log(
82
                LogLevel::DEBUG,
83
                'Transaction not started: nested transaction not supported ' . __METHOD__
84
            );
85
86
            throw new NotSupportedException('Transaction not started: nested transaction not supported.');
87
        }
88
89
        $this->level++;
90
    }
91
92
    /**
93
     * @inheritDoc
94
     */
95
    public function commit(): void
96
    {
97
        if (!$this->isActive()) {
98
            throw new Exception('Failed to commit transaction: transaction was inactive.');
99
        }
100
101
        $this->level--;
102
        if ($this->level === 0) {
103
            $this->logger?->log(LogLevel::DEBUG, 'Commit transaction ' . __METHOD__);
104
            $this->db->getPDO()?->commit();
105
106
            return;
107
        }
108
109
        if ($this->db->isSavepointEnabled()) {
110
            $this->logger?->log(LogLevel::DEBUG, 'Release savepoint ' . $this->level . ' ' . __METHOD__);
111
            $this->releaseSavepoint('LEVEL' . $this->level);
112
        } else {
113
            $this->logger?->log(
114
                LogLevel::INFO,
115
                'Transaction not committed: nested transaction not supported ' . __METHOD__
116
            );
117
        }
118
    }
119
120
    /**
121
     * @inheritDoc
122
     */
123
    public function getLevel(): int
124
    {
125
        return $this->level;
126
    }
127
128
    /**
129
     * @inheritDoc
130
     */
131
    public function isActive(): bool
132
    {
133
        return $this->level > 0 && $this->db->isActive();
134
    }
135
136
    /**
137
     * @inheritDoc
138
     *
139
     * @throws Exception|InvalidConfigException|Throwable
140
     */
141
    public function rollBack(): void
142
    {
143
        if (!$this->isActive()) {
144
            /**
145
             * do nothing if transaction is not active: this could be the transaction is committed but the event handler
146
             * to "commitTransaction" throw an exception
147
             */
148
            return;
149
        }
150
151
        $this->level--;
152
        if ($this->level === 0) {
153
            $this->logger?->log(LogLevel::INFO, 'Roll back transaction ' . __METHOD__);
154
            $this->db->getPDO()?->rollBack();
155
156
            return;
157
        }
158
159
        if ($this->db->isSavepointEnabled()) {
160
            $this->logger?->log(LogLevel::DEBUG, 'Roll back to savepoint ' . $this->level . ' ' . __METHOD__);
161
            $this->rollBackSavepoint('LEVEL' . $this->level);
162
        } else {
163
            $this->logger?->log(
164
                LogLevel::INFO,
165
                'Transaction not rolled back: nested transaction not supported ' . __METHOD__
166
            );
167
        }
168
    }
169
170
    /**
171
     * @inheritDoc
172
     */
173
    public function setIsolationLevel(string $level): void
174
    {
175
        if (!$this->isActive()) {
176
            throw new Exception('Failed to set isolation level: transaction was inactive.');
177
        }
178
179
        $this->logger?->log(
180
            LogLevel::DEBUG,
181
            'Setting transaction isolation level to ' . $this->level . ' ' . __METHOD__
182
        );
183
        $this->setTransactionIsolationLevel($level);
184
    }
185
186
    /**
187
     * @throws Exception|InvalidConfigException|Throwable
188
     */
189
    protected function setTransactionIsolationLevel(string $level): void
190
    {
191
        $this->db->createCommand("SET TRANSACTION ISOLATION LEVEL $level")->execute();
192
    }
193
194
    /**
195
     * Creates a new savepoint.
196
     *
197
     * @param string $name the savepoint name
198
     *
199
     * @throws Exception|InvalidConfigException|Throwable
200
     */
201
    public function createSavepoint(string $name): void
202
    {
203
        $this->db->createCommand("SAVEPOINT $name")->execute();
204
    }
205
206
    public function rollBackSavepoint(string $name): void
207
    {
208
        $this->db->createCommand("ROLLBACK TO SAVEPOINT $name")->execute();
209
    }
210
211
    /**
212
     * @inheritDoc
213
     */
214
    public function releaseSavepoint(string $name): void
215
    {
216
        $this->db->createCommand("RELEASE SAVEPOINT $name")->execute();
217
    }
218
}
219