Completed
Push — work-fleets ( 355465...808c81 )
by SuperNova.WS
06:24
created

DBRow::getDb()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 2
c 1
b 0
f 0
nc 1
nop 0
dl 0
loc 3
ccs 0
cts 3
cp 0
crap 2
rs 10
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 stdClass 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
   * DB_ROW to Class translation scheme
55
   *
56
   * @var array
57
   */
58
  protected static $_properties = array(
59
    'dbId' => array(
60
      P_DB_FIELD => 'id',
61
    ),
62
  );
63
64
  /**
65
   * Object list that should mimic object DB operations - i.e. units on fleet
66
   *
67
   * @var IDbRow[]
68
   */
69
  protected $triggerDbOperationOn = array(); // Not a static - because it's an object array
70
  /**
71
   * List of property names that was changed since last DB operation
72
   *
73
   * @var string[]
74
   */
75
  protected $propertiesChanged = array();
76
  /**
77
   * List of property names->$delta that was adjusted since last DB operation - and then need to be processed as Deltas
78
   *
79
   * @var string[]
80
   */
81
  protected $propertiesAdjusted = array();
82
83
  /**
84
   * @var int
85
   */
86
  protected $_dbId = 0;
87
88
  /**
89
   * Flag to skip lock on current Load operation
90
   *
91
   * @var bool
92
   */
93
  protected $lockSkip = false;
94
95
96
  /**
97
   * @param db_mysql|null $db
98
   */
99
  public static function setDb($db = null) {
100
    if(empty($db) || !($db instanceof db_mysql)) {
101
      $db = null;
102
    }
103
    static::$db = !empty($db) || !class_exists('classSupernova', false) ? $db : classSupernova::$db;
104
  }
105
106
  public static function getDb() {
107
    return static::$db;
108
  }
109
110
  /**
111
   * @param string $tableName
112
   */
113
  public static function setTable($tableName) {
114
    static::$_table = $tableName;
115
  }
116
117
  public static function getTable() {
118
    return static::$_table;
119
  }
120
121
  /**
122
   * @param string $dbIdFieldName
123
   */
124
  public static function setIdFieldName($dbIdFieldName) {
125
    static::$_dbIdFieldName = $dbIdFieldName;
126
  }
127
128
  public static function getIdFieldName() {
129
    return static::$_dbIdFieldName;
130
  }
131
132
  /**
133
   * @param array $properties
134
   */
135
  public static function setProperties($properties) {
136
    static::$_properties = $properties;
137
  }
138
139
  public static function getProperties() {
140
    return static::$_properties;
141
  }
142
143
  // Some magic ********************************************************************************************************
144
145
  /**
146
   * DBRow constructor.
147
   *
148
   * @param db_mysql|null $db
149
   */
150
  public function __construct($db = null) {
151
    if (empty($db)) {
152
      $db = static::getDb();
153
    }
154
155
    static::setDb($db);
156
  }
157
158
  /**
159
   * Getter with support of protected methods
160
   *
161
   * @param $name
162
   *
163
   * @return mixed
164
   */
165
  public function __get($name) {
166
    // Redirecting inaccessible get to __call which will handle the rest
167
    return $this->__call('get' . ucfirst($name), array());
168
  }
169
170
  /**
171
   * Setter with support of protected properties/methods
172
   *
173
   * @param $name
174
   * @param $value
175
   */
176
  // TODO - сеттер должен параллельно изменять значение db_row - for now...
177
  public function __set($name, $value) {
178
    // Redirecting inaccessible set to __call which will handle the rest
179
    $this->__call('set' . ucfirst($name), array($value));
180
  }
181
182
  /**
183
   * Handles getters and setters
184
   *
185
   * @param string $name
186
   * @param array  $arguments
187
   *
188
   * @return mixed
189
   * @throws ExceptionPropertyNotExists
190
   */
