Completed
Push — work-fleets ( ea0fb4...874fb8 )
by SuperNova.WS
06:59
created

DBRow::dbMakeFieldUpdate()   C

Complexity

Conditions 8
Paths 19

Size

Total Lines 30
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 72

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 8
eloc 15
c 1
b 0
f 0
nc 19
nop 0
dl 0
loc 30
rs 5.3846
ccs 0
cts 0
cp 0
crap 72
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;
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 = 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());
0 ignored issues
show
Deprecated Code introduced by
The method DBRow::dbMakeFieldUpdate() has been deprecated.

This method has been deprecated.

Loading history...
Deprecated Code introduced by
The method DBRow::db_field_update() has been deprecated.

This method has been deprecated.

Loading history...
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));
0 ignored issues
show
Bug introduced by
The method doDeleteRowWhere does only exist in db_mysql, but not in Closure.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
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]))) {
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...
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]))) {
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...
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 - ОШИБКА!!!!!!!!!!!!!!!!
0 ignored issues
show
Unused Code Comprehensibility introduced by
73% 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...
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