Completed
Push — work-fleets ( bf14b2...c62b27 )
by SuperNova.WS
06:15
created

DBRow   C

Complexity

Total Complexity 75

Size/Duplication

Total Lines 446
Duplicated Lines 1.79 %

Coupling/Cohesion

Components 1
Dependencies 6

Test Coverage

Coverage 17.73%

Importance

Changes 7
Bugs 0 Features 0
Metric Value
c 7
b 0
f 0
dl 8
loc 446
rs 5.5056
ccs 25
cts 141
cp 0.1773
wmc 75
lcom 1
cbo 6

20 Methods

Rating   Name   Duplication   Size   Complexity  
B setDb() 0 6 5
A getDb() 0 3 1
A setTable() 0 3 1
A getTable() 0 3 1
A setIdFieldName() 0 3 1
A getIdFieldName() 0 3 1
A __construct() 0 8 2
C dbSave() 0 28 7
B dbLoad() 0 23 6
dbGetLockById() 0 1 ?
A dbInsert() 0 14 3
A dbUpdate() 0 7 2
A dbDelete() 0 13 2
A isNew() 0 3 1
isEmpty() 0 1 ?
C dbRowParse() 4 30 11
C dbMakeFieldSet() 4 38 11
C dbMakeFieldUpdate() 0 30 8
B isFieldChanged() 0 20 7
B db_field_update() 0 25 5

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like DBRow often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use DBRow, and based on these observations, apply Extract Interface, too.

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->doDeleteRow(
0 ignored issues
show
Bug introduced by
The method doDeleteRow 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
      static::$_table,
268
      array(
269
        static::$_dbIdFieldName => $this->_dbId,
270
      )
271
    );
272
    $this->_dbId = 0;
273
    // Обо всём остальном должен позаботиться контейнер
274
  }
275
276
  /**
277
   * Является ли запись новой - т.е. не имеет своей записи в БД
278
   *
279
   * @return bool
280
   */
281
  public function isNew() {
282
    return $this->_dbId == 0;
283
  }
284
285
  /**
286
   * Является ли запись пустой - т.е. при исполнении _dbSave должен быть удалён
287
   *
288
   * @return bool
289
   */
290
  abstract public function isEmpty();
291
292
  // Other Methods *****************************************************************************************************
293
  /**
294
   * Парсит запись из БД в поля объекта
295
   *
296
   * @param array $db_row
297
   */
298
  public function dbRowParse(array $db_row) {
299
    foreach ($this->_properties as $property_name => &$property_data) {
300
      // Advanced values extraction procedure. Should be used when at least one of following rules is matched:
301
      // - one field should translate to several properties;
302
      // - one property should be filled according to several fields;
303
      // - property filling requires some lookup in object values;
304 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...
305
        call_user_func_array(array($this, $property_data[P_METHOD_EXTRACT]), array(&$db_row));
306
        continue;
307
      }
308
309
      // If property is read-only - doing nothing
310
      if (!empty($property_data[P_READ_ONLY])) {
311
        continue;
312
      }
313
314
      // Getting field value as base only if $_properties has 1-to-1 relation to object property
315
      $value = !empty($property_data[P_DB_FIELD]) && isset($db_row[$property_data[P_DB_FIELD]]) ? $db_row[$property_data[P_DB_FIELD]] : null;
316
317
      // Making format conversion from string ($db_row default type) to property type
318
      !empty($property_data[P_FUNC_INPUT]) && is_callable($property_data[P_FUNC_INPUT]) ? $value = call_user_func($property_data[P_FUNC_INPUT], $value) : false;
319
320
      // If there is setter for this field - using it. Setters is always a methods of $THIS
321
      if (!empty($property_data[P_METHOD_SET]) && is_callable(array($this, $property_data[P_METHOD_SET]))) {
322
        call_user_func(array($this, $property_data[P_METHOD_SET]), $value);
323
      } else {
324
        $this->{$property_name} = $value;
325
      }
326
    }
327
  }
328
329
  /**
330
   * Делает из свойств класса массив db_field_name => db_field_value
331
   *
332
   * @return array
333
   */
