Completed
Push — work-fleets ( 9cd586...f0ff6c )
by SuperNova.WS
04:56
created

DBRow::isNew()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
c 4
b 0
f 0
dl 0
loc 3
rs 10
cc 1
eloc 2
nc 1
nop 0
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 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
  // Some magic ********************************************************************************************************
97
98
  public function __construct() {
99
    static::$db = classSupernova::$db;
100
  }
101
102
  /**
103
   * Getter with support of protected methods
104
   *
105
   * @param $name
106
   *
107
   * @return mixed
108
   */
109
  public function __get($name) {
110
    // Redirecting inaccessible get to __call which will handle the rest
111
    return $this->__call('get' . ucfirst($name), array());
112
  }
113
114
  /**
115
   * Setter with support of protected properties/methods
116
   *
117
   * @param $name
118
   * @param $value
119
   */
120
  // TODO - сеттер должен параллельно изменять значение db_row - for now...
121
  public function __set($name, $value) {
122
    // Redirecting inaccessible set to __call which will handle the rest
123
    $this->__call('set' . ucfirst($name), array($value));
124
  }
125
126
  /**
127
   * Handles getters and setters
128
   *
129
   * @param string $name
130
   * @param array  $arguments
131
   *
132
   * @return mixed
133
   * @throws ExceptionPropertyNotExists
134
   */
135
  public function __call($name, $arguments) {
136
    $left3 = substr($name, 0, 3);
137
    $propertyName = lcfirst(substr($name, 3));
138
139
    // If method is not getter or setter OR property name not exists in $_properties - raising exception
140
    // Descendants can catch this Exception to make own __call magic
141
    if(($left3 != 'get' && $left3 != 'set') || empty(static::$_properties[$propertyName])) {
142
      throw new ExceptionPropertyNotExists('Property ' . $propertyName . ' not exists when calling getter/setter ' . get_called_class() . '::' . $name, ERR_ERROR);
143
    }
144
145
    // TODO check for read-only
146
147
    if($left3 == 'set') {
148
      if(!empty($this->propertiesAdjusted[$propertyName])) {
149
        throw new PropertyAccessException('Property ' . $propertyName . ' already was adjusted so no SET is possible until dbSave in ' . get_called_class() . '::' . $name, ERR_ERROR);
150
      }
151
      $this->propertiesChanged[$propertyName] = 1;
152
    }
153
154
    // Now deciding - will we call a protected setter or will we work with protected property
155
156
    // If method exists - just calling it
157
    if(method_exists($this, $name)) {
158
      return call_user_func_array(array($this, $name), $arguments);
159
    }
160
    // No getter/setter exists - works directly with protected property
161
162
    // Is it getter?
163
    if($left3 === 'get') {
164
      return $this->{'_' . $propertyName};
165
    }
166
167
    // Not getter? Then it's setter
168
    $this->{'_' . $propertyName} = $arguments[0];
169
170
    return null;
171
  }
172
173
  // IDBrow Implementation *********************************************************************************************
174
175
  /**
176
   * Loading object from DB by primary ID
177
   *
178
   * @param int  $dbId
179
   * @param bool $lockSkip
180
   *
181
   * @return
182
   */
183
  public function dbLoad($dbId, $lockSkip = false) {
184
    $dbId = idval($dbId);
185
    if($dbId <= 0) {
186
      classSupernova::$debug->error(get_called_class() . '::dbLoad $dbId not positive = ' . $dbId);
187
188
      return;
189
    }
190
191
    $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...
192
    $this->lockSkip = $lockSkip;
193
    // TODO - Use classSupernova::$db_records_locked
194
    if(false && !$lockSkip && sn_db_transaction_check(false)) {
195
      $this->dbGetLockById($this->_dbId);
196
    }
197
198
    $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...
199
    if(empty($db_row)) {
200
      return;
201
    }
202
203
    $this->dbRowParse($db_row);
204
    $this->lockSkip = false;
205
  }
206
207
  /**
208
   * Lock all fields that belongs to operation
209
   *
210
   * @param int $dbId
211
   *
212
   * @return
213
   * param DBLock $dbRow - Object that accumulates locks
214
   *
215
   */
216
  abstract public function dbGetLockById($dbId);
217
218
  /**
219
   * Saving object to DB
220
   * This is meta-method:
221
   * - if object is new - then it inserted to DB;
222
   * - if object is empty - it deleted from DB;
223
   * - otherwise object is updated in DB;
224
   */
225
  // TODO - perform operations only if properties was changed
