ActiveRecordAbstract::__construct()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 3
rs 10
c 1
b 0
f 1
ccs 3
cts 3
cp 1
crap 1
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 {
0 ignored issues
show
Deprecated Code introduced by
The class Common\AccessLogged has been deprecated. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

18
abstract class ActiveRecordAbstract extends /** @scrutinizer ignore-deprecated */ AccessLogged {
Loading history...
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();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $dbq->doSelect() also could return the type array which is incompatible with the documented return type boolean|mysqli_result.
Loading history...
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->accept();
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())->fields;
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;
0 ignored issues
show
introduced by
The condition $fieldName === false is always false.
Loading history...
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)) {
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))
0 ignored issues
show
Bug introduced by
It seems like $this->values can also be of type \ArrayObject; however, parameter $names of DBAL\ActiveRecordAbstract::translateNames() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

534
        ->setValues(static::translateNames(/** @scrutinizer ignore-type */ $this->values, self::PROPERTIES_TO_FIELDS))
Loading history...
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