1
|
|
|
<?php |
2
|
|
|
declare(strict_types=1); |
3
|
|
|
|
4
|
|
|
namespace Ivory\Connection; |
5
|
|
|
|
6
|
|
|
use Ivory\Connection\Config\ConfigParam; |
7
|
|
|
use Ivory\Exception\InvalidStateException; |
8
|
|
|
use Ivory\Ivory; |
9
|
|
|
use Ivory\Result\IQueryResult; |
10
|
|
|
use Ivory\Type\Std\StringType; |
11
|
|
|
|
12
|
|
|
class TransactionControl implements IObservableTransactionControl |
13
|
|
|
{ |
14
|
|
|
private $connCtl; |
15
|
|
|
private $stmtExec; |
16
|
|
|
private $sessionCtl; |
17
|
|
|
/** @var ITransactionControlObserver[] */ |
18
|
|
|
private $observers = []; |
19
|
|
|
|
20
|
|
|
|
21
|
|
|
public function __construct(ConnectionControl $connCtl, IStatementExecution $stmtExec, ISessionControl $sessionCtl) |
22
|
|
|
{ |
23
|
|
|
$this->connCtl = $connCtl; |
24
|
|
|
$this->stmtExec = $stmtExec; |
25
|
|
|
$this->sessionCtl = $sessionCtl; |
26
|
|
|
} |
27
|
|
|
|
28
|
|
|
public function inTransaction(): bool |
29
|
|
|
{ |
30
|
|
|
$connHandler = $this->connCtl->requireConnection(); |
31
|
|
|
$txStat = pg_transaction_status($connHandler); |
32
|
|
|
return ($txStat == PGSQL_TRANSACTION_INTRANS || $txStat == PGSQL_TRANSACTION_INERROR); |
33
|
|
|
} |
34
|
|
|
|
35
|
|
|
public function startTransaction($transactionOptions = 0): ITxHandle |
36
|
|
|
{ |
37
|
|
|
if ($this->inTransaction()) { |
38
|
|
|
throw new InvalidStateException('A transaction is already active, cannot start a new one.'); |
39
|
|
|
} |
40
|
|
|
|
41
|
|
|
$txConfig = TxConfig::create($transactionOptions); |
42
|
|
|
$txSql = $txConfig->toSql(); |
43
|
|
|
|
44
|
|
|
$command = 'START TRANSACTION'; |
45
|
|
|
if (strlen($txSql) > 0) { |
46
|
|
|
$command .= ' ' . $txSql; |
47
|
|
|
} |
48
|
|
|
|
49
|
|
|
$this->stmtExec->rawCommand($command); |
50
|
|
|
$this->notifyTransactionStart(); |
51
|
|
|
|
52
|
|
|
$coreFactory = Ivory::getCoreFactory(); |
53
|
|
|
return $coreFactory->createTransactionHandle($this->stmtExec, $this, $this->sessionCtl); |
54
|
|
|
} |
55
|
|
|
|
56
|
|
|
public function setupSubsequentTransactions($transactionOptions): void |
57
|
|
|
{ |
58
|
|
|
$txConfig = TxConfig::create($transactionOptions); |
59
|
|
|
$this->stmtExec->rawCommand('SET SESSION CHARACTERISTICS AS TRANSACTION ' . $txConfig->toSql()); |
60
|
|
|
} |
61
|
|
|
|
62
|
|
|
public function getDefaultTxConfig(): TxConfig |
63
|
|
|
{ |
64
|
|
|
$connConfig = $this->sessionCtl->getConfig(); |
65
|
|
|
return TxConfig::createFromParams( |
66
|
|
|
$connConfig->get(ConfigParam::DEFAULT_TRANSACTION_ISOLATION), |
67
|
|
|
$connConfig->get(ConfigParam::DEFAULT_TRANSACTION_READ_ONLY), |
68
|
|
|
$connConfig->get(ConfigParam::DEFAULT_TRANSACTION_DEFERRABLE) |
69
|
|
|
); |
70
|
|
|
} |
71
|
|
|
|
72
|
|
View Code Duplication |
public function commitPreparedTransaction(string $name): void |
|
|
|
|
73
|
|
|
{ |
74
|
|
|
if ($this->inTransaction()) { |
75
|
|
|
throw new InvalidStateException('Cannot commit a prepared transaction while inside another transaction.'); |
76
|
|
|
} |
77
|
|
|
|
78
|
|
|
$this->stmtExec->rawCommand("COMMIT PREPARED {$this->quoteString($name)}"); |
79
|
|
|
$this->notifyPreparedTransactionCommit($name); |
80
|
|
|
} |
81
|
|
|
|
82
|
|
View Code Duplication |
public function rollbackPreparedTransaction(string $name): void |
|
|
|
|
83
|
|
|
{ |
84
|
|
|
if ($this->inTransaction()) { |
85
|
|
|
throw new InvalidStateException('Cannot rollback a prepared transaction while inside another transaction.'); |
86
|
|
|
} |
87
|
|
|
|
88
|
|
|
$this->stmtExec->rawCommand("ROLLBACK PREPARED {$this->quoteString($name)}"); |
89
|
|
|
$this->notifyPreparedTransactionRollback($name); |
90
|
|
|
} |
91
|
|
|
|
92
|
|
|
public function listPreparedTransactions(): IQueryResult |
93
|
|
|
{ |
94
|
|
|
return $this->stmtExec->rawQuery('SELECT * FROM pg_catalog.pg_prepared_xacts'); |
95
|
|
|
} |
96
|
|
|
|
97
|
|
|
private function quoteString(string $str): string |
98
|
|
|
{ |
99
|
|
|
static $stringSerializer = null; |
100
|
|
|
if ($stringSerializer === null) { |
101
|
|
|
$stringSerializer = new StringType('pg_catalog', 'text'); |
102
|
|
|
} |
103
|
|
|
return $stringSerializer->serializeValue($str); |
104
|
|
|
} |
105
|
|
|
|
106
|
|
|
|
107
|
|
|
//region IObservableTransactionControl |
108
|
|
|
|
109
|
|
|
public function addObserver(ITransactionControlObserver $observer): void |
110
|
|
|
{ |
111
|
|
|
$hash = spl_object_hash($observer); |
112
|
|
|
$this->observers[$hash] = $observer; |
113
|
|
|
} |
114
|
|
|
|
115
|
|
|
public function removeObserver(ITransactionControlObserver $observer): void |
116
|
|
|
{ |
117
|
|
|
$hash = spl_object_hash($observer); |
118
|
|
|
unset($this->observers[$hash]); |
119
|
|
|
} |
120
|
|
|
|
121
|
|
|
public function removeAllObservers(): void |
122
|
|
|
{ |
123
|
|
|
$this->observers = []; |
124
|
|
|
} |
125
|
|
|
|
126
|
|
|
public function notifyTransactionStart(): void |
127
|
|
|
{ |
128
|
|
|
foreach ($this->observers as $observer) { |
129
|
|
|
$observer->handleTransactionStart(); |
130
|
|
|
} |
131
|
|
|
} |
132
|
|
|
|
133
|
|
|
public function notifyTransactionCommit(): void |
134
|
|
|
{ |
135
|
|
|
foreach ($this->observers as $observer) { |
136
|
|
|
$observer->handleTransactionCommit(); |
137
|
|
|
} |
138
|
|
|
} |
139
|
|
|
|
140
|
|
|
public function notifyTransactionRollback(): void |
141
|
|
|
{ |
142
|
|
|
foreach ($this->observers as $observer) { |
143
|
|
|
$observer->handleTransactionRollback(); |
144
|
|
|
} |
145
|
|
|
} |
146
|
|
|
|
147
|
|
|
public function notifySavepointSaved(string $name): void |
148
|
|
|
{ |
149
|
|
|
foreach ($this->observers as $observer) { |
150
|
|
|
$observer->handleSavepointSaved($name); |
151
|
|
|
} |
152
|
|
|
} |
153
|
|
|
|
154
|
|
|
public function notifySavepointReleased(string $name): void |
155
|
|
|
{ |
156
|
|
|
foreach ($this->observers as $observer) { |
157
|
|
|
$observer->handleSavepointReleased($name); |
158
|
|
|
} |
159
|
|
|
} |
160
|
|
|
|
161
|
|
|
public function notifyRollbackToSavepoint(string $name): void |
162
|
|
|
{ |
163
|
|
|
foreach ($this->observers as $observer) { |
164
|
|
|
$observer->handleRollbackToSavepoint($name); |
165
|
|
|
} |
166
|
|
|
} |
167
|
|
|
|
168
|
|
|
public function notifyTransactionPrepared(string $name): void |
169
|
|
|
{ |
170
|
|
|
foreach ($this->observers as $observer) { |
171
|
|
|
$observer->handleTransactionPrepared($name); |
172
|
|
|
} |
173
|
|
|
} |
174
|
|
|
|
175
|
|
|
public function notifyPreparedTransactionCommit(string $name): void |
176
|
|
|
{ |
177
|
|
|
foreach ($this->observers as $observer) { |
178
|
|
|
$observer->handlePreparedTransactionCommit($name); |
179
|
|
|
} |
180
|
|
|
} |
181
|
|
|
|
182
|
|
|
public function notifyPreparedTransactionRollback(string $name): void |
183
|
|
|
{ |
184
|
|
|
foreach ($this->observers as $observer) { |
185
|
|
|
$observer->handlePreparedTransactionRollback($name); |
186
|
|
|
} |
187
|
|
|
} |
188
|
|
|
|
189
|
|
|
//endregion |
190
|
|
|
} |
191
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.