1
|
|
|
<?php |
2
|
|
|
declare(strict_types=1); |
3
|
|
|
|
4
|
|
|
namespace Ivory\Connection; |
5
|
|
|
|
6
|
|
|
use Ivory\Exception\InternalException; |
7
|
|
|
|
8
|
|
|
/** |
9
|
|
|
* Transaction configuration. To be used, e.g., for {@link Connection::startTransaction()}. |
10
|
|
|
* |
11
|
|
|
* There are several configurable parameters of transactions in PostgreSQL. For each of them, a group of constants, |
12
|
|
|
* representing the possible values for the respective parameter, is available. Constants from distinct groups may be |
13
|
|
|
* combined together using the binary `|` operator. |
14
|
|
|
*/ |
15
|
|
|
class TxConfig |
16
|
|
|
{ |
17
|
|
|
/** The `SERIALIZABLE` isolation level. */ |
18
|
|
|
const ISOLATION_SERIALIZABLE = 1; |
19
|
|
|
/** The `REPEATABLE READ` isolation level. */ |
20
|
|
|
const ISOLATION_REPEATABLE_READ = 2; |
21
|
|
|
/** The `READ COMMITTED` isolation level. */ |
22
|
|
|
const ISOLATION_READ_COMMITTED = 4; |
23
|
|
|
/** The `READ UNCOMMITTED` isolation level. */ |
24
|
|
|
const ISOLATION_READ_UNCOMMITTED = 8; |
25
|
|
|
|
26
|
|
|
/** The `READ WRITE` access mode. */ |
27
|
|
|
const ACCESS_READ_WRITE = 128; |
28
|
|
|
/** The `READ ONLY` access mode. */ |
29
|
|
|
const ACCESS_READ_ONLY = 256; |
30
|
|
|
|
31
|
|
|
/** The `DEFERRABLE` deferrable mode. Only effective in `SERIALIZABLE` `READ ONLY` transactions. */ |
32
|
|
|
const DEFERRABLE = 4096; |
33
|
|
|
/** The `NOT DEFERRABLE` deferrable mode. */ |
34
|
|
|
const NOT_DEFERRABLE = 8192; |
35
|
|
|
|
36
|
|
|
|
37
|
|
|
private $isolationLevel = null; |
38
|
|
|
private $readOnly = null; |
39
|
|
|
private $deferrable = null; |
40
|
|
|
|
41
|
|
|
|
42
|
|
|
public static function create($transactionOptions) |
43
|
|
|
{ |
44
|
|
|
if ($transactionOptions instanceof TxConfig) { |
45
|
|
|
return $transactionOptions; |
46
|
|
|
} else { |
47
|
|
|
return new TxConfig($transactionOptions); |
48
|
|
|
} |
49
|
|
|
} |
50
|
|
|
|
51
|
|
|
public static function createFromParams(?string $isolationLevel, ?bool $readOnly, ?bool $deferrable): TxConfig |
52
|
|
|
{ |
53
|
|
|
$txConf = new TxConfig(); |
54
|
|
|
$txConf->setReadOnly($readOnly); |
55
|
|
|
$txConf->setDeferrable($deferrable); |
56
|
|
|
|
57
|
|
|
if ($isolationLevel !== null) { |
58
|
|
|
static $isolationLevels = [ |
59
|
|
|
'serializable' => TxConfig::ISOLATION_SERIALIZABLE, |
60
|
|
|
'repeatable read' => TxConfig::ISOLATION_REPEATABLE_READ, |
61
|
|
|
'read committed' => TxConfig::ISOLATION_READ_COMMITTED, |
62
|
|
|
'read uncommitted' => TxConfig::ISOLATION_READ_UNCOMMITTED, |
63
|
|
|
]; |
64
|
|
|
if (!isset($isolationLevels[$isolationLevel])) { |
65
|
|
|
throw new \InvalidArgumentException("Unrecognized transaction isolation level: '$isolationLevel'"); |
66
|
|
|
} |
67
|
|
|
$txConf->setIsolationLevel($isolationLevels[$isolationLevel]); |
68
|
|
|
} |
69
|
|
|
|
70
|
|
|
return $txConf; |
71
|
|
|
} |
72
|
|
|
|
73
|
|
|
/** |
74
|
|
|
* @param int $options one or more {@link TxConfig} constants combined together with the <tt>|</tt> operator |
75
|
|
|
*/ |
76
|
|
|
public function __construct(int $options = 0) |
77
|
|
|
{ |
78
|
|
|
$isolationLevel = $options & ( |
79
|
|
|
self::ISOLATION_SERIALIZABLE | self::ISOLATION_REPEATABLE_READ | |
80
|
|
|
self::ISOLATION_READ_COMMITTED | self::ISOLATION_READ_UNCOMMITTED |
81
|
|
|
); |
82
|
|
|
if ($isolationLevel != 0) { |
83
|
|
|
$this->setIsolationLevel($isolationLevel); |
84
|
|
|
} |
85
|
|
|
|
86
|
|
|
$this->setReadOnly( |
87
|
|
|
self::initBool($options, self::ACCESS_READ_ONLY, self::ACCESS_READ_WRITE, 'read/write and read-only access mode') |
88
|
|
|
); |
89
|
|
|
$this->setDeferrable( |
90
|
|
|
self::initBool($options, self::DEFERRABLE, self::NOT_DEFERRABLE, 'deferrable and non-deferrable mode') |
91
|
|
|
); |
92
|
|
|
} |
93
|
|
|
|
94
|
|
|
private static function initBool(int $options, int $trueBit, int $falseBit, string $errorDesc): ?bool |
95
|
|
|
{ |
96
|
|
|
if ($options & $trueBit) { |
97
|
|
|
if ($options & $falseBit) { |
98
|
|
|
throw new \InvalidArgumentException(__CLASS__ . " options specify both $errorDesc"); |
99
|
|
|
} |
100
|
|
|
return true; |
101
|
|
|
} elseif ($options & $falseBit) { |
102
|
|
|
return false; |
103
|
|
|
} else { |
104
|
|
|
return null; |
105
|
|
|
} |
106
|
|
|
} |
107
|
|
|
|
108
|
|
|
/** |
109
|
|
|
* @return int|null one of the <tt>ISOLATION_*</tt> constants, or <tt>null</tt> if no isolation level is specified |
110
|
|
|
*/ |
111
|
|
|
public function getIsolationLevel(): ?int |
112
|
|
|
{ |
113
|
|
|
return $this->isolationLevel; |
114
|
|
|
} |
115
|
|
|
|
116
|
|
|
/** |
117
|
|
|
* @param int|null $isolationLevel one of the <tt>ISOLATION_*</tt> constants, or <tt>null</tt> to clear specifying |
118
|
|
|
* the isolation level |
119
|
|
|
* @return $this |
120
|
|
|
*/ |
121
|
|
|
public function setIsolationLevel(?int $isolationLevel) |
122
|
|
|
{ |
123
|
|
|
static $levels = [ |
124
|
|
|
null, |
125
|
|
|
self::ISOLATION_SERIALIZABLE, |
126
|
|
|
self::ISOLATION_REPEATABLE_READ, |
127
|
|
|
self::ISOLATION_READ_COMMITTED, |
128
|
|
|
self::ISOLATION_READ_UNCOMMITTED, |
129
|
|
|
]; |
130
|
|
|
if (!in_array($isolationLevel, $levels)) { |
131
|
|
|
throw new \InvalidArgumentException('Invalid isolation level specification'); |
132
|
|
|
} |
133
|
|
|
|
134
|
|
|
$this->isolationLevel = $isolationLevel; |
135
|
|
|
return $this; |
136
|
|
|
} |
137
|
|
|
|
138
|
|
|
/** |
139
|
|
|
* @return bool|null whether the access mode is specified as read-only, or <tt>null</tt> if no access mode is |
140
|
|
|
* specified |
141
|
|
|
*/ |
142
|
|
|
public function isReadOnly(): ?bool |
143
|
|
|
{ |
144
|
|
|
return $this->readOnly; |
145
|
|
|
} |
146
|
|
|
|
147
|
|
|
public function setReadOnly(?bool $readOnly): TxConfig |
148
|
|
|
{ |
149
|
|
|
$this->readOnly = $readOnly; |
150
|
|
|
return $this; |
151
|
|
|
} |
152
|
|
|
|
153
|
|
|
public function isDeferrable(): ?bool |
154
|
|
|
{ |
155
|
|
|
return $this->deferrable; |
156
|
|
|
} |
157
|
|
|
|
158
|
|
|
public function setDeferrable(?bool $deferrable): TxConfig |
159
|
|
|
{ |
160
|
|
|
$this->deferrable = $deferrable; |
161
|
|
|
return $this; |
162
|
|
|
} |
163
|
|
|
|
164
|
|
|
public function toSql(): string |
165
|
|
|
{ |
166
|
|
|
$clauses = []; |
167
|
|
|
|
168
|
|
|
$il = $this->getIsolationLevel(); |
169
|
|
|
if ($il !== null) { |
170
|
|
|
switch ($il) { |
171
|
|
|
case self::ISOLATION_SERIALIZABLE: |
|
|
|
|
172
|
|
|
$clauses[] = 'ISOLATION LEVEL SERIALIZABLE'; |
173
|
|
|
break; |
174
|
|
|
case self::ISOLATION_REPEATABLE_READ: |
|
|
|
|
175
|
|
|
$clauses[] = 'ISOLATION LEVEL REPEATABLE READ'; |
176
|
|
|
break; |
177
|
|
|
case self::ISOLATION_READ_COMMITTED: |
178
|
|
|
$clauses[] = 'ISOLATION LEVEL READ COMMITTED'; |
179
|
|
|
break; |
180
|
|
|
case self::ISOLATION_READ_UNCOMMITTED: |
181
|
|
|
$clauses[] = 'ISOLATION LEVEL READ UNCOMMITTED'; |
182
|
|
|
break; |
183
|
|
|
default: |
184
|
|
|
throw new InternalException('Undefined isolation level'); |
185
|
|
|
} |
186
|
|
|
} |
187
|
|
|
|
188
|
|
|
$ro = $this->isReadOnly(); |
189
|
|
|
if ($ro !== null) { |
190
|
|
|
$clauses[] = ($ro ? 'READ ONLY' : 'READ WRITE'); |
191
|
|
|
} |
192
|
|
|
|
193
|
|
|
$d = $this->isDeferrable(); |
194
|
|
|
if ($d !== null) { |
195
|
|
|
$clauses[] = ($d ? 'DEFERRABLE' : 'NOT DEFERRABLE'); |
196
|
|
|
} |
197
|
|
|
|
198
|
|
|
return implode(' ', $clauses); |
199
|
|
|
} |
200
|
|
|
} |
201
|
|
|
|
As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next
break
.There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.
To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.