1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
declare(strict_types=1); |
4
|
|
|
|
5
|
|
|
namespace EngineWorks\DBAL; |
6
|
|
|
|
7
|
|
|
use Countable; |
8
|
|
|
use EngineWorks\DBAL\Internal\ConvertObjectToStringMethod; |
9
|
|
|
use InvalidArgumentException; |
10
|
|
|
use IteratorAggregate; |
11
|
|
|
use LogicException; |
12
|
|
|
use Psr\Log\LoggerAwareInterface; |
13
|
|
|
use Psr\Log\LoggerInterface; |
14
|
|
|
use RuntimeException; |
15
|
|
|
|
16
|
|
|
/** |
17
|
|
|
* Recordset class |
18
|
|
|
* Hint: Use DBAL::queryRecordset() instead of using this class directly |
19
|
|
|
* @implements IteratorAggregate<int|string, array<string, mixed>> |
20
|
|
|
*/ |
21
|
|
|
class Recordset implements LoggerAwareInterface, IteratorAggregate, Countable |
22
|
|
|
{ |
23
|
|
|
use ConvertObjectToStringMethod; |
|
|
|
|
24
|
|
|
|
25
|
|
|
public const RSMODE_NOTCONNECTED = 0; |
26
|
|
|
|
27
|
|
|
public const RSMODE_CONNECTED_EDIT = 1; |
28
|
|
|
|
29
|
|
|
public const RSMODE_CONNECTED_ADDNEW = 2; |
30
|
|
|
|
31
|
|
|
/** |
32
|
|
|
* Associative array of the current record |
33
|
|
|
* @var array<string, scalar|null> |
34
|
|
|
*/ |
35
|
|
|
public $values; |
36
|
|
|
|
37
|
|
|
/** @var DBAL */ |
38
|
|
|
private $dbal; |
39
|
|
|
|
40
|
|
|
/** |
41
|
|
|
* Never use this property, use self::getLogger() instead because |
42
|
|
|
* when Logger is NULL it uses the DBAL::getLogger(). |
43
|
|
|
* |
44
|
|
|
* @var LoggerInterface|null |
45
|
|
|
*/ |
46
|
|
|
private $logger; |
47
|
|
|
|
48
|
|
|
/** |
49
|
|
|
* @var Result|null |
50
|
|
|
*/ |
51
|
|
|
private $result; |
52
|
|
|
|
53
|
|
|
/** |
54
|
|
|
* Array of original values |
55
|
|
|
* @var array<string, scalar|null>|null |
56
|
|
|
*/ |
57
|
|
|
private $originalValues; |
58
|
|
|
|
59
|
|
|
/** |
60
|
|
|
* source sql query |
61
|
|
|
* @var string |
62
|
|
|
*/ |
63
|
|
|
private $source; |
64
|
|
|
|
65
|
|
|
/** @var int */ |
66
|
|
|
private $mode; |
67
|
|
|
|
68
|
|
|
/** |
69
|
|
|
* Has the name of the current entity |
70
|
|
|
* @var string |
71
|
|
|
*/ |
72
|
|
|
private $entity; |
73
|
|
|
|
74
|
|
|
/** |
75
|
|
|
* This array is a local copy of $this->result->getFields() |
76
|
|
|
* @var array<string, array{name: string, table: string, commontype: string}> |
77
|
|
|
*/ |
78
|
|
|
private $datafields; |
79
|
|
|
|
80
|
|
|
/** |
81
|
|
|
* Storage of idFields, set after call query method |
82
|
|
|
* @var string[] |
83
|
|
|
*/ |
84
|
|
|
private $idFields; |
85
|
|
|
|
86
|
|
|
/** |
87
|
|
|
* Recordset constructor. |
88
|
|
|
* |
89
|
|
|
* @param DBAL $dbal |
90
|
|
|
* @param LoggerInterface|null $logger If not provided it uses the DBAL Logger |
91
|
|
|
*/ |
92
|
45 |
|
public function __construct(DBAL $dbal, LoggerInterface $logger = null) |
93
|
|
|
{ |
94
|
45 |
|
$this->dbal = $dbal; |
95
|
45 |
|
$this->logger = $logger; |
96
|
45 |
|
$this->initialize(); |
97
|
|
|
} |
98
|
|
|
|
99
|
|
|
/** |
100
|
|
|
* Return the current logger, uses the DBAL logger when local logger is not set |
101
|
|
|
* |
102
|
|
|
* @return LoggerInterface |
103
|
|
|
*/ |
104
|
3 |
|
public function getLogger(): LoggerInterface |
105
|
|
|
{ |
106
|
3 |
|
return $this->logger ?? $this->dbal->getLogger(); |
107
|
|
|
} |
108
|
|
|
|
109
|
|
|
/** |
110
|
|
|
* Define the current logger, if NULL uses the DBAL logger |
111
|
|
|
* |
112
|
|
|
* @param LoggerInterface|null $logger |
113
|
|
|
*/ |
114
|
1 |
|
public function setLogger(?LoggerInterface $logger): void |
115
|
|
|
{ |
116
|
1 |
|
$this->logger = $logger; |
117
|
|
|
} |
118
|
|
|
|
119
|
|
|
/** |
120
|
|
|
* Executes a SQL Query and connect the object with that query in order to operate the recordset |
121
|
|
|
* @param string $sql |
122
|
|
|
* @param string $overrideEntity |
123
|
|
|
* @param string[] $overrideKeys |
124
|
|
|
* @param string[] $overrideTypes |
125
|
|
|
* |
126
|
|
|
* @return true |
127
|
|
|
*/ |
128
|
43 |
|
final public function query( |
129
|
|
|
string $sql, |
130
|
|
|
string $overrideEntity = '', |
131
|
|
|
array $overrideKeys = [], |
132
|
|
|
array $overrideTypes = [] |
133
|
|
|
): bool { |
134
|
43 |
|
$this->initialize(); |
135
|
43 |
|
if (! $this->hasDBAL()) { |
136
|
1 |
|
throw new LogicException('Recordset: object does not have a connected DBAL'); |
137
|
|
|
} |
138
|
42 |
|
$result = $this->dbal->queryResult($sql, $overrideTypes); |
139
|
42 |
|
if (! $result instanceof Result) { |
140
|
12 |
|
throw new LogicException("Recordset: Unable to perform query $sql"); |
141
|
|
|
} |
142
|
30 |
|
$this->mode = self::RSMODE_CONNECTED_EDIT; |
143
|
30 |
|
$this->result = $result; |
144
|
30 |
|
$this->source = $sql; |
145
|
30 |
|
$this->datafields = []; |
146
|
|
|
// get fields into a temporary array |
147
|
|
|
/** @var array<array{name: string, table: string, commontype: string}> $tmpfields */ |
148
|
30 |
|
$tmpfields = $this->result()->getFields(); |
149
|
30 |
|
foreach ($tmpfields as $tmpfield) { |
150
|
30 |
|
$this->datafields[$tmpfield['name']] = $tmpfield; |
151
|
|
|
} |
152
|
|
|
// set the entity name, remove if more than one table exists |
153
|
30 |
|
if (count(array_unique(array_column($tmpfields, 'table'))) > 1) { |
154
|
|
|
$this->entity = ''; |
155
|
|
|
} else { |
156
|
30 |
|
$this->entity = (string) $tmpfields[0]['table']; |
157
|
|
|
} |
158
|
30 |
|
if ('' !== $overrideEntity) { |
159
|
8 |
|
$this->entity = $overrideEntity; |
160
|
|
|
} |
161
|
|
|
// set the id fields if did not override |
162
|
30 |
|
if ([] === $overrideKeys) { |
163
|
26 |
|
$this->idFields = $this->result()->getIdFields() ?: []; |
164
|
|
|
} else { |
165
|
|
|
// validate overrideKeys |
166
|
8 |
|
if ($overrideKeys !== array_unique($overrideKeys)) { |
167
|
|
|
throw new InvalidArgumentException('Keys contains repeated values'); |
168
|
|
|
} |
169
|
8 |
|
foreach ($overrideKeys as $fieldName) { |
170
|
8 |
|
if (! is_string($fieldName)) { |
171
|
|
|
throw new InvalidArgumentException('Keys were set but at least one is not a string'); |
172
|
|
|
} |
173
|
8 |
|
if (! array_key_exists($fieldName, $this->datafields)) { |
174
|
|
|
throw new InvalidArgumentException( |
175
|
|
|
"The field name $fieldName does not exists in the set of fields" |
176
|
|
|
); |
177
|
|
|
} |
178
|
|
|
} |
179
|
8 |
|
$this->idFields = $overrideKeys; |
180
|
|
|
} |
181
|
|
|
// if it has records then load first |
182
|
30 |
|
if ($this->getRecordCount() > 0) { |
183
|
18 |
|
$this->moveNext(); |
184
|
|
|
} |
185
|
30 |
|
return true; |
186
|
|
|
} |
187
|
|
|
|
188
|
|
|
/** |
189
|
|
|
* Internal procedure to initiate all the variables |
190
|
|
|
*/ |
191
|
45 |
|
private function initialize(): void |
192
|
|
|
{ |
193
|
45 |
|
$this->entity = ''; |
194
|
45 |
|
$this->source = ''; |
195
|
45 |
|
$this->mode = self::RSMODE_NOTCONNECTED; |
196
|
45 |
|
$this->result = null; |
197
|
45 |
|
$this->originalValues = null; |
198
|
45 |
|
$this->datafields = []; |
199
|
45 |
|
$this->values = []; |
200
|
45 |
|
$this->idFields = []; |
201
|
|
|
} |
202
|
|
|
|
203
|
|
|
/** |
204
|
|
|
* Return if the current DBAL and Result exists and are connected |
205
|
|
|
* @return bool |
206
|
|
|
*/ |
207
|
18 |
|
final public function isOpen(): bool |
208
|
|
|
{ |
209
|
18 |
|
return ($this->hasDBAL() && null !== $this->result); |
210
|
|
|
} |
211
|
|
|
|
212
|
|
|
/** |
213
|
|
|
* Check if the DBAL instance is connected (if not try to connect again) |
214
|
|
|
* @return bool |
215
|
|
|
*/ |
216
|
43 |
|
final public function hasDBAL(): bool |
217
|
|
|
{ |
218
|
43 |
|
return ($this->dbal->isConnected() || $this->dbal->connect()); |
219
|
|
|
} |
220
|
|
|
|
221
|
|
|
/** |
222
|
|
|
* Return the source query |
223
|
|
|
* @return string |
224
|
|
|
*/ |
225
|
4 |
|
final public function getSource(): string |
226
|
|
|
{ |
227
|
4 |
|
return $this->source; |
228
|
|
|
} |
229
|
|
|
|
230
|
|
|
/** |
231
|
|
|
* Return the recordset mode |
232
|
|
|
* @return int |
233
|
|
|
*/ |
234
|
4 |
|
final public function getMode(): int |
235
|
|
|
{ |
236
|
4 |
|
return $this->mode; |
237
|
|
|
} |
238
|
|
|
|
239
|
|
|
/** |
240
|
|
|
* Return if the current recordset can be edited |
241
|
|
|
* @return bool |
242
|
|
|
*/ |
243
|
4 |
|
public function canModify(): bool |
244
|
|
|
{ |
245
|
4 |
|
return (self::RSMODE_NOTCONNECTED !== $this->mode && '' !== $this->entity); |
246
|
|
|
} |
247
|
|
|
|
248
|
|
|
/** |
249
|
|
|
* Return if the recordset is placed in a valid record |
250
|
|
|
* @phpstan-impure |
251
|
|
|
* @return bool |
252
|
|
|
*/ |
253
|
20 |
|
final public function eof(): bool |
254
|
|
|
{ |
255
|
20 |
|
return (! is_array($this->originalValues)); |
256
|
|
|
} |
257
|
|
|
|
258
|
|
|
/** |
259
|
|
|
* Return the original value of a field |
260
|
|
|
* @param scalar|null $defaultValue |
261
|
|
|
* @return scalar|null |
262
|
|
|
*/ |
263
|
5 |
|
final public function getOriginalValue(string $fieldName, $defaultValue = '') |
264
|
|
|
{ |
265
|
5 |
|
if (! is_array($this->originalValues) || ! array_key_exists($fieldName, $this->originalValues)) { |
266
|
4 |
|
return $defaultValue; |
267
|
|
|
} |
268
|
5 |
|
return $this->originalValues[$fieldName]; |
269
|
|
|
} |
270
|
|
|
|
271
|
|
|
/** |
272
|
|
|
* Return an array with the original values. |
273
|
|
|
* |
274
|
|
|
* @return array<string, scalar|null> |
275
|
|
|
* @throws RuntimeException There are no original values |
276
|
|
|
*/ |
277
|
4 |
|
final public function getOriginalValues(): array |
278
|
|
|
{ |
279
|
4 |
|
if (! is_array($this->originalValues)) { |
280
|
4 |
|
throw new RuntimeException('There are no original values'); |
281
|
|
|
} |
282
|
4 |
|
return $this->originalValues; |
283
|
|
|
} |
284
|
|
|
|
285
|
|
|
/** |
286
|
|
|
* Prepares the recordset to make an insertion |
287
|
|
|
* All the values are set to null |
288
|
|
|
*/ |
289
|
8 |
|
final public function addNew(): void |
290
|
|
|
{ |
291
|
8 |
|
$this->originalValues = null; |
292
|
8 |
|
$this->values = $this->emptyValuesFromDataFields(); |
293
|
8 |
|
$this->mode = self::RSMODE_CONNECTED_ADDNEW; |
294
|
|
|
} |
295
|
|
|
|
296
|
|
|
/** |
297
|
|
|
* Get the last inserted id by asking the DBAL object. |
298
|
|
|
* This means that if an insertion happends between Update and LastInsertedID then the result |
299
|
|
|
* will not be related to the Update |
300
|
|
|
* @return int |
301
|
|
|
*/ |
302
|
1 |
|
final public function lastInsertedID(): int |
303
|
|
|
{ |
304
|
1 |
|
return $this->dbal->lastInsertedID(); |
305
|
|
|
} |
306
|
|
|
|
307
|
|
|
/** |
308
|
|
|
* Check whether the current values are different from the original ones |
309
|
|
|
* The base are the original values |
310
|
|
|
* @return bool |
311
|
|
|
* @throws RuntimeException if no original values exists |
312
|
|
|
*/ |
313
|
2 |
|
final public function valuesHadChanged(): bool |
314
|
|
|
{ |
315
|
2 |
|
if (! is_array($this->originalValues)) { |
316
|
|
|
throw new RuntimeException('The recordset does not contain any original values'); |
317
|
|
|
} |
318
|
2 |
|
foreach ($this->originalValues as $field => $value) { |
319
|
2 |
|
$current = array_key_exists($field, $this->values) ? $this->values[$field] : null; |
320
|
2 |
|
if (static::valueIsDifferent($value, $current)) { |
321
|
2 |
|
return true; |
322
|
|
|
} |
323
|
|
|
} |
324
|
1 |
|
return false; |
325
|
|
|
} |
326
|
|
|
|
327
|
|
|
/** |
328
|
|
|
* Compare values in order to see if they need to be updated |
329
|
|
|
* @param object|scalar|null $original |
330
|
|
|
* @param object|scalar|null $current |
331
|
|
|
* @return bool |
332
|
|
|
*/ |
333
|
6 |
|
final protected static function valueIsDifferent($original, $current): bool |
334
|
|
|
{ |
335
|
|
|
// check if some value is null |
336
|
6 |
|
$originalIsNull = (null === $original); |
337
|
6 |
|
$currentIsNull = (null === $current); |
338
|
|
|
// both are null, there are no difference |
339
|
6 |
|
if ($originalIsNull && $currentIsNull) { |
340
|
|
|
return false; |
341
|
|
|
} |
342
|
|
|
// one is null, the other isn't, there is a difference |
343
|
6 |
|
if ($originalIsNull || $currentIsNull) { |
344
|
4 |
|
return true; |
345
|
|
|
} |
346
|
|
|
// do not continue using the object, convert to string |
347
|
6 |
|
if (is_object($current)) { |
348
|
|
|
$current = self::convertObjectToString($current); |
349
|
|
|
} |
350
|
|
|
// strict comparison if types are strings |
351
|
6 |
|
if (is_string($original) && is_string($current)) { |
352
|
6 |
|
return ($original !== $current); |
353
|
|
|
} |
354
|
|
|
// simple comparison |
355
|
6 |
|
return ($original != $current); |
356
|
|
|
} |
357
|
|
|
|
358
|
|
|
/** |
359
|
|
|
* @return string[] |
360
|
|
|
*/ |
361
|
5 |
|
public function getIdFields(): array |
362
|
|
|
{ |
363
|
5 |
|
return $this->idFields; |
364
|
|
|
} |
365
|
|
|
|
366
|
|
|
/** |
367
|
|
|
* Create an array of conditions based on the current values and ids |
368
|
|
|
* This function is used on Update and on Delete |
369
|
|
|
* @param string $extraWhereClause |
370
|
|
|
* @return string[] |
371
|
|
|
*/ |
372
|
5 |
|
protected function sqlWhereConditions(string $extraWhereClause): array |
373
|
|
|
{ |
374
|
|
|
// get the conditions |
375
|
5 |
|
$conditions = []; |
376
|
5 |
|
if ($extraWhereClause) { |
377
|
|
|
$conditions[] = "($extraWhereClause)"; |
378
|
|
|
} |
379
|
5 |
|
$ids = $this->getIdFields(); |
380
|
5 |
|
if ([] === $ids) { |
381
|
|
|
$this->getLogger()->warning(sprintf( |
382
|
|
|
'Recordset: the where clause will be based on all fields because cannot locate ids.%s', |
383
|
|
|
"\n" . print_r(['entity' => $this->entity, 'values' => $this->values], true) |
|
|
|
|
384
|
|
|
)); |
385
|
|
|
$ids = array_keys($this->datafields); |
386
|
|
|
} |
387
|
5 |
|
foreach ($this->datafields as $fieldname => $field) { |
388
|
5 |
|
if (! array_key_exists($fieldname, $this->values)) { |
389
|
|
|
continue; |
390
|
|
|
} |
391
|
5 |
|
if (! in_array($fieldname, $ids)) { |
392
|
5 |
|
continue; |
393
|
|
|
} |
394
|
5 |
|
$originalValue = $this->getOriginalValue($fieldname, null); |
395
|
5 |
|
if (null === $originalValue) { |
396
|
|
|
$conditions[] = '(' . $this->dbal->sqlIsNull($this->dbal->sqlFieldEscape($fieldname)) . ')'; |
397
|
|
|
} else { |
398
|
5 |
|
$conditions[] = '(' . $this->dbal->sqlFieldEscape($fieldname) . ' = ' |
399
|
5 |
|
. $this->dbal->sqlQuote($originalValue, $field['commontype'], false) . ')'; |
400
|
|
|
} |
401
|
|
|
} |
402
|
5 |
|
return $conditions; |
403
|
|
|
} |
404
|
|
|
|
405
|
|
|
/** |
406
|
|
|
* Create the sql statement for INSERT INTO |
407
|
|
|
* @return string |
408
|
|
|
*/ |
409
|
8 |
|
protected function sqlInsert(): string |
410
|
|
|
{ |
411
|
8 |
|
$inserts = []; |
412
|
8 |
|
foreach ($this->datafields as $fieldname => $field) { |
413
|
8 |
|
$value = (array_key_exists($fieldname, $this->values)) ? $this->values[$fieldname] : null; |
414
|
8 |
|
$escapedFieldName = $this->dbal->sqlFieldEscape($field['name']); |
415
|
8 |
|
$inserts[$escapedFieldName] = $this->dbal->sqlQuote($value, $field['commontype'], true); |
416
|
|
|
} |
417
|
8 |
|
if ([] === $inserts) { |
418
|
|
|
throw new LogicException('Recordset: Insert does not have any fields to insert'); |
419
|
|
|
} |
420
|
8 |
|
return 'INSERT INTO ' . $this->dbal->sqlTableEscape($this->entity) |
421
|
8 |
|
. ' (' . implode(', ', array_keys($inserts)) . ')' |
422
|
8 |
|
. ' VALUES (' . implode(', ', $inserts) . ')' |
423
|
8 |
|
. ';'; |
424
|
|
|
} |
425
|
|
|
|
426
|
|
|
/** |
427
|
|
|
* Create the sql statement for UPDATE |
428
|
|
|
* If nothing to update then will return an empty string |
429
|
|
|
* @param string $extraWhereClause |
430
|
|
|
* @return string |
431
|
|
|
*/ |
432
|
5 |
|
protected function sqlUpdate(string $extraWhereClause): string |
433
|
|
|
{ |
434
|
|
|
// get the conditions to alter the current record |
435
|
5 |
|
$conditions = $this->sqlWhereConditions($extraWhereClause); |
436
|
|
|
// if no conditions then log error and return false |
437
|
5 |
|
if ([] === $conditions) { |
438
|
|
|
throw new LogicException('Recordset: The current record does not have any conditions to update'); |
439
|
|
|
} |
440
|
|
|
// get the fields that have changed compared to originalValues |
441
|
5 |
|
$updates = []; |
442
|
5 |
|
foreach ($this->datafields as $fieldname => $field) { |
443
|
5 |
|
if (! array_key_exists($fieldname, $this->values)) { |
444
|
|
|
$this->values[$fieldname] = null; |
445
|
|
|
} |
446
|
5 |
|
if (static::valueIsDifferent($this->getOriginalValue($fieldname, null), $this->values[$fieldname])) { |
447
|
5 |
|
$updates[] = $this->dbal->sqlFieldEscape($fieldname) . ' = ' |
448
|
5 |
|
. $this->dbal->sqlQuote($this->values[$fieldname], $field['commontype'], true); |
449
|
|
|
} |
450
|
|
|
} |
451
|
|
|
// if nothing to update, log error and return empty string |
452
|
5 |
|
if ([] === $updates) { |
453
|
|
|
return ''; |
454
|
|
|
} |
455
|
|
|
// return the update statement |
456
|
5 |
|
return 'UPDATE' |
457
|
5 |
|
. ' ' . $this->dbal->sqlTableEscape($this->entity) |
458
|
5 |
|
. ' SET ' . implode(', ', $updates) |
459
|
5 |
|
. ' WHERE ' . implode(' AND ', $conditions) |
460
|
5 |
|
. ';'; |
461
|
|
|
} |
462
|
|
|
|
463
|
|
|
/** |
464
|
|
|
* Create the sql statement for DELETE |
465
|
|
|
* @param string $extraWhereClause |
466
|
|
|
* @return string |
467
|
|
|
*/ |
468
|
4 |
|
protected function sqlDelete(string $extraWhereClause): string |
469
|
|
|
{ |
470
|
|
|
// get the conditions to alter the current record |
471
|
4 |
|
$conditions = $this->sqlWhereConditions($extraWhereClause); |
472
|
|
|
// if no conditions then log error and return false |
473
|
4 |
|
if ([] === $conditions) { |
474
|
|
|
throw new LogicException('Recordset: The current record does not have any conditions to delete'); |
475
|
|
|
} |
476
|
4 |
|
return 'DELETE' |
477
|
4 |
|
. ' FROM ' . $this->dbal->sqlTableEscape($this->entity) |
478
|
4 |
|
. ' WHERE ' . implode(' AND ', $conditions) |
479
|
4 |
|
. ';'; |
480
|
|
|
} |
481
|
|
|
|
482
|
|
|
/** |
483
|
|
|
* Build and execute an SQL UPDATE or INSERT sentence |
484
|
|
|
* Return how many rows where altered, if an update does not change any value then it returns zero |
485
|
|
|
* Return false in case of error execution |
486
|
|
|
* |
487
|
|
|
* @param string $extraWhereClause where clause to be appended into sql on UPDATE (not insert) |
488
|
|
|
* @return int|false |
489
|
|
|
*/ |
490
|
9 |
|
final public function update(string $extraWhereClause = '') |
491
|
|
|
{ |
492
|
|
|
// check the current mode is on ADDNEW or EDIT |
493
|
9 |
|
if (self::RSMODE_CONNECTED_ADDNEW !== $this->mode && self::RSMODE_CONNECTED_EDIT !== $this->mode) { |
494
|
|
|
throw new LogicException( |
495
|
|
|
"Recordset: The recordset is not on edit or addnew mode [current: {$this->mode}]" |
496
|
|
|
); |
497
|
|
|
} |
498
|
|
|
// check the entity is not empty |
499
|
9 |
|
if ('' === $this->entity) { |
500
|
|
|
throw new LogicException('Recordset: The recordset does not have a valid unique entity'); |
501
|
|
|
} |
502
|
9 |
|
$sql = ''; |
503
|
9 |
|
if (self::RSMODE_CONNECTED_ADDNEW == $this->mode) { |
504
|
8 |
|
$sql = $this->sqlInsert(); |
505
|
|
|
} |
506
|
9 |
|
if (self::RSMODE_CONNECTED_EDIT == $this->mode) { |
507
|
5 |
|
if ('' === $sql = $this->sqlUpdate($extraWhereClause)) { |
508
|
|
|
return 0; |
509
|
|
|
} |
510
|
|
|
} |
511
|
9 |
|
$altered = $this->dbal->execute($sql); |
512
|
9 |
|
if (0 === $altered) { |
513
|
1 |
|
$diffs = []; |
514
|
1 |
|
if (is_array($this->originalValues)) { |
515
|
1 |
|
foreach ($this->originalValues as $name => $value) { |
516
|
1 |
|
if (! static::valueIsDifferent($value, $this->values[$name])) { |
517
|
1 |
|
continue; |
518
|
|
|
} |
519
|
1 |
|
$diffs[] = $name; |
520
|
|
|
} |
521
|
|
|
} |
522
|
1 |
|
$this->getLogger()->warning(print_r([ |
|
|
|
|
523
|
1 |
|
'message' => "Recordset: The statement $sql return zero affected rows but the values are different", |
524
|
1 |
|
'entity' => $this->entity, |
525
|
1 |
|
'extraWhereClause' => $extraWhereClause, |
526
|
1 |
|
'original' => $this->originalValues, |
527
|
1 |
|
'current' => $this->values, |
528
|
1 |
|
'diffs' => $diffs, |
529
|
1 |
|
], true)); |
530
|
|
|
} |
531
|
9 |
|
return $altered; |
532
|
|
|
} |
533
|
|
|
|
534
|
|
|
/** |
535
|
|
|
* Build and execute the SQL DELETE sentence |
536
|
|
|
* Return how many rows where altered |
537
|
|
|
* |
538
|
|
|
* @return int|false |
539
|
|
|
*/ |
540
|
4 |
|
final public function delete(string $extraWhereClause = '') |
541
|
|
|
{ |
542
|
4 |
|
if (self::RSMODE_CONNECTED_EDIT !== $this->mode) { |
543
|
|
|
throw new LogicException('Recordset: The recordset is not on edit mode [current: ' . $this->mode . ']'); |
544
|
|
|
} |
545
|
|
|
// check the entity is not empty |
546
|
4 |
|
if ('' === $this->entity) { |
547
|
|
|
throw new LogicException('Recordset: The recordset does not have a valid unique entity'); |
548
|
|
|
} |
549
|
4 |
|
$sql = $this->sqlDelete($extraWhereClause); |
550
|
4 |
|
$altered = $this->dbal->execute($sql); |
551
|
4 |
|
if (0 === $altered) { |
552
|
|
|
$this->getLogger()->warning(print_r([ |
|
|
|
|
553
|
|
|
'message' => "Recordset: The statement '$sql' return zero affected rows" |
554
|
|
|
. ' but it should delete at least one record', |
555
|
|
|
'entity' => $this->entity, |
556
|
|
|
'extraWhereClause' => $extraWhereClause, |
557
|
|
|
'original' => $this->originalValues, |
558
|
|
|
], true)); |
559
|
|
|
} |
560
|
4 |
|
return $altered; |
561
|
|
|
} |
562
|
|
|
|
563
|
|
|
/** |
564
|
|
|
* Move to the next row and read the values |
565
|
|
|
* @return bool |
566
|
|
|
*/ |
567
|
18 |
|
final public function moveNext(): bool |
568
|
|
|
{ |
569
|
18 |
|
return ($this->isOpen() && $this->fetchLoadValues()); |
570
|
|
|
} |
571
|
|
|
|
572
|
|
|
/** |
573
|
|
|
* Move to the first row and read the values |
574
|
|
|
* @return bool |
575
|
|
|
*/ |
576
|
4 |
|
final public function moveFirst(): bool |
577
|
|
|
{ |
578
|
4 |
|
return ($this->isOpen() && $this->result()->moveFirst() && $this->fetchLoadValues()); |
579
|
|
|
} |
580
|
|
|
|
581
|
|
|
/** |
582
|
|
|
* Internal function that returns an array with the content from fields and row |
583
|
|
|
* @return array<string, null> |
584
|
|
|
*/ |
585
|
8 |
|
private function emptyValuesFromDataFields(): array |
586
|
|
|
{ |
587
|
8 |
|
return array_fill_keys(array_keys($this->datafields), null); |
588
|
|
|
} |
589
|
|
|
|
590
|
|
|
/** |
591
|
|
|
* Internal function that returns an array with the content of all datafields |
592
|
|
|
* filled with the values cast |
593
|
|
|
* |
594
|
|
|
* @param array<string, scalar|null> $source |
595
|
|
|
* @return array<string, scalar|null> |
596
|
|
|
*/ |
597
|
18 |
|
private function setValuesFromDatafields(array $source): array |
598
|
|
|
{ |
599
|
18 |
|
$values = []; |
600
|
18 |
|
foreach ($this->datafields as $fieldname => $field) { |
601
|
18 |
|
$values[$fieldname] = $this->castValueWithCommonType( |
602
|
18 |
|
array_key_exists($fieldname, $source) ? $source[$fieldname] : null, |
603
|
18 |
|
$field['commontype'] |
604
|
18 |
|
); |
605
|
|
|
} |
606
|
18 |
|
return $values; |
607
|
|
|
} |
608
|
|
|
|
609
|
|
|
/** |
610
|
|
|
* Cast a generic value from the source to a typed value, if null return null |
611
|
|
|
* |
612
|
|
|
* @param scalar|null $value |
613
|
|
|
* @param string $commonType |
614
|
|
|
* @return scalar|null |
615
|
|
|
*/ |
616
|
18 |
|
protected function castValueWithCommonType($value, string $commonType) |
617
|
|
|
{ |
618
|
|
|
// these are sorted by the most common data types to avoid extra comparisons |
619
|
18 |
|
if (null === $value) { |
620
|
4 |
|
return null; |
621
|
|
|
} |
622
|
18 |
|
if (CommonTypes::TTEXT === $commonType) { |
623
|
17 |
|
return strval($value); |
624
|
|
|
} |
625
|
18 |
|
if (CommonTypes::TINT === $commonType) { |
626
|
18 |
|
return intval($value); |
627
|
|
|
} |
628
|
17 |
|
if (CommonTypes::TNUMBER === $commonType) { |
629
|
17 |
|
return floatval($value); |
630
|
|
|
} |
631
|
13 |
|
if (CommonTypes::TBOOL === $commonType) { |
632
|
13 |
|
return boolval($value); |
633
|
|
|
} |
634
|
13 |
|
if (in_array($commonType, [CommonTypes::TDATE, CommonTypes::TTIME, CommonTypes::TDATETIME], true)) { |
635
|
13 |
|
return strtotime((string) $value); |
636
|
|
|
} |
637
|
|
|
return strval($value); |
638
|
|
|
} |
639
|
|
|
|
640
|
|
|
/** |
641
|
|
|
* Try to load values fetching a new row |
642
|
|
|
* Return true if success |
643
|
|
|
* Return false if no row was fetched, also put values to an empty array |
644
|
|
|
* @return bool |
645
|
|
|
*/ |
646
|
18 |
|
private function fetchLoadValues(): bool |
647
|
|
|
{ |
648
|
18 |
|
$row = $this->result()->fetchRow(); |
649
|
18 |
|
if (false === $row) { |
650
|
8 |
|
$this->originalValues = null; |
651
|
8 |
|
$this->values = []; |
652
|
8 |
|
return false; |
653
|
|
|
} |
654
|
18 |
|
$this->originalValues = $this->setValuesFromDatafields($row); |
655
|
18 |
|
$this->values = $this->originalValues; |
656
|
18 |
|
return true; |
657
|
|
|
} |
658
|
|
|
|
659
|
|
|
/** |
660
|
|
|
* Return the number of records in the query |
661
|
|
|
* @return int |
662
|
|
|
*/ |
663
|
30 |
|
final public function getRecordCount(): int |
664
|
|
|
{ |
665
|
30 |
|
return $this->result()->resultCount(); |
666
|
|
|
} |
667
|
|
|
|
668
|
|
|
/** |
669
|
|
|
* Return an associative array of fields, the key is the field name |
670
|
|
|
* and the content is an array containing name, common type and table |
671
|
|
|
* @return array<string, array{name: string, table: string, commontype: string}> |
672
|
|
|
*/ |
673
|
|
|
final public function getFields(): array |
674
|
|
|
{ |
675
|
|
|
return $this->datafields; |
676
|
|
|
} |
677
|
|
|
|
678
|
|
|
/** |
679
|
|
|
* Return the entity name of the query |
680
|
|
|
* @return string |
681
|
|
|
*/ |
682
|
4 |
|
final public function getEntityName(): string |
683
|
|
|
{ |
684
|
4 |
|
return $this->entity; |
685
|
|
|
} |
686
|
|
|
|
687
|
4 |
|
final public function count(): int |
688
|
|
|
{ |
689
|
4 |
|
return $this->getRecordCount(); |
690
|
|
|
} |
691
|
|
|
|
692
|
4 |
|
final public function getIterator(): Iterators\RecordsetIterator |
693
|
|
|
{ |
694
|
4 |
|
return new Iterators\RecordsetIterator($this); |
695
|
|
|
} |
696
|
|
|
|
697
|
|
|
/** |
698
|
|
|
* @return Result |
699
|
|
|
*/ |
700
|
30 |
|
private function result(): Result |
701
|
|
|
{ |
702
|
30 |
|
if (null === $this->result) { |
703
|
|
|
throw new RuntimeException('The current state of the result is NULL'); |
704
|
|
|
} |
705
|
30 |
|
return $this->result; |
706
|
|
|
} |
707
|
|
|
} |
708
|
|
|
|