226
  public function dbSave() {
227
    if($this->isNew()) {
228
      // No DB_ID - new unit
229
      if($this->isEmpty()) {
230
        classSupernova::$debug->error(__FILE__ . ':' . __LINE__ . ' - object is empty on ' . get_called_class() . '::dbSave');
231
      }
232
      $this->dbInsert();
233
    } else {
234
      // DB_ID is present
235
      if($this->isEmpty()) {
236
        $this->dbDelete();
237
      } else {
238
        if(!sn_db_transaction_check(false)) {
239
          classSupernova::$debug->error(__FILE__ . ':' . __LINE__ . ' - transaction should always be started on ' . get_called_class() . '::dbUpdate');
240
        }
241
        $this->dbUpdate();
242
      }
243
    }
244
245
    if(!empty($this->triggerDbOperationOn)) {
246
      foreach($this->triggerDbOperationOn as $item) {
247
        $item->dbSave();
248
      }
249
    }
250
251
    $this->propertiesChanged = array();
252
    $this->propertiesAdjusted = array();
253
  }
254
255
256
257
  // CRUD **************************************************************************************************************
258
259
  /**
260
   * Inserts record to DB
261
   *
262
   * @return int|string
263
   */
264
  // TODO - protected
265
  public function dbInsert() {
266
    if(!$this->isNew()) {
267
      classSupernova::$debug->error(__FILE__ . ':' . __LINE__ . ' - record db_id is not empty on ' . get_called_class() . '::dbInsert');
268
    }
269
    $this->_dbId = $this->db_field_set_create($this->dbMakeFieldSet());
270
271
    if(empty($this->_dbId)) {
272
      classSupernova::$debug->error(__FILE__ . ':' . __LINE__ . ' - error saving record ' . get_called_class() . '::dbInsert');
273
    }
274
275
    return $this->_dbId;
276
  }
277
278
  /**
279
   * Updates record in DB
280
   */
281
  // TODO - protected
282
  public function dbUpdate() {
283
    // TODO - Update
284
    if($this->isNew()) {
285
      classSupernova::$debug->error(__FILE__ . ':' . __LINE__ . ' - unit db_id is empty on dbUpdate');
286
    }
287
    $this->db_field_update($this->dbMakeFieldSet(true));
288
  }
289
290
  /**
291
   * Deletes record from DB
292
   */
293
  // TODO - protected
294
  public function dbDelete() {
295
    if($this->isNew()) {
296
      classSupernova::$debug->error(__FILE__ . ':' . __LINE__ . ' - unit db_id is empty on dbDelete');
297
    }
298
    doquery("DELETE FROM {{" . static::$_table . "}} WHERE `" . static::$_dbIdFieldName . "` = " . $this->_dbId);
299
    $this->_dbId = 0;
300
    // Обо всём остальном должен позаботиться контейнер
301
  }
302
303
  /**
304
   * Является ли запись новой - т.е. не имеет своей записи в БД
305
   *
306
   * @return bool
307
   */
308
  public function isNew() {
309
    return $this->_dbId == 0;
310
  }
311
312
  /**
313
   * Является ли запись пустой - т.е. при исполнении _dbSave должен быть удалён
314
   *
315
   * @return bool
316
   */
317
  abstract public function isEmpty();
318
319
  // Other Methods *****************************************************************************************************
320
321
//  /**
322
//   * Resets object to zero state
323
//   * @see DBRow::dbLoad()
324
//   *
325
//   * @return void
326
//   */
327
//  protected function _reset() {
328
//    $this->dbRowParse(array());
329
//  }
330
331
  /**
332
   * Парсит запись из БД в поля объекта
333
   *
334
   * @param array $db_row
335
   */
336
  public function dbRowParse(array $db_row) {
337
    foreach(static::$_properties as $property_name => &$property_data) {
338
      // Advanced values extraction procedure. Should be used when at least one of following rules is matched:
339
      // - one field should translate to several properties;
340
      // - one property should be filled according to several fields;
341
      // - property filling requires some lookup in object values;
342 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...
343
        call_user_func_array(array($this, $property_data[P_METHOD_EXTRACT]), array(&$db_row));
344
        continue;
345
      }
346
347
      // If property is read-only - doing nothing
348
      if(!empty($property_data[P_READ_ONLY])) {
349
        continue;
350
      }
351
352
      // Getting field value as base only if $_properties has 1-to-1 relation to object property
353
      $value = !empty($property_data[P_DB_FIELD]) && isset($db_row[$property_data[P_DB_FIELD]]) ? $db_row[$property_data[P_DB_FIELD]] : null;
354
355
      // Making format conversion from string ($db_row default type) to property type
356
      !empty($property_data[P_FUNC_INPUT]) && is_callable($property_data[P_FUNC_INPUT]) ? $value = call_user_func($property_data[P_FUNC_INPUT], $value) : false;
357
358
      // If there is setter for this field - using it. Setters is always a methods of $THIS
359
      if(!empty($property_data[P_METHOD_SET]) && is_callable(array($this, $property_data[P_METHOD_SET]))) {
360
        call_user_func(array($this, $property_data[P_METHOD_SET]), $value);
361
      } else {
362
        $this->{$property_name} = $value;
363
      }
364
    }
