Completed
Push — trunk ( 8dcff2...338765 )
by SuperNova.WS
04:11
created

ActiveRecordAbstract   F

Complexity

Total Complexity 75

Size/Duplication

Total Lines 531
Duplicated Lines 0 %

Test Coverage

Coverage 68.64%

Importance

Changes 0
Metric Value
dl 0
loc 531
ccs 116
cts 169
cp 0.6864
rs 2.3076
c 0
b 0
f 0
wmc 75

32 Methods

Rating   Name   Duplication   Size   Complexity  
A calcTableName() 0 8 2
A build() 0 6 3
A __set() 0 3 1
A getDefault() 0 7 2
A setDb() 0 2 1
A __get() 0 2 2
A haveProperty() 0 2 1
B defaultValues() 0 17 6
A dbPrepareQuery() 0 2 1
B translateNames() 0 16 5
A getPropertyName() 0 8 3
A dbGetFieldsDescription() 0 2 1
A findRecordsAll() 0 2 2
A shieldName() 0 4 2
A haveTranslationToProperty() 0 2 1
A fromProperties() 0 6 2
A findFirst() 0 11 3
A insert() 0 18 4
A tableName() 0 4 2
A find() 0 13 3
A db() 0 4 2
A ptlArray() 0 7 2
B getFieldName() 0 17 5
A findRecordFirst() 0 5 3
C fromRecordList() 0 24 8
A buildEvenEmpty() 0 8 2
A setForUpdate() 0 2 1
A findAll() 0 2 1
A __construct() 0 3 1
A dbInsert() 0 5 1
A haveField() 0 2 1
A fromFields() 0 2 1

How to fix   Complexity   

Complex Class

Complex classes like ActiveRecordAbstract 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.

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 ActiveRecordAbstract, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * Created by Gorlum 12.07.2017 10:27
4
 */
5
6
namespace DBAL;
7
8
use Common\AccessLogged;
9
use Core\GlobalContainer;
10
use Common\Exceptions\DbalFieldInvalidException;
11
12
/**
13
 * Class ActiveRecordAbstract
14
 * @package DBAL
15
 *
16
 * Adds some DB functionality
17
 */
