Completed
Push — work-fleets ( a017ed...86976e )
by SuperNova.WS
06:16
created

DBRow::__set()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 1
eloc 2
nc 1
nop 2
dl 0
loc 4
rs 10
c 3
b 0
f 0
ccs 0
cts 3
cp 0
crap 2
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 PropertyHider 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 static $_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;
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...
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 = 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...
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
    $this->_dbId = $this->db_field_set_create($this->dbMakeFieldSet());
236
237
    if (empty($this->_dbId)) {
238
      classSupernova::$debug->error(__FILE__ . ':' . __LINE__ . ' - error saving record ' . get_called_class() . '::dbInsert');
239
    }
240
241
    return $this->_dbId;
242
  }
243
244
  /**
245
   * Updates record in DB
246
   */
247
  // TODO - protected
248
  public function dbUpdate() {
249
    // TODO - Update
250
    if ($this->isNew()) {
251
      classSupernova::$debug->error(__FILE__ . ':' . __LINE__ . ' - unit db_id is empty on dbUpdate');
252
    }
253
    $this->db_field_update($this->dbMakeFieldSet(true));
254
  }
255
256
  /**
257
   * Deletes record from DB
258
   */
259
  // TODO - protected
260
  public function dbDelete() {
261
    if ($this->isNew()) {
262
      classSupernova::$debug->error(__FILE__ . ':' . __LINE__ . ' - unit db_id is empty on dbDelete');
263
    }
264
    doquery("DELETE FROM {{" . static::$_table . "}} WHERE `" . static::$_dbIdFieldName . "` = " . $this->_dbId);
265
    $this->_dbId = 0;
266
    // Обо всём остальном должен позаботиться контейнер
267
  }
268
269
  /**
270
   * Является ли запись новой - т.е. не имеет своей записи в БД
271
   *
272
   * @return bool
273
   */
274
  public function isNew() {
275
    return $this->_dbId == 0;
276
  }
277
278
  /**
279
   * Является ли запись пустой - т.е. при исполнении _dbSave должен быть удалён
280
   *
281
   * @return bool
282
   */
283
  abstract public function isEmpty();
284
285
  // Other Methods *****************************************************************************************************
286
287
//  /**
288
//   * Resets object to zero state
289
//   * @see DBRow::dbLoad()
290
//   *
291
//   * @return void
292
//   */
293
//  protected function _reset() {
294
//    $this->dbRowParse(array());
295
//  }
296
297
  /**
298
   * Парсит запись из БД в поля объекта
299
   *
300
   * @param array $db_row
301
   */
302
  public function dbRowParse(array $db_row) {
303
    foreach (static::$_properties as $property_name => &$property_data) {
304
      // Advanced values extraction procedure. Should be used when at least one of following rules is matched:
305
      // - one field should translate to several properties;
306
      // - one property should be filled according to several fields;
307
      // - property filling requires some lookup in object values;
308 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...
309
        call_user_func_array(array($this, $property_data[P_METHOD_EXTRACT]), array(&$db_row));
310
        continue;
311
      }
312
313
      // If property is read-only - doing nothing
314
      if (!empty($property_data[P_READ_ONLY])) {
315
        continue;
316
      }
317
318
      // Getting field value as base only if $_properties has 1-to-1 relation to object property
319
      $value = !empty($property_data[P_DB_FIELD]) && isset($db_row[$property_data[P_DB_FIELD]]) ? $db_row[$property_data[P_DB_FIELD]] : null;
320
321
      // Making format conversion from string ($db_row default type) to property type
322
      !empty($property_data[P_FUNC_INPUT]) && is_callable($property_data[P_FUNC_INPUT]) ? $value = call_user_func($property_data[P_FUNC_INPUT], $value) : false;
323
324
      // If there is setter for this field - using it. Setters is always a methods of $THIS
325
      if (!empty($property_data[P_METHOD_SET]) && is_callable(array($this, $property_data[P_METHOD_SET]))) {
326
        call_user_func(array($this, $property_data[P_METHOD_SET]), $value);
327
      } else {
328
        $this->{$property_name} = $value;
329
      }
330
    }
331
  }
332
333
  /**
334
   * Делает из свойств класса массив db_field_name => db_field_value
335
   *
336
   * @return array
337
   */