191
  public function __call($name, $arguments) {
192
    $left3 = substr($name, 0, 3);
193
    $propertyName = lcfirst(substr($name, 3));
194
195
    // If method is not getter or setter OR property name not exists in $_properties - raising exception
196
    // Descendants can catch this Exception to make own __call magic
197
    if (($left3 != 'get' && $left3 != 'set') || empty(static::$_properties[$propertyName])) {
198
      throw new ExceptionPropertyNotExists('Property ' . $propertyName . ' not exists when calling getter/setter ' . get_called_class() . '::' . $name, ERR_ERROR);
199
    }
200
201
    // TODO check for read-only
202
203
    if ($left3 == 'set') {
204
      if (!empty($this->propertiesAdjusted[$propertyName])) {
205
        throw new PropertyAccessException('Property ' . $propertyName . ' already was adjusted so no SET is possible until dbSave in ' . get_called_class() . '::' . $name, ERR_ERROR);
206
      }
207
      $this->propertiesChanged[$propertyName] = 1;
208
    }
209
210
    // Now deciding - will we call a protected setter or will we work with protected property
211
212
    // If method exists - just calling it
213
    if (method_exists($this, $name)) {
214
      return call_user_func_array(array($this, $name), $arguments);
215
    }
216
    // No getter/setter exists - works directly with protected property
217
218
    // Is it getter?
219
    if ($left3 === 'get') {
220
      return $this->{'_' . $propertyName};
221
    }
222
223
    // Not getter? Then it's setter
224
    $this->{'_' . $propertyName} = $arguments[0];
225
226
    return null;
227
  }
228
229
  // IDBrow Implementation *********************************************************************************************
230
231
  /**
232
   * Loading object from DB by primary ID
233
   *
234
   * @param int  $dbId
235
   * @param bool $lockSkip
236
   *
237
   * @return
238
   */
239
  public function dbLoad($dbId, $lockSkip = false) {
240
    $dbId = idval($dbId);
241
    if ($dbId <= 0) {
242
      classSupernova::$debug->error(get_called_class() . '::dbLoad $dbId not positive = ' . $dbId);
243
244
      return;
245
    }
246
247
    $this->_dbId = $dbId;
0 ignored issues
show
Documentation Bug introduced by
It seems like $dbId can also be of type double. However, the property $_dbId is declared as type integer. Maybe add an additional type check?

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 the id property of an instance of the Account 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.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
248
    $this->lockSkip = $lockSkip;
249
    // TODO - Use classSupernova::$db_records_locked
250
    if (false && !$lockSkip && sn_db_transaction_check(false)) {
251
      $this->dbGetLockById($this->_dbId);
252
    }
253
254
    $db_row = doquery("SELECT * FROM `{{" . static::$_table . "}}` WHERE `" . static::$_dbIdFieldName . "` = " . $this->_dbId . " LIMIT 1 FOR UPDATE;", true);
0 ignored issues
show
Documentation introduced by
true is of type boolean, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
255
    if (empty($db_row)) {
256
      return;
257
    }
258
259
    $this->dbRowParse($db_row);
260
    $this->lockSkip = false;
261
  }
262
263
  /**
264
   * Lock all fields that belongs to operation
265
   *
266
   * @param int $dbId
267
   *
268
   * @return
269
   * param DBLock $dbRow - Object that accumulates locks
270
   *
271
   */
272
  abstract public function dbGetLockById($dbId);
273
274
  /**
275
   * Saving object to DB
276
   * This is meta-method:
277
   * - if object is new - then it inserted to DB;
278
   * - if object is empty - it deleted from DB;
279
   * - otherwise object is updated in DB;
280
   */
281
  // TODO - perform operations only if properties was changed
282
  public function dbSave() {
283
    if ($this->isNew()) {
284
      // No DB_ID - new unit
285
      if ($this->isEmpty()) {
286
        classSupernova::$debug->error(__FILE__ . ':' . __LINE__ . ' - object is empty on ' . get_called_class() . '::dbSave');
287
      }
288
      $this->dbInsert();
289
    } else {
290
      // DB_ID is present
291
      if ($this->isEmpty()) {
292
        $this->dbDelete();
293
      } else {
294
        if (!sn_db_transaction_check(false)) {
295
          classSupernova::$debug->error(__FILE__ . ':' . __LINE__ . ' - transaction should always be started on ' . get_called_class() . '::dbUpdate');
296
        }
297
        $this->dbUpdate();
298
      }
299
    }
300
301
    if (!empty($this->triggerDbOperationOn)) {
302
      foreach ($this->triggerDbOperationOn as $item) {
303
        $item->dbSave();
304
      }
305
    }
306
307
    $this->propertiesChanged = array();
308
    $this->propertiesAdjusted = array();
309
  }
310
311
312
313
  // CRUD **************************************************************************************************************
314
315
  /**
316
   * Inserts record to DB
317
   *
318
   * @return int|string
319
   */
320
  // TODO - protected
