1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/** |
4
|
|
|
* Handles DB operations on row level |
5
|
|
|
* |
6
|
|
|
* Class screens ordinary CRUD tasks providing high-level IDBRow interface. |
7
|
|
|
* - advanced DB data parsing to class properties with type conversion; |
8
|
|
|
* - safe storing with string escape function; |
9
|
|
|
* - smart class self-storing procedures; |
10
|
|
|
* - smart DB row partial update; |
11
|
|
|
* - delta updates for numeric DB fields on demand; |
12
|
|
|
* - managed external access to properties - READ/WRITE, READ-ONLY, READ-PROHIBITED; |
13
|
|
|
* - virtual properties - with only setter/getter and no corresponding real property; |
14
|
|
|
* - dual external access to protected data via property name or via getter/setter - including access to virtual properties; |
15
|
|
|
* |
16
|
|
|
* |
17
|
|
|
* No properties should be directly exposed to public |
18
|
|
|
* All property modifications should pass through __call() method to made partial update feature work; |
19
|
|
|
* All declared internal properties should start with '_' following by one of the indexes from $_properties static |
20
|
|
|
* |
21
|
|
|
* method int getDbId() |
22
|
|
|
* @property int dbId |
23
|
|
|
*/ |
24
|
|
|
abstract class DBRow extends PropertyHiderInObject implements IDbRow { |
25
|
|
|
// TODO |
26
|
|
|
/** |
27
|
|
|
* Should be this object - (!) not class - cached |
28
|
|
|
* There exists tables that didn't need to cache rows - logs as example |
29
|
|
|
* And there can be special needs to not cache some class instances when stream-reading many rows i.e. fleets in stat calculation |
30
|
|
|
* |
31
|
|
|
* @var bool $_cacheable |
32
|
|
|
*/ |
33
|
|
|
public $_cacheable = true; // |
34
|
|
|
// TODO |
35
|
|
|
/** |
36
|
|
|
* БД для доступа к данным |
37
|
|
|
* |
38
|
|
|
* @var db_mysql $db |
39
|
|
|
*/ |
40
|
|
|
protected static $db = null; |
41
|
|
|
/** |
42
|
|
|
* Table name in DB |
43
|
|
|
* |
44
|
|
|
* @var string |
45
|
|
|
*/ |
46
|
|
|
protected static $_table = ''; |
47
|
|
|
/** |
48
|
|
|
* Name of ID field in DB |
49
|
|
|
* |
50
|
|
|
* @var string |
51
|
|
|
*/ |
52
|
|
|
protected static $_dbIdFieldName = 'id'; |
53
|
|
|
|
54
|
|
|
/** |
55
|
|
|
* DB_ROW to Class translation scheme |
56
|
|
|
* |
57
|
|
|
* @var array |
58
|
|
|
*/ |
59
|
|
|
protected $_properties = array( |
60
|
|
|
'dbId' => array( |
61
|
|
|
P_DB_FIELD => 'id', |
62
|
|
|
), |
63
|
|
|
); |
64
|
|
|
|
65
|
|
|
/** |
66
|
|
|
* Object list that should mimic object DB operations - i.e. units on fleet |
67
|
|
|
* |
68
|
|
|
* @var IDbRow[] |
69
|
|
|
*/ |
70
|
|
|
protected $triggerDbOperationOn = array(); // Not a static - because it's an object array |
71
|
|
|
|
72
|
|
|
/** |
73
|
|
|
* @var int |
74
|
|
|
*/ |
75
|
|
|
protected $_dbId = 0; |
76
|
|
|
|
77
|
|
|
/** |
78
|
|
|
* Flag to skip lock on current Load operation |
79
|
|
|
* |
80
|
|
|
* @var bool |
81
|
|
|
*/ |
82
|
|
|
protected $lockSkip = false; |
83
|
|
|
|
84
|
|
|
|
85
|
|
|
/** |
86
|
|
|
* @param db_mysql|null $db |
87
|
|
|
*/ |
88
|
1 |
|
public static function setDb($db = null) { |
89
|
1 |
|
if(empty($db) || !($db instanceof db_mysql)) { |
90
|
1 |
|
$db = null; |
91
|
1 |
|
} |
92
|
1 |
|
static::$db = !empty($db) || !class_exists('classSupernova', false) ? $db : classSupernova::$db; |
93
|
1 |
|
} |
94
|
|
|
|
95
|
2 |
|
public static function getDb() { |
96
|
2 |
|
return static::$db; |
97
|
|
|
} |
98
|
|
|
|
99
|
|
|
/** |
100
|
|
|
* @param string $tableName |
101
|
|
|
*/ |
102
|
1 |
|
public static function setTable($tableName) { |
103
|
1 |
|
static::$_table = $tableName; |
104
|
1 |
|
} |
105
|
|
|
|
106
|
1 |
|
public static function getTable() { |
107
|
1 |
|
return static::$_table; |
108
|
|
|
} |
109
|
|
|
|
110
|
|
|
/** |
111
|
|
|
* @param string $dbIdFieldName |
112
|
|
|
*/ |
113
|
1 |
|
public static function setIdFieldName($dbIdFieldName) { |
114
|
1 |
|
static::$_dbIdFieldName = $dbIdFieldName; |
115
|
1 |
|
} |
116
|
|
|
|
117
|
1 |
|
public static function getIdFieldName() { |
118
|
1 |
|
return static::$_dbIdFieldName; |
119
|
|
|
} |
120
|
|
|
|
121
|
|
|
// Some magic ******************************************************************************************************** |
122
|
|
|
|
123
|
|
|
/** |
124
|
|
|
* DBRow constructor. |
125
|
|
|
* |
126
|
|
|
* @param db_mysql|null $db |
127
|
|
|
*/ |
128
|
1 |
|
public function __construct($db = null) { |
129
|
1 |
|
parent::__construct(); |
130
|
1 |
|
if (empty($db)) { |
131
|
1 |
|
$db = static::getDb(); |
132
|
1 |
|
} |
133
|
|
|
|
134
|
1 |
|
static::setDb($db); |
135
|
1 |
|
} |
136
|
|
|
|
137
|
|
|
|
138
|
|
|
|
139
|
|
|
// IDBrow Implementation ********************************************************************************************* |
140
|
|
|
|
141
|
|
|
/** |
142
|
|
|
* Loading object from DB by primary ID |
143
|
|
|
* |
144
|
|
|
* @param int $dbId |
145
|
|
|
* @param bool $lockSkip |
146
|
|
|
* |
147
|
|
|
* @return |
148
|
|
|
*/ |
149
|
|
|
public function dbLoad($dbId, $lockSkip = false) { |
150
|
|
|
$dbId = idval($dbId); |
151
|
|
|
if ($dbId <= 0) { |
152
|
|
|
classSupernova::$debug->error(get_called_class() . '::' . __METHOD__ . ' $dbId not positive = ' . $dbId); |
153
|
|
|
|
154
|
|
|
return; |
155
|
|
|
} |
156
|
|
|
|
157
|
|
|
$this->_dbId = $dbId; |
|
|
|
|
158
|
|
|
$this->lockSkip = $lockSkip; |
159
|
|
|
// TODO - Use classSupernova::$db_records_locked |
160
|
|
|
if (false && !$lockSkip && sn_db_transaction_check(false)) { |
161
|
|
|
$this->dbGetLockById($this->_dbId); |
162
|
|
|
} |
163
|
|
|
|
164
|
|
|
$db_row = classSupernova::$db->doSelectFetch("SELECT * FROM `{{" . static::$_table . "}}` WHERE `" . static::$_dbIdFieldName . "` = " . $this->_dbId . " LIMIT 1 FOR UPDATE;"); |
165
|
|
|
if (empty($db_row)) { |
166
|
|
|
return; |
167
|
|
|
} |
168
|
|
|
|
169
|
|
|
$this->dbRowParse($db_row); |
170
|
|
|
$this->lockSkip = false; |
171
|
|
|
} |
172
|
|
|
|
173
|
|
|
/** |
174
|
|
|
* Lock all fields that belongs to operation |
175
|
|
|
* |
176
|
|
|
* @param int $dbId |
177
|
|
|
* |
178
|
|
|
* @return |
179
|
|
|
* param DBLock $dbRow - Object that accumulates locks |
180
|
|
|
* |
181
|
|
|
*/ |
182
|
|
|
abstract public function dbGetLockById($dbId); |
183
|
|
|
|
184
|
|
|
/** |
185
|
|
|
* Saving object to DB |
186
|
|
|
* This is meta-method: |
187
|
|
|
* - if object is new - then it inserted to DB; |
188
|
|
|
* - if object is empty - it deleted from DB; |
189
|
|
|
* - otherwise object is updated in DB; |
190
|
|
|
*/ |
191
|
|
|
// TODO - perform operations only if properties was changed |
192
|
|
|
public function dbSave() { |
193
|
|
|
if ($this->isNew()) { |
194
|
|
|
// No DB_ID - new unit |
195
|
|
|
if ($this->isEmpty()) { |
196
|
|
|
classSupernova::$debug->error(__FILE__ . ':' . __LINE__ . ' - object is empty on ' . get_called_class() . '::dbSave'); |
197
|
|
|
} |
198
|
|
|
$this->dbInsert(); |
199
|
|
|
} else { |
200
|
|
|
// DB_ID is present |
201
|
|
|
if ($this->isEmpty()) { |
202
|
|
|
$this->dbDelete(); |
203
|
|
|
} else { |
204
|
|
|
if (!sn_db_transaction_check(false)) { |
205
|
|
|
classSupernova::$debug->error(__FILE__ . ':' . __LINE__ . ' - transaction should always be started on ' . get_called_class() . '::dbUpdate'); |
206
|
|
|
} |
207
|
|
|
$this->dbUpdate(); |
208
|
|
|
} |
209
|
|
|
} |
210
|
|
|
|
211
|
|
|
if (!empty($this->triggerDbOperationOn)) { |
212
|
|
|
foreach ($this->triggerDbOperationOn as $item) { |
213
|
|
|
$item->dbSave(); |
214
|
|
|
} |
215
|
|
|
} |
216
|
|
|
|
217
|
|
|
$this->propertiesChanged = array(); |
218
|
|
|
$this->propertiesAdjusted = array(); |
219
|
|
|
} |
220
|
|
|
|
221
|
|
|
|
222
|
|
|
|
223
|
|
|
// CRUD ************************************************************************************************************** |
224
|
|
|
|
225
|
|
|
/** |
226
|
|
|
* Inserts record to DB |
227
|
|
|
* |
228
|
|
|
* @return int|string |
229
|
|
|
*/ |
230
|
|
|
// TODO - protected |
231
|
|
|
public function dbInsert() { |
232
|
|
|
if (!$this->isNew()) { |
233
|
|
|
classSupernova::$debug->error(__FILE__ . ':' . __LINE__ . ' - record db_id is not empty on ' . get_called_class() . '::dbInsert'); |
234
|
|
|
} |
235
|
|
|
|
236
|
|
|
$fieldSet = $this->dbMakeFieldSet(false); |
237
|
|
|
|
238
|
|
|
if (!static::$db->doInsertSet(static::$_table, $fieldSet)) { |
239
|
|
|
classSupernova::$debug->error(__FILE__ . ':' . __LINE__ . ' - error saving record ' . get_called_class() . '::dbInsert'); |
240
|
|
|
} |
241
|
|
|
$this->_dbId = static::$db->db_insert_id(); |
242
|
|
|
|
243
|
|
|
return $this->_dbId; |
244
|
|
|
} |
245
|
|
|
|
246
|
|
|
/** |
247
|
|
|
* Updates record in DB |
248
|
|
|
*/ |
249
|
|
|
// TODO - protected |
250
|
|
|
public function dbUpdate() { |
251
|
|
|
// TODO - Update |
252
|
|
|
if ($this->isNew()) { |
253
|
|
|
classSupernova::$debug->error(__FILE__ . ':' . __LINE__ . ' - unit db_id is empty on dbUpdate'); |
254
|
|
|
} |
255
|
|
|
$this->db_field_update($this->dbMakeFieldUpdate()); |
|
|
|
|
256
|
|
|
} |
257
|
|
|
|
258
|
|
|
/** |
259
|
|
|
* Deletes record from DB |
260
|
|
|
*/ |
261
|
|
|
// TODO - protected |
262
|
|
|
public function dbDelete() { |
263
|
|
|
if ($this->isNew()) { |
264
|
|
|
classSupernova::$debug->error(__FILE__ . ':' . __LINE__ . ' - unit db_id is empty on dbDelete'); |
265
|
|
|
} |
266
|
|
|
classSupernova::$gc->db->doDeleteRowWhere(static::$_table, array(static::$_dbIdFieldName => $this->_dbId)); |
|
|
|
|
267
|
|
|
$this->_dbId = 0; |
268
|
|
|
// Обо всём остальном должен позаботиться контейнер |
269
|
|
|
} |
270
|
|
|
|
271
|
|
|
/** |
272
|
|
|
* Является ли запись новой - т.е. не имеет своей записи в БД |
273
|
|
|
* |
274
|
|
|
* @return bool |
275
|
|
|
*/ |
276
|
|
|
public function isNew() { |
277
|
|
|
return $this->_dbId == 0; |
278
|
|
|
} |
279
|
|
|
|
280
|
|
|
/** |
281
|
|
|
* Является ли запись пустой - т.е. при исполнении _dbSave должен быть удалён |
282
|
|
|
* |
283
|
|
|
* @return bool |
284
|
|
|
*/ |
285
|
|
|
abstract public function isEmpty(); |
286
|
|
|
|
287
|
|
|
// Other Methods ***************************************************************************************************** |
288
|
|
|
/** |
289
|
|
|
* Парсит запись из БД в поля объекта |
290
|
|
|
* |
291
|
|
|
* @param array $db_row |
292
|
|
|
*/ |
293
|
|
|
public function dbRowParse(array $db_row) { |
294
|
|
|
foreach ($this->_properties as $property_name => &$property_data) { |
295
|
|
|
// Advanced values extraction procedure. Should be used when at least one of following rules is matched: |
296
|
|
|
// - one field should translate to several properties; |
297
|
|
|
// - one property should be filled according to several fields; |
298
|
|
|
// - property filling requires some lookup in object values; |
299
|
|
View Code Duplication |
if (!empty($property_data[P_METHOD_EXTRACT]) && is_callable(array($this, $property_data[P_METHOD_EXTRACT]))) { |
|
|
|
|
300
|
|
|
call_user_func_array(array($this, $property_data[P_METHOD_EXTRACT]), array(&$db_row)); |
301
|
|
|
continue; |
302
|
|
|
} |
303
|
|
|
|
304
|
|
|
// If property is read-only - doing nothing |
305
|
|
|
if (!empty($property_data[P_READ_ONLY])) { |
306
|
|
|
continue; |
307
|
|
|
} |
308
|
|
|
|
309
|
|
|
// Getting field value as base only if $_properties has 1-to-1 relation to object property |
310
|
|
|
$value = !empty($property_data[P_DB_FIELD]) && isset($db_row[$property_data[P_DB_FIELD]]) ? $db_row[$property_data[P_DB_FIELD]] : null; |
311
|
|
|
|
312
|
|
|
// Making format conversion from string ($db_row default type) to property type |
313
|
|
|
!empty($property_data[P_FUNC_INPUT]) && is_callable($property_data[P_FUNC_INPUT]) ? $value = call_user_func($property_data[P_FUNC_INPUT], $value) : false; |
314
|
|
|
|
315
|
|
|
// If there is setter for this field - using it. Setters is always a methods of $THIS |
316
|
|
|
if (!empty($property_data[P_METHOD_SET]) && is_callable(array($this, $property_data[P_METHOD_SET]))) { |
317
|
|
|
call_user_func(array($this, $property_data[P_METHOD_SET]), $value); |
318
|
|
|
} else { |
319
|
|
|
$this->{$property_name} = $value; |
320
|
|
|
} |
321
|
|
|
} |
322
|
|
|
} |
323
|
|
|
|
324
|
|
|
/** |
325
|
|
|
* Делает из свойств класса массив db_field_name => db_field_value |
326
|
|
|
* |
327
|
|
|
* @return array |
328
|
|
|
*/ |
329
|
|
|
protected function dbMakeFieldSet($isUpdate = false) { |
330
|
|
|
$array = array(); |
331
|
|
|
|
332
|
|
|
foreach (static::$_properties as $property_name => &$property_data) { |
333
|
|
|
// TODO - on isUpdate add only changed/adjusted properties |
334
|
|
|
|
335
|
|
View Code Duplication |
if (!empty($property_data[P_METHOD_INJECT]) && is_callable(array($this, $property_data[P_METHOD_INJECT]))) { |
|
|
|
|
336
|
|
|
call_user_func_array(array($this, $property_data[P_METHOD_INJECT]), array(&$array)); |
337
|
|
|
continue; |
338
|
|
|
} |
339
|
|
|
|
340
|
|
|
// Skipping properties which have no corresponding field in DB |
341
|
|
|
if (empty($property_data[P_DB_FIELD])) { |
342
|
|
|
continue; |
343
|
|
|
} |
344
|
|
|
|
345
|
|
|
// Checking - is property was adjusted or changed |
346
|
|
|
if ($isUpdate && array_key_exists($property_name, $this->propertiesAdjusted)) { |
347
|
|
|
// For adjusted property - take value from propertiesAdjusted array |
348
|
|
|
// TODO - differ how treated conversion to string for changed and adjusted properties |
349
|
|
|
$value = $this->propertiesAdjusted[$property_name]; |
350
|
|
|
} else { |
351
|
|
|
// TODO - ОШИБКА!!!!!!!!!!!!!!!! |
|
|
|
|
352
|
|
|
// Getting property value. Optionally getter is invoked by __get() |
353
|
|
|
$value = $this->{$property_name}; |
354
|
|
|
} |
355
|
|
|
|
356
|
|
|
// If need some conversion to DB format - doing it |
357
|
|
|
!empty($property_data[P_FUNC_OUTPUT]) && is_callable($property_data[P_FUNC_OUTPUT]) |
358
|
|
|
? $value = call_user_func($property_data[P_FUNC_OUTPUT], $value) : false; |
359
|
|
|
!empty($property_data[P_METHOD_OUTPUT]) && is_callable(array($this, $property_data[P_METHOD_OUTPUT])) |
360
|
|
|
? $value = call_user_func(array($this, $property_data[P_METHOD_OUTPUT]), $value) : false; |
361
|
|
|
|
362
|
|
|
$array[$property_data[P_DB_FIELD]] = $value; |
363
|
|
|
} |
364
|
|
|
|
365
|
|
|
return $array; |
366
|
|
|
} |
367
|
|
|
|
368
|
|
|
/** |
369
|
|
|
* Делает из свойств класса массив db_field_name => db_field_value |
370
|
|
|
* @return array |
371
|
|
|
* @deprecated |
372
|
|
|
*/ |
373
|
|
|
protected function dbMakeFieldUpdate() { |
374
|
|
|
$array = array(); |
375
|
|
|
foreach (static::$_properties as $property_name => &$property_data) { |
376
|
|
|
// TODO - on isUpdate add only changed/adjusted properties |
377
|
|
|
// Skipping properties which have no corresponding field in DB |
378
|
|
|
if (empty($property_data[P_DB_FIELD])) { |
379
|
|
|
continue; |
380
|
|
|
} |
381
|
|
|
|
382
|
|
|
// Checking - is property was adjusted or changed |
383
|
|
|
if (array_key_exists($property_name, $this->propertiesAdjusted)) { |
384
|
|
|
// For adjusted property - take value from propertiesAdjusted array |
385
|
|
|
// TODO - differ how treated conversion to string for changed and adjusted properties |
386
|
|
|
$value = $this->propertiesAdjusted[$property_name]; |
387
|
|
|
} else { |
388
|
|
|
// Skipping not updated properties |
389
|
|
|
continue; |
390
|
|
|
} |
391
|
|
|
|
392
|
|
|
// If need some conversion to DB format - doing it |
393
|
|
|
!empty($property_data[P_FUNC_OUTPUT]) && is_callable($property_data[P_FUNC_OUTPUT]) |
394
|
|
|
? $value = call_user_func($property_data[P_FUNC_OUTPUT], $value) : false; |
395
|
|
|
!empty($property_data[P_METHOD_OUTPUT]) && is_callable(array($this, $property_data[P_METHOD_OUTPUT])) |
396
|
|
|
? $value = call_user_func(array($this, $property_data[P_METHOD_OUTPUT]), $value) : false; |
397
|
|
|
|
398
|
|
|
$array[$property_data[P_DB_FIELD]] = $value; |
399
|
|
|
} |
400
|
|
|
|
401
|
|
|
return $array; |
402
|
|
|
} |
403
|
|
|
|
404
|
|
|
/** |
405
|
|
|
* Check if DB field changed on property change and if it changed - returns name of property which triggered change |
406
|
|
|
* |
407
|
|
|
* @param string $fieldName |
408
|
|
|
* |
409
|
|
|
* @return string|false |
410
|
|
|
*/ |
411
|
|
|
protected function isFieldChanged($fieldName) { |
412
|
|
|
$isFieldChanged = false; |
413
|
|
|
foreach ($this->propertiesChanged as $propertyName => $cork) { |
414
|
|
|
$propertyScheme = static::$_properties[$propertyName]; |
415
|
|
|
if (!empty($propertyScheme[P_DB_FIELDS_LINKED])) { |
416
|
|
|
foreach ($propertyScheme[P_DB_FIELDS_LINKED] as $linkedFieldName) { |
417
|
|
|
if ($linkedFieldName == $fieldName) { |
418
|
|
|
$isFieldChanged = $propertyName; |
419
|
|
|
break 2; |
420
|
|
|
} |
421
|
|
|
} |
422
|
|
|
} |
423
|
|
|
if (!empty($propertyScheme[P_DB_FIELD]) && $propertyScheme[P_DB_FIELD] == $fieldName) { |
424
|
|
|
$isFieldChanged = $propertyName; |
425
|
|
|
break; |
426
|
|
|
} |
427
|
|
|
} |
428
|
|
|
|
429
|
|
|
return $isFieldChanged; |
430
|
|
|
} |
431
|
|
|
|
432
|
|
|
/** |
433
|
|
|
* @param array $field_set |
434
|
|
|
* |
435
|
|
|
* @return array|bool|mysqli_result|null |
436
|
|
|
* @deprecated |
437
|
|
|
*/ |
438
|
|
|
protected function db_field_update(array $field_set) { |
439
|
|
|
$set = array(); |
440
|
|
|
foreach ($field_set as $fieldName => $value) { |
441
|
|
|
if (!($changedProperty = $this->isFieldChanged($fieldName))) { |
442
|
|
|
continue; |
443
|
|
|
} |
444
|
|
|
|
445
|
|
|
if (array_key_exists($changedProperty, $this->propertiesAdjusted)) { |
446
|
|
|
$set[$fieldName] = $value; |
447
|
|
|
} |
448
|
|
|
} |
449
|
|
|
|
450
|
|
|
if(empty($set)) { |
451
|
|
|
$theResult = true; |
452
|
|
|
} else { |
453
|
|
|
$theResult = classSupernova::$db->doUpdateRowAdjust( |
454
|
|
|
static::$_table, |
455
|
|
|
array(), |
456
|
|
|
$field_set, |
457
|
|
|
array( |
458
|
|
|
static::$_dbIdFieldName => $this->_dbId, |
459
|
|
|
)); |
460
|
|
|
} |
461
|
|
|
return $theResult; |
462
|
|
|
} |
463
|
|
|
|
464
|
|
|
} |
465
|
|
|
|
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.
For example, imagine you have a variable
$accountId
that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to theid
property of an instance of theAccount
class. This class holds a proper account, so the id value must no longer be false.Either this assignment is in error or a type check should be added for that assignment.