Test Failed
Branch trunk (412648)
by SuperNova.WS
03:40
created

ActiveRecordAbstract::fromRecordList()   C

Complexity

Conditions 8
Paths 2

Size

Total Lines 22
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
eloc 14
nc 2
nop 2
dl 0
loc 22
rs 6.6037
c 0
b 0
f 0
1
<?php
2
/**
3
 * Created by Gorlum 12.07.2017 10:27
4
 */
5
6
namespace DBAL;
7
8
use Common\AccessLogged;
9
use Common\GlobalContainer;
10
use Exceptions\DbalFieldInvalidException;
11
12
abstract class ActiveRecordAbstract extends AccessLogged {
13
  const FIELDS_TO_PROPERTIES = true;
14
  const PROPERTIES_TO_FIELDS = false;
15
16
  const IGNORE_PREFIX = 'Record';
17
  const ID_PROPERTY_NAME = 'id';
18
19
  /**
20
   * @var \db_mysql $db
21
   */
22
  protected static $db;
23
24
  /**
25
   * Table name for current Active Record
26
   *
27
   * Can be predefined in class or calculated in run-time
28
   *
29
   * ALWAYS SHOULD BE OVERRIDEN IN CHILD CLASSES!
30
   *
31
   * @var string $_tableName
32
   */
33
  protected static $_tableName = '';
34
  /**
35
   * Autoincrement index field name in DB
36
   * Would be normalized to 'id' ($id class property)
37
   *
38
   * @var string $_primaryIndexField
39
   */
40
  protected static $_primaryIndexField = 'id';
41
42
  /**
43
   * Field name translations to property names
44
   *
45
   * @var string[] $_fieldsToProperties
46
   */
47
  protected static $_fieldsToProperties = [];
48
  /**
49
   * Property name translations to field names
50
   *
51
   * Cached structure
52
   *
53
   * @var string[] $_propertiesToFields
54
   */
55
  private static $_propertiesToFields = [];
0 ignored issues
show
Unused Code introduced by
The property $_propertiesToFields is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
56
57
  // AR's service fields
58
  /**
59
   * Is this field - new field?
60
   *
61
   * @var bool $_isNew
62
   */
63
  protected $_isNew = true;
64
65
  // ABSTRACT METHODS - need override ==================================================================================
66
67
  /**
68
   * @return bool
69
   */
70
  abstract protected function dbInsert();
71
72
  /**
73
   * Asks DB for last insert ID
74
   *
75
   * @return int|string
76
   */
77
  abstract protected function dbLastInsertId();
78
79
  /**
80
   * @return bool
81
   */
82
  abstract protected function dbUpdate();
83
84
85
  /**
86
   * Get used DB
87
   *
88
   * @return \db_mysql
89
   */
90
  public static function db() {
91
    empty(static::$db) ? static::$db = \classSupernova::services()->db : false;
0 ignored issues
show
Documentation introduced by
The property db does not exist on object<Common\GlobalContainer>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
92
93
    return static::$db;
94
  }
95
96
  /**
97
   * @return array[]
98
   */
99
  protected static function dbGetTableFields() {
100
    return static::db()->schema()->getTableSchema(static::tableName())->fields;
101
  }
102
103
  public static function setDb(\db_mysql $db) {
104
    static::$db = $db;
105
  }
106
107
  /**
108
   * Get table name
109
   *
110
   * @return string
111
   */
112
  public static function tableName() {
113
    empty(static::$_tableName) ? static::$_tableName = static::calcTableName() : false;
114
115
    return static::$_tableName;
116
  }
117
118
  /**
119
   * Instate ActiveRecord from array of field values - even if it is empty
120
   *
121
   * @param array $fields List of field values [$propertyName => $propertyValue]
0 ignored issues
show
Bug introduced by
There is no parameter named $fields. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
122
   *
123
   * @return static
124
   */
125
  public static function buildEvenEmpty(array $properties = []) {
126
    $record = new static();
127
    $record->fromProperties($properties);
128
129
    return $record;
130
  }
131
132
  /**
133
   * Instate ActiveRecord from array of field values
134
   *
135
   * @param array $fields List of field values [$propertyName => $propertyValue]
0 ignored issues
show
Bug introduced by
There is no parameter named $fields. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
136
   *
137
   * @return static|bool
138
   */
139
  public static function build(array $properties = []) {
140
    if (!is_array($properties) || empty($properties)) {
141
      return false;
142
    }
143
144
    return static::buildEvenEmpty($properties);
145
  }
146
147
  /**
148
   * Finds records by property - equivalent of SELECT ... WHERE ... AND ...
149
   *
150
   * @param array|mixed $propertyFilter - ID of record to find OR [$propertyName => $propertyValue]. Pass [] to find all records in table
151
   *
152
   * @return bool|\mysqli_result
153
   */
154
  public static function find($propertyFilter) {
155
    if (!is_array($propertyFilter)) {
156
      $propertyFilter = [self::ID_PROPERTY_NAME => $propertyFilter];
157
    }
158
159
    $dbq = static::dbPrepareQuery();
160
    if (!empty($propertyFilter)) {
161
      $dbq->setWhereArray(static::translateNames($propertyFilter, static::PROPERTIES_TO_FIELDS));
162
    }
163
164
    return $dbq->doSelect();
165
  }
166
167
  /**
168
   * Gets first record by $where
169
   *
170
   * @param array|mixed $propertyFilter - ID of record to find OR [$propertyName => $propertyValue]. Pass [] to find all records in table
171
   *
172
   * @return string[] - [$field_name => $field_value]
173
   */
174
  public static function findRecordFirst($propertyFilter) {
175
    $result = empty($mysqliResult = static::find($propertyFilter)) ? [] : $mysqliResult->fetch_assoc();
176
177
    // Secondary check - for fetch_assoc() result
178
    return empty($result) ? [] : $result;
179
  }
180
181
  /**
182
   * Gets all records by $where
183
   *
184
   * @param array|mixed $propertyFilter - ID of record to find OR [$property_name => $property_value]
185
   *
186
   * @return string[][] - [(int) => [$field_name => $field_value]]
187
   */
188
  public static function findRecordsAll($propertyFilter) {
189
    return empty($mysqliResult = static::find($propertyFilter)) ? [] : $mysqliResult->fetch_all(MYSQLI_ASSOC);
190
  }
191
192
  /**
193
   * Gets all records by $where - array indexes is a record IDs
194
   *
195
   * @param array|mixed $propertyFilter - ID of record to find OR [$property_name => $property_value]
196
   *
197
   * @return string[][] - [$record_db_id => [$field_name => $field_value]]
198
   */
199
  public static function findRecordsAllIndexed($propertyFilter) {
200
    $result = [];
201
    if (!empty($mysqliResult = static::find($propertyFilter))) {
202
      while ($row = $mysqliResult->fetch_assoc()) {
203
        $result[$row[static::$_primaryIndexField]] = $row;
204
      }
205
    }
206
207
    return $result;
208
  }
209
210
  /**
211
   * Gets first ActiveRecord by $where
212
   *
213
   * @param array|mixed $propertyFilter - ID of record to find OR [$propertyName => $propertyValue]. Pass [] to find all records in table
214
   *
215
   * @return static|bool
216
   */
217
  public static function findFirst($propertyFilter) {
218
    $record = false;
219
    $fields = static::findRecordFirst($propertyFilter);
220
    if (!empty($fields)) {
221
      $record = static::build(static::translateNames($fields, self::FIELDS_TO_PROPERTIES));
222
      if (is_object($record)) {
223
        $record->_isNew = false;
224
      }
225
    }
226
227
    return $record;
228
  }
229
230
  /**
231
   * Gets all ActiveRecords by $where
232
   *
233
   * @param array|mixed $propertyFilter - ID of record to find OR [$propertyName => $propertyValue]. Pass [] to find all records in table
234
   *
235
   * @return array|static[] - [(int) => static]
236
   */
237
  public static function findAll($propertyFilter) {
238
    return static::fromRecordList(static::findRecordsAll($propertyFilter));
239
  }
240
241
  /**
242
   * Gets all ActiveRecords by $where - array indexed by record IDs
243
   *
244
   * @param array|mixed $propertyFilter - ID of record to find OR [$propertyName => $propertyValue]. Pass [] to find all records in table
245
   *
246
   * @return array|static[] - [$record_db_id => static]
247
   */
248
  public static function findAllIndexed($propertyFilter) {
249
    return static::fromRecordList(static::findRecordsAllIndexed($propertyFilter));
250
  }
251
252
253
  /**
254
   * ActiveRecord constructor.
255
   *
256
   * @param GlobalContainer|null $services
257
   */
258
  public function __construct(GlobalContainer $services = null) {
259
    parent::__construct($services);
260
  }
261
262
  public function acceptChanges() {
263
    parent::acceptChanges();
264
    $this->_isNew = empty($this->id);
0 ignored issues
show
Documentation introduced by
The property id does not exist on object<DBAL\ActiveRecordAbstract>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
265
  }
266
267
  /**
268
   * Reload current record from ID
269
   *
270
   * @return bool
271
   */
272
  public function reload() {
273
    $recordId = $this->id;
0 ignored issues
show
Documentation introduced by
The property id does not exist on object<DBAL\ActiveRecordAbstract>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
274
    if (empty($recordId)) {
275
      return false;
276
    }
277
278
    $this->acceptChanges();
279
280
    $fields = static::findRecordFirst($recordId);
281
    if (empty($fields)) {
282
      return false;
283
    }
284
285
    $this->fromFields($fields);
286
    $this->_isNew = false;
287
288
    return true;
289
  }
290
291
  /**
292
   * @return array|bool|\mysqli_result|null
293
   */
294
  // TODO - do a check that all fields present in stored data. I.e. no empty fields with no defaults
295
  public function insert() {
296
    if ($this->isEmpty()) {
297
      return false;
298
    }
299
    if (!$this->_isNew) {
300
      return false;
301
    }
302
303
    $this->defaultValues();
304
305
    if (!$this->dbInsert()) {
306
      return false;
307
    }
308
309
    $this->id = $this->dbLastInsertId();
0 ignored issues
show
Documentation introduced by
The property id does not exist on object<DBAL\ActiveRecordAbstract>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
310
    $this->acceptChanges();
311
    $this->_isNew = false;
312
313
//    return $this->reload();
0 ignored issues
show
Unused Code Comprehensibility introduced by
67% 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...
314
    return true;
315
  }
316
317
  /**
318
   * @return array|bool|\mysqli_result|null
319
   */
320
  public function update() {
321
    if (empty($this->_changes) && empty($this->_deltas)) {
322
      return true;
323
    }
324
325
    $this->defaultValues();
326
327
    if (!$this->dbUpdate()) {
328
      return false;
329
    }
330
331
    $this->acceptChanges();
332
333
    return true;
334
  }
335
336
  /**
337
   * Normalize array
338
   *
339
   * Basically - uppercase all field names to make it use in PTL
340
   * Can be override by descendants to make more convinient, clear and robust indexes
341
   *
342
   * @return array
343
   */
344
  public function ptlArray() {
345
    $result = [];
346
    foreach ($this->values as $key => $value) {
347
      $result[strtoupper(\HelperString::camelToUnderscore($key))] = $value;
348
    }
349
350
    return $result;
351
  }
352
353
354
  /**
355
   * Returns default value if original value not set
356
   *
357
   * @param string $propertyName
358
   *
359
   * @return mixed
360
   */
361
  public function __get($propertyName) {
362
    return $this->__isset($propertyName) ? parent::__get($propertyName) : $this->getDefault($propertyName);
363
  }
364
365
  /**
366
   * Protects object from setting non-existing property
367
   *
368
   * @param string $propertyName
369
   */
370
  protected function shieldName($propertyName) {
371
    if (!self::haveProperty($propertyName)) {
372
      throw new DbalFieldInvalidException(sprintf(
373
        '{{{ Свойство \'%1$s\' не существует в ActiveRecord \'%2$s\' }}}', $propertyName, get_called_class()
374
      ));
375
    }
376
  }
377
378
  public function __set($propertyName, $value) {
379
    $this->shieldName($propertyName);
380
    parent::__set($propertyName, $value);
381
  }
382
383
  /**
384
   * Get default value for field
385
   *
386
   * @param string $propertyName
387
   *
388
   * @return mixed
389
   */
390
  public function getDefault($propertyName) {
391
    $fieldName = self::getFieldName($propertyName);
392
393
    return
394
      isset(static::dbGetTableFields()[$fieldName]['Default'])
395
        ? static::dbGetTableFields()[$fieldName]['Default']
396
        : null;
397
  }
398
399
  /**
400
   * Calculate table name by class name and fills internal property
401
   *
402
   * Namespaces does not count - only class name taken into account
403
   * Class name converted from CamelCase to underscore_name
404
   * Prefix "Record" is ignored - can be override
405
   *
406
   * Examples:
407
   * Class \Namespace\ClassName will map to table `class_name`
408
   * Class \NameSpace\RecordLongName will map to table `long_name`
409
   *
410
   * Can be overriden to provide different name
411
   *
412
   * @return string - table name in DB
413
   *
414
   */
415
  protected static function calcTableName() {
416
    $temp = explode('\\', get_called_class());
417
    $className = end($temp);
418
    if (strpos($className, static::IGNORE_PREFIX) === 0) {
419
      $className = substr($className, strlen(static::IGNORE_PREFIX));
420
    }
421
422
    return \HelperString::camelToUnderscore($className);
423
  }
424
425
  /**
426
   * Is there translation for this field name to property name
427
   *
428
   * @param string $fieldName
429
   *
430
   * @return bool
431
   */
432
  protected static function haveTranslationToProperty($fieldName) {
433
    return !empty(static::$_fieldsToProperties[$fieldName]);
434
  }
435
436
  /**
437
   * Check if field exists
438
   *
439
   * @param string $fieldName
440
   *
441
   * @return bool
442
   */
443
  protected static function haveField($fieldName) {
444
    return !empty(static::dbGetTableFields()[$fieldName]);
445
  }
446
447
  /**
448
   * Returns field name by property name
449
   *
450
   * @param string $propertyName
451
   *
452
   * @return string Field name for property or '' if not field
453
   */
454
  protected static function getFieldName($propertyName) {
455
    $fieldName = array_search($propertyName, static::$_fieldsToProperties);
456
    if (
457
      // No translation found for property name
458
      !$fieldName
0 ignored issues
show
Bug Best Practice introduced by
The expression $fieldName of type false|integer is loosely compared to false; this is ambiguous if the integer can be zero. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
459
      &&
460
      // AND Property name is not among translatable field names
461
      !static::haveTranslationToProperty($propertyName)
462
      &&
463
      // AND field name exists
464
      static::haveField($propertyName)
465
    ) {
466
      // Returning property name as field name
467
      $fieldName = $propertyName;
468
    }
469
470
    return $fieldName === false ? '' : $fieldName;
471
  }
472
473
  /**
474
   * Does property exists?
475
   *
476
   * @param string $propertyName
477
   *
478
   * @return bool
479
   */
480
  protected static function haveProperty($propertyName) {
481
    return !empty(static::getFieldName($propertyName));
482
  }
483
484
  /**
485
   * Translate field name to property name
486
   *
487
   * @param string $fieldName
488
   *
489
   * @return string Property name for field if field exists or '' otherwise
490
   */
491
  protected static function getPropertyName($fieldName) {
492
    return
493
      // If there translation of field name = returning translation result
494
      static::haveTranslationToProperty($fieldName)
495
        ? static::$_fieldsToProperties[$fieldName]
496
        // No, there is no translation
497
        // Is field exists in table? If yes - returning field name as property name
498
        : (static::haveField($fieldName) ? $fieldName : '');
499
  }
500
501
  /**
502
   * Converts property-indexed value array to field-indexed via translation table
503
   *
504
   * @param array $names
505
   * @param bool  $fieldToProperties - translation direction:
506
   *    - self::FIELDS_TO_PROPERTIES - field to props.
507
   *    - self::PROPERTIES_TO_FIELDS - prop to fields
508
   *
509
   * @return array
510
   */
511
  // TODO - Throw exception on incorrect field
512
  protected static function translateNames(array $names, $fieldToProperties = self::FIELDS_TO_PROPERTIES) {
513
    $translations = $fieldToProperties == self::FIELDS_TO_PROPERTIES ? static::$_fieldsToProperties : array_flip(static::$_fieldsToProperties);
514
515
    $result = [];
516
    foreach ($names as $name => $value) {
517
      $exists = $fieldToProperties == self::FIELDS_TO_PROPERTIES ? static::haveField($name) : static::haveProperty($name);
518
      if (!$exists) {
519
        continue;
520
      }
521
522
      if (!empty($translations[$name])) {
523
        $name = $translations[$name];
524
      }
525
      $result[$name] = $value;
526
    }
527
528
    return $result;
529
  }
530
531
  /**
532
   * Makes array of object from field/property list array
533
   *
534
   * Empty records and non-records (non-subarrays) are ignored
535
   * Function maintains record indexes
536
   *
537
   * @param array $records - array of DB records [(int) => [$name => $value]]
538
   * @param bool  $fieldToProperties - should names be translated (true - for field records, false - for property records)
539
   *
540
   * @return array|static[]
541
   */
542
  protected static function fromRecordList($records, $fieldToProperties = self::FIELDS_TO_PROPERTIES) {
543
    $result = [];
544
    if (is_array($records) && !empty($records)) {
545
      foreach ($records as $key => $recordArray) {
546
        if (!is_array($recordArray) || empty($recordArray)) {
547
          continue;
548
        }
549
550
        $fieldToProperties === self::FIELDS_TO_PROPERTIES
551
          ? $recordArray = static::translateNames($recordArray, self::FIELDS_TO_PROPERTIES)
552
          : false;
553
554
        $theRecord = static::build($recordArray);
555
        if (is_object($theRecord)) {
556
          $theRecord->_isNew = false;
557
          $result[$key] = $theRecord;
558
        }
559
      }
560
    }
561
562
    return $result;
563
  }
564
565
  /**
566
   * Prepares DbQuery object for further operations
567
   *
568
   * @return DbQuery
569
   */
570
  protected static function dbPrepareQuery() {
571
    return DbQuery::build(static::db())->setTable(static::tableName());
572
  }
573
574
  protected static function dbFetch(\mysqli_result $mysqliResult) {
575
    return $mysqliResult->fetch_assoc();
576
  }
577
578
579
  protected function defaultValues() {
580
    foreach (static::dbGetTableFields() as $fieldName => $fieldData) {
581
      if (array_key_exists($propertyName = static::getPropertyName($fieldName), $this->values)) {
582
        continue;
583
      }
584
585
      // Skipping auto increment fields
586
      if (strpos($fieldData['Extra'], SN_SQL_EXTRA_AUTO_INCREMENT) !== false) {
587
        continue;
588
      }
589
590
      if ($fieldData['Type'] == SN_SQL_TYPE_NAME_TIMESTAMP && $fieldData['Default'] == SN_SQL_DEFAULT_CURRENT_TIMESTAMP) {
591
        $this->__set($propertyName, SN_TIME_SQL);
592
        continue;
593
      }
594
595
      $this->__set($propertyName, $fieldData['Default']);
596
    }
597
  }
598
599
  /**
600
   * Set AR properties from array of PROPERTIES
601
   *
602
   * DOES NOT override existing values
603
   * DOES set default values for empty fields
604
   *
605
   * @param array $properties
606
   */
607
  protected function fromProperties(array $properties) {
608
    foreach ($properties as $name => $value) {
609
      $this->__set($name, $value);
610
    }
611
612
    $this->defaultValues();
613
  }
614
615
  /**
616
   * Set AR properties from array of FIELDS
617
   *
618
   * DOES NOT override existing values
619
   * DOES set default values for empty fields
620
   *
621
   * @param array $fields List of field values [$fieldName => $fieldValue]
622
   */
623
  protected function fromFields(array $fields) {
624
    $this->fromProperties(static::translateNames($fields, self::FIELDS_TO_PROPERTIES));
625
  }
626
627
}
628