321
  public function dbInsert() {
322
    if (!$this->isNew()) {
323
      classSupernova::$debug->error(__FILE__ . ':' . __LINE__ . ' - record db_id is not empty on ' . get_called_class() . '::dbInsert');
324
    }
325
    $this->_dbId = $this->db_field_set_create($this->dbMakeFieldSet());
326
327
    if (empty($this->_dbId)) {
328
      classSupernova::$debug->error(__FILE__ . ':' . __LINE__ . ' - error saving record ' . get_called_class() . '::dbInsert');
329
    }
330
331
    return $this->_dbId;
332
  }
333
334
  /**
335
   * Updates record in DB
336
   */
337
  // TODO - protected
338
  public function dbUpdate() {
339
    // TODO - Update
340
    if ($this->isNew()) {
341
      classSupernova::$debug->error(__FILE__ . ':' . __LINE__ . ' - unit db_id is empty on dbUpdate');
342
    }
343
    $this->db_field_update($this->dbMakeFieldSet(true));
344
  }
345
346
  /**
347
   * Deletes record from DB
348
   */
349
  // TODO - protected
350
  public function dbDelete() {
351
    if ($this->isNew()) {
352
      classSupernova::$debug->error(__FILE__ . ':' . __LINE__ . ' - unit db_id is empty on dbDelete');
353
    }
354
    doquery("DELETE FROM {{" . static::$_table . "}} WHERE `" . static::$_dbIdFieldName . "` = " . $this->_dbId);
355
    $this->_dbId = 0;
356
    // Обо всём остальном должен позаботиться контейнер
357
  }
358
359
  /**
360
   * Является ли запись новой - т.е. не имеет своей записи в БД
361
   *
362
   * @return bool
363
   */
364
  public function isNew() {
365
    return $this->_dbId == 0;
366
  }
367
368
  /**
369
   * Является ли запись пустой - т.е. при исполнении _dbSave должен быть удалён
370
   *
371
   * @return bool
372
   */
373
  abstract public function isEmpty();
374
375
  // Other Methods *****************************************************************************************************
376
377
//  /**
378
//   * Resets object to zero state
379
//   * @see DBRow::dbLoad()
380
//   *
381
//   * @return void
382
//   */
383
//  protected function _reset() {
384
//    $this->dbRowParse(array());
385
//  }
386
387
  /**
388
   * Парсит запись из БД в поля объекта
389
   *
390
   * @param array $db_row
391
   */
392
  public function dbRowParse(array $db_row) {
393
    foreach (static::$_properties as $property_name => &$property_data) {
394
      // Advanced values extraction procedure. Should be used when at least one of following rules is matched:
395
      // - one field should translate to several properties;
396
      // - one property should be filled according to several fields;
397
      // - property filling requires some lookup in object values;
398 View Code Duplication
      if (!empty($property_data[P_METHOD_EXTRACT]) && is_callable(array($this, $property_data[P_METHOD_EXTRACT]))) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
399
        call_user_func_array(array($this, $property_data[P_METHOD_EXTRACT]), array(&$db_row));
400
        continue;
401
      }
402
403
      // If property is read-only - doing nothing
404
      if (!empty($property_data[P_READ_ONLY])) {
405
        continue;
406
      }
407
408
      // Getting field value as base only if $_properties has 1-to-1 relation to object property
409
      $value = !empty($property_data[P_DB_FIELD]) && isset($db_row[$property_data[P_DB_FIELD]]) ? $db_row[$property_data[P_DB_FIELD]] : null;
410
411
      // Making format conversion from string ($db_row default type) to property type
412
      !empty($property_data[P_FUNC_INPUT]) && is_callable($property_data[P_FUNC_INPUT]) ? $value = call_user_func($property_data[P_FUNC_INPUT], $value) : false;
413
414
      // If there is setter for this field - using it. Setters is always a methods of $THIS
415
      if (!empty($property_data[P_METHOD_SET]) && is_callable(array($this, $property_data[P_METHOD_SET]))) {
416
        call_user_func(array($this, $property_data[P_METHOD_SET]), $value);
417
      } else {
418
        $this->{$property_name} = $value;
419
      }
420
    }
421
  }
422
423
  /**
424
   * Делает из свойств класса массив db_field_name => db_field_value
425
   *
426
   * @return array
427
   */