365
  }
366
367
  /**
368
   * Делает из свойств класса массив db_field_name => db_field_value
369
   *
370
   * @return array
371
   */
372
  protected function dbMakeFieldSet($isUpdate = false) {
373
    $array = array();
374
375
    foreach(static::$_properties as $property_name => &$property_data) {
376
      // TODO - on isUpdate add only changed/adjusted properties
377
378 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...
379
        call_user_func_array(array($this, $property_data[P_METHOD_INJECT]), array(&$array));
380
        continue;
381
      }
382
383
      // Skipping properties which have no corresponding field in DB
384
      if(empty($property_data[P_DB_FIELD])) {
385
        continue;
386
      }
387
388
      // Checking - is property was adjusted or changed
389
      if($isUpdate && array_key_exists($property_name, $this->propertiesAdjusted)) {
390
        // For adjusted property - take value from propertiesAdjusted array
391
        // TODO - differ how treated conversion to string for changed and adjusted properties
392
        $value = $this->propertiesAdjusted[$property_name];
393
      } else {
394
        // Getting property value. Optionally getter is invoked by __get()
395
        $value = $this->{$property_name};
396
      }
397
398
      // If need some conversion to DB format - doing it
399
      !empty($property_data[P_FUNC_OUTPUT]) && is_callable($property_data[P_FUNC_OUTPUT])
400
        ? $value = call_user_func($property_data[P_FUNC_OUTPUT], $value) : false;
401
      !empty($property_data[P_METHOD_OUTPUT]) && is_callable(array($this, $property_data[P_METHOD_OUTPUT]))
402
        ? $value = call_user_func(array($this, $property_data[P_METHOD_OUTPUT]), $value) : false;
403
404
      $array[$property_data[P_DB_FIELD]] = $value;
405
    }
406
407
    return $array;
408
  }
409
410
  /**
411
   * Check if DB field changed on property change and if it changed - returns name of property which triggered change
412
   *
413
   * @param string $fieldName
414
   *
415
   * @return string|false
416
   */
417
  protected function isFieldChanged($fieldName) {
418
    $isFieldChanged = false;
419
    foreach($this->propertiesChanged as $propertyName => $cork) {
420
      $propertyScheme = static::$_properties[$propertyName];
421
      if(!empty($propertyScheme[P_DB_FIELDS_LINKED])) {
422
        foreach($propertyScheme[P_DB_FIELDS_LINKED] as $linkedFieldName) {
423
          if($linkedFieldName == $fieldName) {
424
            $isFieldChanged = $propertyName;
425
            break 2;
426
          }
427
        }
428
      }
429
      if(!empty($propertyScheme[P_DB_FIELD]) && $propertyScheme[P_DB_FIELD] == $fieldName) {
430
        $isFieldChanged = $propertyName;
431
        break;
432
      }
433
    }
434
435
    return $isFieldChanged;
436
  }
437
438
  /**
439
   * @param array $field_set
440
   *
441
   * @return int|string
442
   */
443
  protected function db_field_set_create(array $field_set) {
444
    !sn_db_field_set_is_safe($field_set) ? $field_set = sn_db_field_set_make_safe($field_set) : false;
445
    sn_db_field_set_safe_flag_clear($field_set);
446
447
    $values = implode(',', $field_set);
448
    $fields = implode(',', array_keys($field_set));
449
450
    $result = 0;
451
    if(classSupernova::db_query("INSERT INTO `{{" . static::$_table . "}}` ({$fields}) VALUES ({$values});")) {
452
      $result = db_insert_id();
453
    }
454
455
    return $result;
456
  }
457
458
  /**
459
   * @param array $field_set
460
   *
461
   * @return array|bool|mysqli_result|null
462
   */
463
  // TODO - UPDATE ONLY CHANGED FIELDS
464
  protected function db_field_update(array $field_set) {
465
    !sn_db_field_set_is_safe($field_set) ? $field_set = sn_db_field_set_make_safe($field_set) : false;
466
    sn_db_field_set_safe_flag_clear($field_set);
467
468
    $set = array();
469
    foreach($field_set as $fieldName => $value) {
470
      if(!($changedProperty = $this->isFieldChanged($fieldName))) {
471
        continue;
472
      }
473
474
      // TODO - separate sets from adjusts
475
      if(array_key_exists($changedProperty, $this->propertiesAdjusted)) {
476
        $value = "`{$fieldName}` + ($value)"; // braces for negative values
477
      }
478
479
      $set[] = "`{$fieldName}` = $value";
480
    }
481
    $set_string = implode(',', $set);
482
483
//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...
484
485
    return empty($set_string)
486
      ? true
487
      : classSupernova::db_query("UPDATE `{{" . static::$_table . "}}` SET {$set_string} WHERE `" . static::$_dbIdFieldName . "` = " . $this->_dbId);
488
  }
489
490
}
491