334
  protected function dbMakeFieldSet($isUpdate = false) {
335
    $array = array();
336
337
    foreach (static::$_properties as $property_name => &$property_data) {
338
      // TODO - on isUpdate add only changed/adjusted properties
339
340 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...
341
        call_user_func_array(array($this, $property_data[P_METHOD_INJECT]), array(&$array));
342
        continue;
343
      }
344
345
      // Skipping properties which have no corresponding field in DB
346
      if (empty($property_data[P_DB_FIELD])) {
347
        continue;
348
      }
349
350
      // Checking - is property was adjusted or changed
351
      if ($isUpdate && array_key_exists($property_name, $this->propertiesAdjusted)) {
352
        // For adjusted property - take value from propertiesAdjusted array
353
        // TODO - differ how treated conversion to string for changed and adjusted properties
354
        $value = $this->propertiesAdjusted[$property_name];
355
      } else {
356
        // 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...
357
        // Getting property value. Optionally getter is invoked by __get()
358
        $value = $this->{$property_name};
359
      }
360
361
      // If need some conversion to DB format - doing it
362
      !empty($property_data[P_FUNC_OUTPUT]) && is_callable($property_data[P_FUNC_OUTPUT])
363
        ? $value = call_user_func($property_data[P_FUNC_OUTPUT], $value) : false;
364
      !empty($property_data[P_METHOD_OUTPUT]) && is_callable(array($this, $property_data[P_METHOD_OUTPUT]))
365
        ? $value = call_user_func(array($this, $property_data[P_METHOD_OUTPUT]), $value) : false;
366
367
      $array[$property_data[P_DB_FIELD]] = $value;
368
    }
369
370
    return $array;
371
  }
372
373
  /**
374
   * Делает из свойств класса массив db_field_name => db_field_value
375
   * @return array
376
   * @deprecated
377
   */
378
  protected function dbMakeFieldUpdate() {
379
    $array = array();
380
    foreach (static::$_properties as $property_name => &$property_data) {
381
      // TODO - on isUpdate add only changed/adjusted properties
382
      // Skipping properties which have no corresponding field in DB
383
      if (empty($property_data[P_DB_FIELD])) {
384
        continue;
385
      }
386
387
      // Checking - is property was adjusted or changed
388
      if (array_key_exists($property_name, $this->propertiesAdjusted)) {
389
        // For adjusted property - take value from propertiesAdjusted array
390
        // TODO - differ how treated conversion to string for changed and adjusted properties
391
        $value = $this->propertiesAdjusted[$property_name];
392
      } else {
393
        // Skipping not updated properties
394
        continue;
395
      }
396
397
      // If need some conversion to DB format - doing it
398
      !empty($property_data[P_FUNC_OUTPUT]) && is_callable($property_data[P_FUNC_OUTPUT])
399
        ? $value = call_user_func($property_data[P_FUNC_OUTPUT], $value) : false;
400
      !empty($property_data[P_METHOD_OUTPUT]) && is_callable(array($this, $property_data[P_METHOD_OUTPUT]))
401
        ? $value = call_user_func(array($this, $property_data[P_METHOD_OUTPUT]), $value) : false;
402
403
      $array[$property_data[P_DB_FIELD]] = $value;
404
    }
405
406
    return $array;
407
  }
408
409
  /**
410
   * Check if DB field changed on property change and if it changed - returns name of property which triggered change
411
   *
412
   * @param string $fieldName
413
   *
414
   * @return string|false
415
   */
416
  protected function isFieldChanged($fieldName) {
417
    $isFieldChanged = false;
418
    foreach ($this->propertiesChanged as $propertyName => $cork) {
419
      $propertyScheme = static::$_properties[$propertyName];
420
      if (!empty($propertyScheme[P_DB_FIELDS_LINKED])) {
421
        foreach ($propertyScheme[P_DB_FIELDS_LINKED] as $linkedFieldName) {
422
          if ($linkedFieldName == $fieldName) {
423
            $isFieldChanged = $propertyName;
424
            break 2;
425
          }
426
        }
427
      }
428
      if (!empty($propertyScheme[P_DB_FIELD]) && $propertyScheme[P_DB_FIELD] == $fieldName) {
429
        $isFieldChanged = $propertyName;
430
        break;
431
      }
432
    }
433
434
    return $isFieldChanged;
435
  }
436
437
  /**
438
   * @param array $field_set
439
   *
440
   * @return array|bool|mysqli_result|null
441
   * @deprecated
442
   */
443
  protected function db_field_update(array $field_set) {
444
    $set = array();
445
    foreach ($field_set as $fieldName => $value) {
446
      if (!($changedProperty = $this->isFieldChanged($fieldName))) {
447
        continue;
448
      }
449
450
      if (array_key_exists($changedProperty, $this->propertiesAdjusted)) {
451
        $set[$fieldName] = $value;
452
      }
453
    }
454
455
    if(empty($set)) {
456
      $theResult = true;
457
    } else {
458
      $theResult = classSupernova::$db->doUpdateRowAdjust(
459
        static::$_table,
460
        array(),
461
        $field_set,
462
        array(
463
          static::$_dbIdFieldName => $this->_dbId,
464
        ));
465
    }
466
    return $theResult;
467
  }
468
469
}
470