|
1
|
|
|
<?php |
|
2
|
|
|
/* |
|
3
|
|
|
* This file is part of the PommProject/ModelManager package. |
|
4
|
|
|
* |
|
5
|
|
|
* (c) 2014 - 2015 Grégoire HUBERT <[email protected]> |
|
6
|
|
|
* |
|
7
|
|
|
* For the full copyright and license information, please view the LICENSE |
|
8
|
|
|
* file that was distributed with this source code. |
|
9
|
|
|
*/ |
|
10
|
|
|
namespace PommProject\ModelManager\ModelLayer; |
|
11
|
|
|
|
|
12
|
|
|
use PommProject\Foundation\Client\Client; |
|
13
|
|
|
use PommProject\Foundation\Client\ClientInterface; |
|
14
|
|
|
use PommProject\Foundation\Session\Connection; |
|
15
|
|
|
use PommProject\Foundation\Session\ResultHandler; |
|
16
|
|
|
use PommProject\ModelManager\Exception\ModelLayerException; |
|
17
|
|
|
use PommProject\ModelManager\Model\Model; |
|
18
|
|
|
|
|
19
|
|
|
/** |
|
20
|
|
|
* ModelLayer |
|
21
|
|
|
* |
|
22
|
|
|
* ModelLayer handles mechanisms around model method calls (transactions, |
|
23
|
|
|
* events etc.). |
|
24
|
|
|
* |
|
25
|
|
|
* @package ModelManager |
|
26
|
|
|
* @copyright 2014 - 2015 Grégoire HUBERT |
|
27
|
|
|
* @author Grégoire HUBERT |
|
28
|
|
|
* @license X11 {@link http://opensource.org/licenses/mit-license.php} |
|
29
|
|
|
* @see Client |
|
30
|
|
|
*/ |
|
31
|
|
|
abstract class ModelLayer extends Client |
|
32
|
|
|
{ |
|
33
|
|
|
/** |
|
34
|
|
|
* getClientType |
|
35
|
|
|
* |
|
36
|
|
|
* @see ClientInterface |
|
37
|
|
|
*/ |
|
38
|
|
|
public function getClientType() |
|
39
|
|
|
{ |
|
40
|
|
|
return 'model_layer'; |
|
41
|
|
|
} |
|
42
|
|
|
|
|
43
|
|
|
/** |
|
44
|
|
|
* getClientIdentifier |
|
45
|
|
|
* |
|
46
|
|
|
* @see ClientInterface |
|
47
|
|
|
*/ |
|
48
|
|
|
public function getClientIdentifier() |
|
49
|
|
|
{ |
|
50
|
|
|
return get_class($this); |
|
51
|
|
|
} |
|
52
|
|
|
|
|
53
|
|
|
/** |
|
54
|
|
|
* shutdown |
|
55
|
|
|
* |
|
56
|
|
|
* @see ClientInterface |
|
57
|
|
|
*/ |
|
58
|
|
|
public function shutdown() |
|
59
|
|
|
{ |
|
60
|
|
|
} |
|
61
|
|
|
|
|
62
|
|
|
/** |
|
63
|
|
|
* startTransaction |
|
64
|
|
|
* |
|
65
|
|
|
* Start a new transaction. |
|
66
|
|
|
* |
|
67
|
|
|
* @access protected |
|
68
|
|
|
* @return ModelLayer $this |
|
69
|
|
|
*/ |
|
70
|
|
|
protected function startTransaction() |
|
71
|
|
|
{ |
|
72
|
|
|
$this->executeAnonymousQuery('begin transaction'); |
|
73
|
|
|
|
|
74
|
|
|
return $this; |
|
75
|
|
|
} |
|
76
|
|
|
|
|
77
|
|
|
/** |
|
78
|
|
|
* setDeferrable |
|
79
|
|
|
* |
|
80
|
|
|
* Set given constraints to deferred/immediate in the current transaction. |
|
81
|
|
|
* This applies to constraints being deferrable or deferred by default. |
|
82
|
|
|
* If the keys is an empty arrays, ALL keys will be set at the given state. |
|
83
|
|
|
* @see http://www.postgresql.org/docs/9.0/static/sql-set-constraints.html |
|
84
|
|
|
* |
|
85
|
|
|
* @access protected |
|
86
|
|
|
* @param array $keys |
|
87
|
|
|
* @param string $state |
|
88
|
|
|
* @throws ModelLayerException if not valid state |
|
89
|
|
|
* @return ModelLayer $this |
|
90
|
|
|
*/ |
|
91
|
|
|
protected function setDeferrable(array $keys, $state) |
|
92
|
|
|
{ |
|
93
|
|
|
if (count($keys) === 0) { |
|
94
|
|
|
$string = 'ALL'; |
|
95
|
|
|
} else { |
|
96
|
|
|
$string = join( |
|
97
|
|
|
', ', |
|
98
|
|
|
array_map( |
|
99
|
|
|
function ($key) { |
|
100
|
|
|
$parts = explode('.', $key); |
|
101
|
|
|
$escaped_parts = []; |
|
102
|
|
|
|
|
103
|
|
|
foreach ($parts as $part) { |
|
104
|
|
|
$escaped_parts[] = $this->escapeIdentifier($part); |
|
105
|
|
|
} |
|
106
|
|
|
|
|
107
|
|
|
return join('.', $escaped_parts); |
|
108
|
|
|
}, |
|
109
|
|
|
$keys |
|
110
|
|
|
) |
|
111
|
|
|
); |
|
112
|
|
|
} |
|
113
|
|
|
|
|
114
|
|
|
if (!in_array($state, [ Connection::CONSTRAINTS_DEFERRED, Connection::CONSTRAINTS_IMMEDIATE ])) { |
|
115
|
|
|
throw new ModelLayerException( |
|
116
|
|
|
sprintf(<<<EOMSG |
|
117
|
|
|
'%s' is not a valid constraint modifier. |
|
118
|
|
|
Use Connection::CONSTRAINTS_DEFERRED or Connection::CONSTRAINTS_IMMEDIATE. |
|
119
|
|
|
EOMSG |
|
120
|
|
|
, |
|
121
|
|
|
$state |
|
122
|
|
|
) |
|
123
|
|
|
); |
|
124
|
|
|
} |
|
125
|
|
|
|
|
126
|
|
|
$this->executeAnonymousQuery( |
|
127
|
|
|
sprintf( |
|
128
|
|
|
"set constraints %s %s", |
|
129
|
|
|
$string, |
|
130
|
|
|
$state |
|
131
|
|
|
) |
|
132
|
|
|
); |
|
133
|
|
|
|
|
134
|
|
|
return $this; |
|
135
|
|
|
} |
|
136
|
|
|
|
|
137
|
|
|
/** |
|
138
|
|
|
* setTransactionIsolationLevel |
|
139
|
|
|
* |
|
140
|
|
|
* Transaction isolation level tells PostgreSQL how to manage with the |
|
141
|
|
|
* current transaction. The default is "READ COMMITTED". |
|
142
|
|
|
* @see http://www.postgresql.org/docs/9.0/static/sql-set-transaction.html |
|
143
|
|
|
* |
|
144
|
|
|
* @access protected |
|
145
|
|
|
* @param string $isolation_level |
|
146
|
|
|
* @throws ModelLayerException if not valid isolation level |
|
147
|
|
|
* @return ModelLayer $this |
|
148
|
|
|
*/ |
|
149
|
|
View Code Duplication |
protected function setTransactionIsolationLevel($isolation_level) |
|
|
|
|
|
|
150
|
|
|
{ |
|
151
|
|
|
$valid_isolation_levels = |
|
152
|
|
|
[ |
|
153
|
|
|
Connection::ISOLATION_READ_COMMITTED, |
|
154
|
|
|
Connection::ISOLATION_REPEATABLE_READ, |
|
155
|
|
|
Connection::ISOLATION_SERIALIZABLE |
|
156
|
|
|
]; |
|
157
|
|
|
|
|
158
|
|
|
if (!in_array( |
|
159
|
|
|
$isolation_level, |
|
160
|
|
|
$valid_isolation_levels |
|
161
|
|
|
)) { |
|
162
|
|
|
throw new ModelLayerException( |
|
163
|
|
|
sprintf( |
|
164
|
|
|
"'%s' is not a valid transaction isolation level. Valid isolation levels are {%s} see Connection class constants.", |
|
165
|
|
|
$isolation_level, |
|
166
|
|
|
join(', ', $valid_isolation_levels) |
|
167
|
|
|
) |
|
168
|
|
|
); |
|
169
|
|
|
} |
|
170
|
|
|
|
|
171
|
|
|
return $this->sendParameter( |
|
172
|
|
|
"set transaction isolation level %s", |
|
173
|
|
|
$isolation_level |
|
174
|
|
|
); |
|
175
|
|
|
} |
|
176
|
|
|
|
|
177
|
|
|
/** |
|
178
|
|
|
* setTransactionAccessMode |
|
179
|
|
|
* |
|
180
|
|
|
* Transaction access modes tell PostgreSQL if transaction are able to |
|
181
|
|
|
* write or read only. |
|
182
|
|
|
* @see http://www.postgresql.org/docs/9.0/static/sql-set-transaction.html |
|
183
|
|
|
* |
|
184
|
|
|
* @access protected |
|
185
|
|
|
* @param string $access_mode |
|
186
|
|
|
* @throws ModelLayerException if not valid access mode |
|
187
|
|
|
* @return ModelLayer $this |
|
188
|
|
|
*/ |
|
189
|
|
View Code Duplication |
protected function setTransactionAccessMode($access_mode) |
|
|
|
|
|
|
190
|
|
|
{ |
|
191
|
|
|
$valid_access_modes = |
|
192
|
|
|
[ |
|
193
|
|
|
Connection::ACCESS_MODE_READ_ONLY, |
|
194
|
|
|
Connection::ACCESS_MODE_READ_WRITE |
|
195
|
|
|
]; |
|
196
|
|
|
|
|
197
|
|
|
if (!in_array( |
|
198
|
|
|
$access_mode, |
|
199
|
|
|
$valid_access_modes |
|
200
|
|
|
)) { |
|
201
|
|
|
throw new ModelLayerException( |
|
202
|
|
|
sprintf( |
|
203
|
|
|
"'%s' is not a valid transaction access mode. Valid access modes are {%s}, see Connection class constants.", |
|
204
|
|
|
$access_mode, |
|
205
|
|
|
join(', ', $valid_access_modes) |
|
206
|
|
|
) |
|
207
|
|
|
); |
|
208
|
|
|
} |
|
209
|
|
|
|
|
210
|
|
|
return $this->sendParameter( |
|
211
|
|
|
"set transaction %s", |
|
212
|
|
|
$access_mode |
|
213
|
|
|
); |
|
214
|
|
|
} |
|
215
|
|
|
|
|
216
|
|
|
/** |
|
217
|
|
|
* setSavePoint |
|
218
|
|
|
* |
|
219
|
|
|
* Set a savepoint in a transaction. |
|
220
|
|
|
* |
|
221
|
|
|
* @access protected |
|
222
|
|
|
* @param string $name |
|
223
|
|
|
* @return ModelLayer $this |
|
224
|
|
|
*/ |
|
225
|
|
|
protected function setSavepoint($name) |
|
226
|
|
|
{ |
|
227
|
|
|
return $this->sendParameter( |
|
228
|
|
|
"savepoint %s", |
|
229
|
|
|
$this->escapeIdentifier($name) |
|
230
|
|
|
); |
|
231
|
|
|
} |
|
232
|
|
|
|
|
233
|
|
|
/** |
|
234
|
|
|
* releaseSavepoint |
|
235
|
|
|
* |
|
236
|
|
|
* Drop a savepoint. |
|
237
|
|
|
* |
|
238
|
|
|
* @access protected |
|
239
|
|
|
* @param string $name |
|
240
|
|
|
* @return ModelLayer $this |
|
241
|
|
|
*/ |
|
242
|
|
|
protected function releaseSavepoint($name) |
|
243
|
|
|
{ |
|
244
|
|
|
return $this->sendParameter( |
|
245
|
|
|
"release savepoint %s", |
|
246
|
|
|
$this->escapeIdentifier($name) |
|
247
|
|
|
); |
|
248
|
|
|
} |
|
249
|
|
|
|
|
250
|
|
|
/** |
|
251
|
|
|
* rollbackTransaction |
|
252
|
|
|
* |
|
253
|
|
|
* Rollback a transaction. If a name is specified, the transaction is |
|
254
|
|
|
* rollback to the given savepoint. Otherwise, the whole transaction is |
|
255
|
|
|
* rollback. |
|
256
|
|
|
* |
|
257
|
|
|
* @access protected |
|
258
|
|
|
* @param string|null $name |
|
259
|
|
|
* @return ModelLayer $this |
|
260
|
|
|
*/ |
|
261
|
|
|
protected function rollbackTransaction($name = null) |
|
262
|
|
|
{ |
|
263
|
|
|
$sql = "rollback transaction"; |
|
264
|
|
|
if ($name !== null) { |
|
265
|
|
|
$sql = sprintf("rollback to savepoint %s", $this->escapeIdentifier($name)); |
|
266
|
|
|
} |
|
267
|
|
|
|
|
268
|
|
|
$this->executeAnonymousQuery($sql); |
|
269
|
|
|
|
|
270
|
|
|
return $this; |
|
271
|
|
|
} |
|
272
|
|
|
|
|
273
|
|
|
/** |
|
274
|
|
|
* commitTransaction |
|
275
|
|
|
* |
|
276
|
|
|
* Commit a transaction. |
|
277
|
|
|
* |
|
278
|
|
|
* @access protected |
|
279
|
|
|
* @return ModelLayer $this |
|
280
|
|
|
*/ |
|
281
|
|
|
protected function commitTransaction() |
|
282
|
|
|
{ |
|
283
|
|
|
$this->executeAnonymousQuery('commit transaction'); |
|
284
|
|
|
|
|
285
|
|
|
return $this; |
|
286
|
|
|
} |
|
287
|
|
|
|
|
288
|
|
|
/** |
|
289
|
|
|
* isInTransaction |
|
290
|
|
|
* |
|
291
|
|
|
* Tell if a transaction is open or not. |
|
292
|
|
|
* |
|
293
|
|
|
* @see Cient |
|
294
|
|
|
* @access protected |
|
295
|
|
|
* @return bool |
|
296
|
|
|
*/ |
|
297
|
|
|
protected function isInTransaction() |
|
298
|
|
|
{ |
|
299
|
|
|
$status = $this |
|
300
|
|
|
->getSession() |
|
301
|
|
|
->getConnection() |
|
302
|
|
|
->getTransactionStatus() |
|
303
|
|
|
; |
|
304
|
|
|
|
|
305
|
|
|
return (bool) ($status === \PGSQL_TRANSACTION_INTRANS || $status === \PGSQL_TRANSACTION_INERROR || $status === \PGSQL_TRANSACTION_ACTIVE); |
|
306
|
|
|
} |
|
307
|
|
|
|
|
308
|
|
|
/** |
|
309
|
|
|
* isTransactionOk |
|
310
|
|
|
* |
|
311
|
|
|
* In PostgreSQL, an error during a transaction cancels all the queries and |
|
312
|
|
|
* rollback the transaction on commit. This method returns the current |
|
313
|
|
|
* transaction's status. If no transactions are open, it returns null. |
|
314
|
|
|
* |
|
315
|
|
|
* @access public |
|
316
|
|
|
* @return bool|null |
|
317
|
|
|
*/ |
|
318
|
|
|
protected function isTransactionOk() |
|
319
|
|
|
{ |
|
320
|
|
|
if (!$this->isInTransaction()) { |
|
321
|
|
|
return null; |
|
322
|
|
|
} |
|
323
|
|
|
|
|
324
|
|
|
$status = $this |
|
325
|
|
|
->getSession() |
|
326
|
|
|
->getConnection() |
|
327
|
|
|
->getTransactionStatus() |
|
328
|
|
|
; |
|
329
|
|
|
|
|
330
|
|
|
return (bool) ($status === \PGSQL_TRANSACTION_INTRANS); |
|
331
|
|
|
} |
|
332
|
|
|
|
|
333
|
|
|
/** |
|
334
|
|
|
* sendNotify |
|
335
|
|
|
* |
|
336
|
|
|
* Send a NOTIFY event to the database server. An optional data can be sent |
|
337
|
|
|
* with the notification. |
|
338
|
|
|
* |
|
339
|
|
|
* @access protected |
|
340
|
|
|
* @param string $channel |
|
341
|
|
|
* @param string $data |
|
342
|
|
|
* @return ModelLayer $this |
|
343
|
|
|
*/ |
|
344
|
|
|
protected function sendNotify($channel, $data = '') |
|
345
|
|
|
{ |
|
346
|
|
|
return $this->sendParameter( |
|
347
|
|
|
'notify %s, %s', |
|
348
|
|
|
$channel, |
|
349
|
|
|
$this->escapeLiteral($data) |
|
350
|
|
|
); |
|
351
|
|
|
} |
|
352
|
|
|
|
|
353
|
|
|
/** |
|
354
|
|
|
* executeAnonymousQuery |
|
355
|
|
|
* |
|
356
|
|
|
* Proxy to Connection::executeAnonymousQuery() |
|
357
|
|
|
* |
|
358
|
|
|
* @access protected |
|
359
|
|
|
* @param string $sql |
|
360
|
|
|
* @return ResultHandler |
|
361
|
|
|
*/ |
|
362
|
|
|
protected function executeAnonymousQuery($sql) |
|
363
|
|
|
{ |
|
364
|
|
|
return $this |
|
|
|
|
|
|
365
|
|
|
->getSession() |
|
366
|
|
|
->getConnection() |
|
367
|
|
|
->executeAnonymousQuery($sql) |
|
368
|
|
|
; |
|
369
|
|
|
} |
|
370
|
|
|
|
|
371
|
|
|
/** |
|
372
|
|
|
* escapeIdentifier |
|
373
|
|
|
* |
|
374
|
|
|
* Proxy to Connection::escapeIdentifier() |
|
375
|
|
|
* |
|
376
|
|
|
* @access protected |
|
377
|
|
|
* @param string $string |
|
378
|
|
|
* @return string |
|
379
|
|
|
*/ |
|
380
|
|
|
protected function escapeIdentifier($string) |
|
381
|
|
|
{ |
|
382
|
|
|
return $this |
|
383
|
|
|
->getSession() |
|
384
|
|
|
->getConnection() |
|
385
|
|
|
->escapeIdentifier($string) |
|
386
|
|
|
; |
|
387
|
|
|
} |
|
388
|
|
|
|
|
389
|
|
|
/** |
|
390
|
|
|
* escapeLiteral |
|
391
|
|
|
* |
|
392
|
|
|
* Proxy to Connection::escapeLiteral() |
|
393
|
|
|
* |
|
394
|
|
|
* @access protected |
|
395
|
|
|
* @param string $string |
|
396
|
|
|
* @return string |
|
397
|
|
|
*/ |
|
398
|
|
|
protected function escapeLiteral($string) |
|
399
|
|
|
{ |
|
400
|
|
|
return $this |
|
401
|
|
|
->getSession() |
|
402
|
|
|
->getConnection() |
|
403
|
|
|
->escapeLiteral($string) |
|
404
|
|
|
; |
|
405
|
|
|
} |
|
406
|
|
|
|
|
407
|
|
|
/** |
|
408
|
|
|
* getModel |
|
409
|
|
|
* |
|
410
|
|
|
* Proxy to Session::getModel(); |
|
411
|
|
|
* |
|
412
|
|
|
* @access protected |
|
413
|
|
|
* @param string model identifier |
|
414
|
|
|
* @return Model |
|
415
|
|
|
*/ |
|
416
|
|
|
protected function getModel($identifier) |
|
417
|
|
|
{ |
|
418
|
|
|
return $this |
|
419
|
|
|
->getSession() |
|
420
|
|
|
->getClientUsingPooler('model', $identifier); |
|
421
|
|
|
} |
|
422
|
|
|
|
|
423
|
|
|
/** |
|
424
|
|
|
* sendParameter |
|
425
|
|
|
* |
|
426
|
|
|
* Send a parameter to the server. |
|
427
|
|
|
* The parameter MUST have been properly checked and escaped if needed as |
|
428
|
|
|
* it is going to be passed AS IS to the server. Sending untrusted |
|
429
|
|
|
* parameters may lead to potential SQL injection. |
|
430
|
|
|
* |
|
431
|
|
|
* @access private |
|
432
|
|
|
* @param string $sql |
|
433
|
|
|
* @param string $identifier |
|
434
|
|
|
* @param string $parameter |
|
435
|
|
|
* @return ModelLayer $this |
|
436
|
|
|
*/ |
|
437
|
|
|
private function sendParameter($sql, $identifier, $parameter = null) |
|
438
|
|
|
{ |
|
439
|
|
|
$this |
|
440
|
|
|
->executeAnonymousQuery( |
|
441
|
|
|
sprintf( |
|
442
|
|
|
$sql, |
|
443
|
|
|
$identifier, |
|
444
|
|
|
$parameter |
|
445
|
|
|
) |
|
446
|
|
|
); |
|
447
|
|
|
|
|
448
|
|
|
return $this; |
|
449
|
|
|
} |
|
450
|
|
|
} |
|
451
|
|
|
|
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.