338
  protected function dbMakeFieldSet($isUpdate = false) {
339
    $array = array();
340
341
    foreach (static::$_properties as $property_name => &$property_data) {
342
      // TODO - on isUpdate add only changed/adjusted properties
343
344 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...
345
        call_user_func_array(array($this, $property_data[P_METHOD_INJECT]), array(&$array));
346
        continue;
347
      }
348
349
      // Skipping properties which have no corresponding field in DB
350
      if (empty($property_data[P_DB_FIELD])) {
351
        continue;
352
      }
353
354
      // Checking - is property was adjusted or changed
355
      if ($isUpdate && array_key_exists($property_name, $this->propertiesAdjusted)) {
356
        // For adjusted property - take value from propertiesAdjusted array
357
        // TODO - differ how treated conversion to string for changed and adjusted properties
358
        $value = $this->propertiesAdjusted[$property_name];
359
      } else {
360
        // Getting property value. Optionally getter is invoked by __get()
361
        $value = $this->{$property_name};
362
      }
363
364
      // If need some conversion to DB format - doing it
365
      !empty($property_data[P_FUNC_OUTPUT]) && is_callable($property_data[P_FUNC_OUTPUT])
366
        ? $value = call_user_func($property_data[P_FUNC_OUTPUT], $value) : false;
367
      !empty($property_data[P_METHOD_OUTPUT]) && is_callable(array($this, $property_data[P_METHOD_OUTPUT]))
368
        ? $value = call_user_func(array($this, $property_data[P_METHOD_OUTPUT]), $value) : false;
369
370
      $array[$property_data[P_DB_FIELD]] = $value;
371
    }
372
373
    return $array;
374
  }
375
376
  /**
377
   * Check if DB field changed on property change and if it changed - returns name of property which triggered change
378
   *
379
   * @param string $fieldName
380
   *
381
   * @return string|false
382
   */
383
  protected function isFieldChanged($fieldName) {
384
    $isFieldChanged = false;
385
    foreach ($this->propertiesChanged as $propertyName => $cork) {
386
      $propertyScheme = static::$_properties[$propertyName];
387
      if (!empty($propertyScheme[P_DB_FIELDS_LINKED])) {
388
        foreach ($propertyScheme[P_DB_FIELDS_LINKED] as $linkedFieldName) {
389
          if ($linkedFieldName == $fieldName) {
390
            $isFieldChanged = $propertyName;
391
            break 2;
392
          }
393
        }
394
      }
395
      if (!empty($propertyScheme[P_DB_FIELD]) && $propertyScheme[P_DB_FIELD] == $fieldName) {
396
        $isFieldChanged = $propertyName;
397
        break;
398
      }
399
    }
400
401
    return $isFieldChanged;
402
  }
403
404
  /**
405
   * @param array $field_set
406
   *
407
   * @return int|string
408
   */
409
  protected function db_field_set_create(array $field_set) {
410
    !sn_db_field_set_is_safe($field_set) ? $field_set = sn_db_field_set_make_safe($field_set) : false;
411
    sn_db_field_set_safe_flag_clear($field_set);
412
413
    $values = implode(',', $field_set);
414
    $fields = implode(',', array_keys($field_set));
415
416
    $result = 0;
417
    if (classSupernova::db_query("INSERT INTO `{{" . static::$_table . "}}` ({$fields}) VALUES ({$values});")) {
418
      $result = db_insert_id();
419
    }
420
421
    return $result;
422
  }
423
424
  /**
425
   * @param array $field_set
426
   *
427
   * @return array|bool|mysqli_result|null
428
   */
429
  // TODO - UPDATE ONLY CHANGED FIELDS
430
  protected function db_field_update(array $field_set) {
431
    !sn_db_field_set_is_safe($field_set) ? $field_set = sn_db_field_set_make_safe($field_set) : false;
432
    sn_db_field_set_safe_flag_clear($field_set);
433
434
    $set = array();
435
    foreach ($field_set as $fieldName => $value) {
436
      if (!($changedProperty = $this->isFieldChanged($fieldName))) {
437
        continue;
438
      }
439
440
      // TODO - separate sets from adjusts
441
      if (array_key_exists($changedProperty, $this->propertiesAdjusted)) {
442
        $value = "`{$fieldName}` + ($value)"; // braces for negative values
443
      }
444
445
      $set[] = "`{$fieldName}` = $value";
446
    }
447
    $set_string = implode(',', $set);
448
449
//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...
450
451
    return empty($set_string)
452
      ? true
453
      : classSupernova::db_query("UPDATE `{{" . static::$_table . "}}` SET {$set_string} WHERE `" . static::$_dbIdFieldName . "` = " . $this->_dbId);
454
  }
455
456
}
457