428
  protected function dbMakeFieldSet($isUpdate = false) {
429
    $array = array();
430
431
    foreach (static::$_properties as $property_name => &$property_data) {
432
      // TODO - on isUpdate add only changed/adjusted properties
433
434 View Code Duplication
      if (!empty($property_data[P_METHOD_INJECT]) && is_callable(array($this, $property_data[P_METHOD_INJECT]))) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
435
        call_user_func_array(array($this, $property_data[P_METHOD_INJECT]), array(&$array));
436
        continue;
437
      }
438
439
      // Skipping properties which have no corresponding field in DB
440
      if (empty($property_data[P_DB_FIELD])) {
441
        continue;
442
      }
443
444
      // Checking - is property was adjusted or changed
445
      if ($isUpdate && array_key_exists($property_name, $this->propertiesAdjusted)) {
446
        // For adjusted property - take value from propertiesAdjusted array
447
        // TODO - differ how treated conversion to string for changed and adjusted properties
448
        $value = $this->propertiesAdjusted[$property_name];
449
      } else {
450
        // Getting property value. Optionally getter is invoked by __get()
451
        $value = $this->{$property_name};
452
      }
453
454
      // If need some conversion to DB format - doing it
455
      !empty($property_data[P_FUNC_OUTPUT]) && is_callable($property_data[P_FUNC_OUTPUT])
456
        ? $value = call_user_func($property_data[P_FUNC_OUTPUT], $value) : false;
457
      !empty($property_data[P_METHOD_OUTPUT]) && is_callable(array($this, $property_data[P_METHOD_OUTPUT]))
458
        ? $value = call_user_func(array($this, $property_data[P_METHOD_OUTPUT]), $value) : false;
459
460
      $array[$property_data[P_DB_FIELD]] = $value;
461
    }
462
463
    return $array;
464
  }
465
466
  /**
467
   * Check if DB field changed on property change and if it changed - returns name of property which triggered change
468
   *
469
   * @param string $fieldName
470
   *
471
   * @return string|false
472
   */
473
  protected function isFieldChanged($fieldName) {
474
    $isFieldChanged = false;
475
    foreach ($this->propertiesChanged as $propertyName => $cork) {
476
      $propertyScheme = static::$_properties[$propertyName];
477
      if (!empty($propertyScheme[P_DB_FIELDS_LINKED])) {
478
        foreach ($propertyScheme[P_DB_FIELDS_LINKED] as $linkedFieldName) {
479
          if ($linkedFieldName == $fieldName) {
480
            $isFieldChanged = $propertyName;
481
            break 2;
482
          }
483
        }
484
      }
485
      if (!empty($propertyScheme[P_DB_FIELD]) && $propertyScheme[P_DB_FIELD] == $fieldName) {
486
        $isFieldChanged = $propertyName;
487
        break;
488
      }
489
    }
490
491
    return $isFieldChanged;
492
  }
493
494
  /**
495
   * @param array $field_set
496
   *
497
   * @return int|string
498
   */
499
  protected function db_field_set_create(array $field_set) {
500
    !sn_db_field_set_is_safe($field_set) ? $field_set = sn_db_field_set_make_safe($field_set) : false;
501
    sn_db_field_set_safe_flag_clear($field_set);
502
503
    $values = implode(',', $field_set);
504
    $fields = implode(',', array_keys($field_set));
505
506
    $result = 0;
507
    if (classSupernova::db_query("INSERT INTO `{{" . static::$_table . "}}` ({$fields}) VALUES ({$values});")) {
508
      $result = db_insert_id();
509
    }
510
511
    return $result;
512
  }
513
514
  /**
515
   * @param array $field_set
516
   *
517
   * @return array|bool|mysqli_result|null
518
   */
519
  // TODO - UPDATE ONLY CHANGED FIELDS
520
  protected function db_field_update(array $field_set) {
521
    !sn_db_field_set_is_safe($field_set) ? $field_set = sn_db_field_set_make_safe($field_set) : false;
522
    sn_db_field_set_safe_flag_clear($field_set);
523
524
    $set = array();
525
    foreach ($field_set as $fieldName => $value) {
526
      if (!($changedProperty = $this->isFieldChanged($fieldName))) {
527
        continue;
528
      }
529
530
      // TODO - separate sets from adjusts
531
      if (array_key_exists($changedProperty, $this->propertiesAdjusted)) {
532
        $value = "`{$fieldName}` + ($value)"; // braces for negative values
533
      }
534
535
      $set[] = "`{$fieldName}` = $value";
536
    }
537
    $set_string = implode(',', $set);
538
539
//pdump($set_string, get_called_class());
0 ignored issues
show
Unused Code Comprehensibility introduced by
70% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
540
541
    return empty($set_string)
542
      ? true
543
      : classSupernova::db_query("UPDATE `{{" . static::$_table . "}}` SET {$set_string} WHERE `" . static::$_dbIdFieldName . "` = " . $this->_dbId);
544
  }
545
546
}
547