18
abstract class ActiveRecordAbstract extends AccessLogged {
19
  const FIELDS_TO_PROPERTIES = true;
20
  const PROPERTIES_TO_FIELDS = false;
21
22
  const IGNORE_PREFIX = 'Record';
23
24
  /**
25
   * @var \DBAL\db_mysql $db
26
   */
27
  protected static $db;
28
  /**
29
   * Table name for current Active Record
30
   *
31
   * Can be predefined in class or calculated in run-time
32
   *
33
   * ALWAYS SHOULD BE OVERRIDEN IN CHILD CLASSES!
34
   *
35
   * @var string $_tableName
36
   */
37
  protected static $_tableName = '';
38
  /**
39
   * Field name translations to property names
40
   *
41
   * @var string[] $_fieldsToProperties
42
   */
43
  protected static $_fieldsToProperties = [];
44
45
  /**
46
   * @var bool $_forUpdate
47
   */
48
  protected static $_forUpdate = DbQuery::DB_SHARED;
49
50
  // AR's service fields
51
  /**
52
   * Is this field - new field?
53
   *
54
   * @var bool $_isNew
55
   */
56
  protected $_isNew = true;
57
58
  protected $_isDeleted = false;
59
60
61
  /**
62
   * Get table name
63
   *
64
   * @return string
65
   */
66 1
  public static function tableName() {
67 1
    empty(static::$_tableName) ? static::$_tableName = static::calcTableName() : false;
68
69 1
    return static::$_tableName;
70
  }
71
72
  /**
73
   * @param \DBAL\db_mysql $db
74
   */
75 1
  public static function setDb(\DBAL\db_mysql $db) {
76 1
    static::$db = $db;
77 1
  }
78
79
  /**
80
   * Get DB
81
   *
82
   * @return \DBAL\db_mysql
83
   */
84 1
  public static function db() {
85 1
    empty(static::$db) ? static::$db = \SN::services()->db : false;
86
87 1
    return static::$db;
88
  }
89
90
  /**
91
   * Instate ActiveRecord from array of field values - even if it is empty
92
   *
93
   * @param array $properties List of field values [$propertyName => $propertyValue]
94
   *
95
   * @return static
96
   */
97 1
  public static function buildEvenEmpty(array $properties = []) {
98 1
    $record = new static();
99 1
    if (!empty($properties)) {
100 1
      $record->clear();
101 1
      $record->fromProperties($properties);
102 1
    }
103
104 1
    return $record;
105
  }
106
107
  /**
108
   * Instate ActiveRecord from array of field values
109
   *
110
   * @param array $properties List of field values [$propertyName => $propertyValue]
111
   *
112
   * @return static|bool
113
   */
114 1
  public static function build(array $properties = []) {
115 1
    if (!is_array($properties) || empty($properties)) {
0 ignored issues
show
introduced by
The condition is_array($properties) is always true.
Loading history...
116 1
      return false;
117
    }
118
119 1
    return static::buildEvenEmpty($properties);
120
  }
121
122
  /**
123
   * Set flag "for update"
124
   *
125
   * @param bool $forUpdate - DbQuery::DB_FOR_UPDATE | DbQuery::DB_SHARED
126
   */
127
  public static function setForUpdate($forUpdate = DbQuery::DB_FOR_UPDATE) {
128
    static::$_forUpdate = $forUpdate;
129
  }
130
131
  /**
132
   * Finds records by property - equivalent of SELECT ... WHERE ... AND ...
133
   *
134
   * @param array $propertyFilter - [$propertyName => $propertyValue]. Pass [] to find all records in table
135
   *
136
   * @return bool|\mysqli_result
137
   */
138
  public static function find($propertyFilter) {
139
    $dbq = static::dbPrepareQuery();
140
    if (!empty($propertyFilter)) {
141
      $dbq->setWhereArray(static::translateNames($propertyFilter, static::PROPERTIES_TO_FIELDS));
142
    }
143
144
    if (static::$_forUpdate == DbQuery::DB_FOR_UPDATE) {
145
      $dbq->setForUpdate();
146
      // Restoring default forUpdate state
147
      static::$_forUpdate = DbQuery::DB_SHARED;
148
    }
149
150
    return $dbq->doSelect();
151
  }
152
153
  /**
154
   * Gets first record by $where
155
   *
156
   * @param array $propertyFilter - [$propertyName => $propertyValue]. Pass [] to find all records in table
157
   *
158
   * @return string[] - [$field_name => $field_value]
159
   */
160
  public static function findRecordFirst($propertyFilter) {
161
    $result = empty($mysqliResult = static::find($propertyFilter)) ? [] : $mysqliResult->fetch_assoc();
162
163
    // Secondary check - for fetch_assoc() result
164
    return empty($result) ? [] : $result;
165
  }
166
167
  /**
168
   * Gets all records by $where
169
   *
170
   * @param array $propertyFilter - [$propertyName => $propertyValue]. Pass [] to find all records in table
171
   *
172
   * @return array[] - [(int) => [$field_name => $field_value]]
173
   */
174
  public static function findRecordsAll($propertyFilter) {
175
    return empty($mysqliResult = static::find($propertyFilter)) ? [] : $mysqliResult->fetch_all(MYSQLI_ASSOC);
176
  }
177
178
  /**
179
   * Gets first ActiveRecord by $where
180
   *
181
   * @param array $propertyFilter - [$propertyName => $propertyValue]. Pass [] to find all records in table
182
   *
183
   * @return static|bool
184
   */
185
  public static function findFirst($propertyFilter) {
186
    $record = false;
187
    $fields = static::findRecordFirst($propertyFilter);
188
    if (!empty($fields)) {
189
      $record = static::build(static::translateNames($fields, self::FIELDS_TO_PROPERTIES));
190
      if (is_object($record)) {
191
        $record->_isNew = false;
192
      }
193
    }
194
195
    return $record;
196
  }
197
198
  /**
199
   * Gets all ActiveRecords by $where
200
   *
201
   * @param array $propertyFilter - [$propertyName => $propertyValue]. Pass [] to find all records in table
202
   *
203
   * @return array|static[] - [(int) => static]
204
   */
205
  public static function findAll($propertyFilter) {
206
    return static::fromRecordList(static::findRecordsAll($propertyFilter));
207
  }
208
209
210
  /**
211
   * ActiveRecord constructor.
212
   *
213
   * @param \Core\GlobalContainer|null $services
214
   */
215 1
  public function __construct(GlobalContainer $services = null) {
216 1
    parent::__construct($services);
217 1
    $this->defaultValues();
218 1
  }
219
220
  /**
221
   * @return bool
222
   */
223
  // TODO - do a check that all fields present in stored data. I.e. no empty fields with no defaults
224
  public function insert() {
225
    if ($this->isEmpty()) {
226
      return false;
227
    }
228
    if (!$this->_isNew) {
229
      return false;
230
    }
231
232
    $this->defaultValues();
233
234
    if (!$this->dbInsert()) {
235
      return false;
236
    }
237
238
    $this->acceptChanges();
239
    $this->_isNew = false;
240
241
    return true;
242
  }
243
244
  /**
245
   * Normalize array
246
   *
247
   * Basically - uppercase all field names to make it use in PTL
248
   * Can be override by descendants to make more convinient, clear and robust indexes
249
   *
250
   * @return array
251
   */
252
  public function ptlArray() {
253
    $result = [];
254
    foreach ($this->values as $key => $value) {
255
      $result[strtoupper(\HelperString::camelToUnderscore($key))] = $value;
256
    }
257
258
    return $result;
259
  }
260
261
  /**
262
   * Get default value for field
263
   *
264
   * @param string $propertyName
265
   *
266
   * @return mixed
267
   */
268 2
  public function getDefault($propertyName) {
269 2
    $fieldName = self::getFieldName($propertyName);
270
271
    return
272 2
      isset(static::dbGetFieldsDescription()[$fieldName]->Default)
273 2
        ? static::dbGetFieldsDescription()[$fieldName]->Default
274 2
        : null;
275
  }
276
277
  /**
278
   * Returns default value if original value not set
279
   *
280
   * @param string $propertyName
281
   *
282
   * @return mixed
283
   */
284 1
  public function __get($propertyName) {
285 1
    return $this->__isset($propertyName) ? parent::__get($propertyName) : $this->getDefault($propertyName);
286
  }
287
288 1
  public function __set($propertyName, $value) {
289 1
    $this->shieldName($propertyName);
290 1
    parent::__set($propertyName, $value);
291 1
  }
292
293
294
  /**
295
   * Calculate table name by class name and fills internal property
296
   *
297
   * Namespaces does not count - only class name taken into account
298
   * Class name converted from CamelCase to underscore_name
299
   * Prefix "Record" is ignored - can be override
300
   *
301
   * Examples:
302
   * Class \Namespace\ClassName will map to table `class_name`
303
   * Class \NameSpace\RecordLongName will map to table `long_name`
304
   *
305
   * Can be overriden to provide different name
306
   *
307
   * @return string - table name in DB
308
   *
309
   */
310 1
  protected static function calcTableName() {
311 1
    $temp = explode('\\', get_called_class());
312 1
    $className = end($temp);
313 1
    if (strpos($className, static::IGNORE_PREFIX) === 0) {
314 1
      $className = substr($className, strlen(static::IGNORE_PREFIX));
315 1
    }
316
317 1
    return \HelperString::camelToUnderscore($className);
318
  }
319
320
  /**
321
   * Get table fields description
322
   *
323
   * @return DbFieldDescription[]
324
   */
325
  protected static function dbGetFieldsDescription() {
326
    return static::db()->schema()->getTableSchema(static::tableName())->fieldsObject;
327
  }
328
329
  /**
330
   * Prepares DbQuery object for further operations
331
   *
332
   * @return DbQuery
333
   */
334 1
  protected static function dbPrepareQuery() {
335 1
    return DbQuery::build(static::db())->setTable(static::tableName());
336
  }
337
338
  /**
339
   * Is there translation for this field name to property name
340
   *
341
   * @param string $fieldName
342
   *
343
   * @return bool
344
   */
345 1
  protected static function haveTranslationToProperty($fieldName) {
346 1
    return !empty(static::$_fieldsToProperties[$fieldName]);
347
  }
348
349
  /**
350
   * Check if field exists
351
   *
352
   * @param string $fieldName
353
   *
354
   * @return bool
355
   */
356 1
  protected static function haveField($fieldName) {
357 1
    return !empty(static::dbGetFieldsDescription()[$fieldName]);
358
  }
359
360
  /**
361
   * Returns field name by property name
362
   *
363
   * @param string $propertyName
364
   *
365
   * @return string Field name for property or '' if not field
366
   */
367 4
  protected static function getFieldName($propertyName) {
368 4
    $fieldName = array_search($propertyName, static::$_fieldsToProperties);
369
    if (
370
      // No translation found for property name
371
      $fieldName === false
372 4
      &&
373
      // AND Property name is not among translatable field names
374 3
      !static::haveTranslationToProperty($propertyName)
375 4
      &&
376
      // AND field name exists
377 2
      static::haveField($propertyName)
378 4
    ) {
379
      // Returning property name as field name
380 1
      $fieldName = $propertyName;
381 1
    }
382
383 4
    return $fieldName === false ? '' : $fieldName;
384
  }
385
386
  /**
387
   * Does property exists?
388
   *
389
   * @param string $propertyName
390
   *
391
   * @return bool
392
   */
393 1
  protected static function haveProperty($propertyName) {
394 1
    return !empty(static::getFieldName($propertyName));
395
  }
396
397
  /**
398
   * Translate field name to property name
399
   *
400
   * @param string $fieldName
401
   *
402
   * @return string Property name for field if field exists or '' otherwise
403
   */
404 4
  protected static function getPropertyName($fieldName) {
405
    return
406
      // If there translation of field name = returning translation result
407 4
      static::haveTranslationToProperty($fieldName)
408 4
        ? static::$_fieldsToProperties[$fieldName]
409
        // No, there is no translation
410
        // Is field exists in table? If yes - returning field name as property name
411 4
        : (static::haveField($fieldName) ? $fieldName : '');
412
  }
413
414
  /**
415
   * Converts property-indexed value array to field-indexed via translation table
416
   *
417
   * @param array $names
418
   * @param bool  $fieldToProperties - translation direction:
419
   *                                 - self::FIELDS_TO_PROPERTIES - field to props
420
   *                                 - self::PROPERTIES_TO_FIELDS - prop to fields
421
   *
422
   * @return array
423
   */
424
  // TODO - Throw exception on incorrect field
425 1
  protected static function translateNames(array $names, $fieldToProperties = self::FIELDS_TO_PROPERTIES) {
426 1
    $result = [];
427 1
    foreach ($names as $name => $value) {
428 1
      $exists = $fieldToProperties == self::FIELDS_TO_PROPERTIES ? static::haveField($name) : static::haveProperty($name);
429 1
      if (!$exists) {
430 1
        continue;
431
      }
432
433
      $name =
434
        $fieldToProperties == self::FIELDS_TO_PROPERTIES
435 1
          ? static::getPropertyName($name)
436 1
          : static::getFieldName($name);
437 1
      $result[$name] = $value;
438 1
    }
439
440 1
    return $result;
441
  }
442
443
  /**
444
   * Makes array of object from field/property list array
445
   *
446
   * Empty records and non-records (non-subarrays) are ignored
447
   * Function maintains record indexes
448
   *
449
   * @param array[] $records           - array of DB records [(int) => [$name => $value]]
450
   * @param bool    $fieldToProperties - should names be translated (true - for field records, false - for property records)
451
   *
452
   * @return array|static[]
453
   */
454 1
  protected static function fromRecordList($records, $fieldToProperties = self::FIELDS_TO_PROPERTIES) {
455 1
    $result = [];
456 1
    if (is_array($records) && !empty($records)) {
457 1
      foreach ($records as $key => $recordArray) {
458 1
        if (!is_array($recordArray) || empty($recordArray)) {
459 1
          continue;
460
        }
461
462
        $fieldToProperties === self::FIELDS_TO_PROPERTIES
463 1
          ? $recordArray = static::translateNames($recordArray, self::FIELDS_TO_PROPERTIES)
464 1
          : false;
465
466 1
        $theRecord = static::build($recordArray);
467 1
        if (is_object($theRecord)) {
468 1
          $theRecord->_isNew = false;
469
//          if(!empty($theRecord->id)) {
0 ignored issues
show
Unused Code Comprehensibility introduced by
58% 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...
470
//            $key = $theRecord->id;
471
//          }
472 1
          $result[$key] = $theRecord;
473 1
        }
474 1
      }
475 1
    }
476
477 1
    return $result;
478
  }
479
480 1
  protected function defaultValues() {
481 1
    foreach (static::dbGetFieldsDescription() as $fieldName => $fieldData) {
482 1
      if (array_key_exists($propertyName = static::getPropertyName($fieldName), $this->values)) {
483 1
        continue;
484
      }
485
486
      // Skipping auto increment fields
487 1
      if (strpos($fieldData->Extra, SN_SQL_EXTRA_AUTO_INCREMENT) !== false) {
488 1
        continue;
489
      }
490
491 1
      if ($fieldData->Type == SN_SQL_TYPE_NAME_TIMESTAMP && $fieldData->Default == SN_SQL_DEFAULT_CURRENT_TIMESTAMP) {
492 1
        $this->__set($propertyName, SN_TIME_SQL);
493 1
        continue;
494
      }
495
496 1
      $this->__set($propertyName, $fieldData->Default);
497 1
    }
498 1
  }
499
500
  /**
501
   * Set AR properties from array of PROPERTIES
502
   *
503
   * DOES NOT override existing values
504
   * DOES set default values for empty fields
505
   *
506
   * @param array $properties
507
   */
508 1
  protected function fromProperties(array $properties) {
509 1
    foreach ($properties as $name => $value) {
510 1
      $this->__set($name, $value);
511 1
    }
512
513 1
    $this->defaultValues();
514 1
  }
515
516
  /**
517
   * Set AR properties from array of FIELDS
518
   *
519
   * DOES NOT override existing values
520
   * DOES set default values for empty fields
521
   *
522
   * @param array $fields List of field values [$fieldName => $fieldValue]
523
   */
524 1
  protected function fromFields(array $fields) {
525 1
    $this->fromProperties(static::translateNames($fields, self::FIELDS_TO_PROPERTIES));
526 1
  }
527
528
  /**
529
   * @return bool
530
   */
531
  protected function dbInsert() {
532
    return
533
      static::dbPrepareQuery()
534
        ->setValues(static::translateNames($this->values, self::PROPERTIES_TO_FIELDS))
535
        ->doInsert();
536
  }
537
538
  /**
539
   * Protects object from setting non-existing property
540
   *
541
   * @param string $propertyName
542
   *
543
   * @throws DbalFieldInvalidException
544
   */
545 2
  protected function shieldName($propertyName) {
546 2
    if (!self::haveProperty($propertyName)) {
547 1
      throw new DbalFieldInvalidException(sprintf(
548 1
        '{{{ Свойство \'%1$s\' не существует в ActiveRecord \'%2$s\' }}}', $propertyName, get_called_class()
549 1
      ));
550
    }
551 2
  }
552
553
}
554