Completed
Push — trunk ( 1f2f20...9c1015 )
by SuperNova.WS
04:43
created

ActiveRecordAbstract::build()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 4
nc 2
nop 1
dl 0
loc 7
ccs 4
cts 4
cp 1
crap 3
rs 9.4285
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
/**
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 \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
  // AR's service fields
46
  /**
47
   * Is this field - new field?
48
   *
49
   * @var bool $_isNew
50
   */
51
  protected $_isNew = true;
52
53
54
55
56
57
  /**
58
   * Get table name
59
   *
60
   * @return string
61
   */
62 1
  public static function tableName() {
63 1
    empty(static::$_tableName) ? static::$_tableName = static::calcTableName() : false;
64
65 1
    return static::$_tableName;
66
  }
67
68
  /**
69
   * @param \db_mysql $db
70
   */
71 1
  public static function setDb(\db_mysql $db) {
72 1
    static::$db = $db;
73 1
  }
74
75
  /**
76
   * Get DB
77
   *
78
   * @return \db_mysql
79
   */
80 1
  public static function db() {
81 1
    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...
82
83 1
    return static::$db;
84
  }
85
86
  /**
87
   * Instate ActiveRecord from array of field values - even if it is empty
88
   *
89
   * @param array $properties List of field values [$propertyName => $propertyValue]
90
   *
91
   * @return static
92
   */
93 1
  public static function buildEvenEmpty(array $properties = []) {
94 1
    $record = new static();
95 1
    $record->fromProperties($properties);
96
97 1
    return $record;
98
  }
99
100
  /**
101
   * Instate ActiveRecord from array of field values
102
   *
103
   * @param array $properties List of field values [$propertyName => $propertyValue]
104
   *
105
   * @return static|bool
106
   */
107 1
  public static function build(array $properties = []) {
108 1
    if (!is_array($properties) || empty($properties)) {
109 1
      return false;
110
    }
111
112 1
    return static::buildEvenEmpty($properties);
113
  }
114
115
  /**
116
   * Finds records by property - equivalent of SELECT ... WHERE ... AND ...
117
   *
118
   * @param array|mixed $propertyFilter - [$propertyName => $propertyValue]. Pass [] to find all records in table
119
   *
120
   * @return bool|\mysqli_result
0 ignored issues
show
Documentation introduced by
Should the return type not be array|boolean|\mysqli_result|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
121
   */
122
  public static function find($propertyFilter) {
123
    $dbq = static::dbPrepareQuery();
124
    if (!empty($propertyFilter)) {
125
      $dbq->setWhereArray(static::translateNames($propertyFilter, static::PROPERTIES_TO_FIELDS));
126
    }
127
128
    return $dbq->doSelect();
129
  }
130
131
  /**
132
   * Gets first record by $where
133
   *
134
   * @param array|mixed $propertyFilter - ID of record to find OR [$propertyName => $propertyValue]. Pass [] to find all records in table
135
   *
136
   * @return string[] - [$field_name => $field_value]
137
   */
138
  public static function findRecordFirst($propertyFilter) {
139
    $result = empty($mysqliResult = static::find($propertyFilter)) ? [] : $mysqliResult->fetch_assoc();
140
141
    // Secondary check - for fetch_assoc() result
142
    return empty($result) ? [] : $result;
143
  }
144
145
  /**
146
   * Gets all records by $where
147
   *
148
   * @param array|mixed $propertyFilter - ID of record to find OR [$property_name => $property_value]
149
   *
150
   * @return array[] - [(int) => [$field_name => $field_value]]
151
   */
152
  public static function findRecordsAll($propertyFilter) {
153
    return empty($mysqliResult = static::find($propertyFilter)) ? [] : $mysqliResult->fetch_all(MYSQLI_ASSOC);
154
  }
155
156
  /**
157
   * Gets first ActiveRecord by $where
158
   *
159
   * @param array|mixed $propertyFilter - ID of record to find OR [$propertyName => $propertyValue]. Pass [] to find all records in table
160
   *
161
   * @return static|bool
162
   */
163
  public static function findFirst($propertyFilter) {
164
    $record = false;
165
    $fields = static::findRecordFirst($propertyFilter);
166
    if (!empty($fields)) {
167
      $record = static::build(static::translateNames($fields, self::FIELDS_TO_PROPERTIES));
168
      if (is_object($record)) {
169
        $record->_isNew = false;
170
      }
171
    }
172
173
    return $record;
174
  }
175
176
  /**
177
   * Gets all ActiveRecords by $where
178
   *
179
   * @param array|mixed $propertyFilter - ID of record to find OR [$propertyName => $propertyValue]. Pass [] to find all records in table
180
   *
181
   * @return array|static[] - [(int) => static]
182
   */
183
  public static function findAll($propertyFilter) {
184
    return static::fromRecordList(static::findRecordsAll($propertyFilter));
185
  }
186
187
188
  /**
189
   * ActiveRecord constructor.
190
   *
191
   * @param GlobalContainer|null $services
192
   */
193 1
  public function __construct(GlobalContainer $services = null) {
194 1
    parent::__construct($services);
195 1
  }
196
197
  /**
198
   * @return bool
199
   */
200
  // TODO - do a check that all fields present in stored data. I.e. no empty fields with no defaults
201
  public function insert() {
202
    if ($this->isEmpty()) {
203
      return false;
204
    }
205
    if (!$this->_isNew) {
206
      return false;
207
    }
208
209
    $this->defaultValues();
210
211
    if (!$this->dbInsert()) {
212
      return false;
213
    }
214
215
    $this->acceptChanges();
216
    $this->_isNew = false;
217
218
    return true;
219
  }
220
221
  /**
222
   * Normalize array
223
   *
224
   * Basically - uppercase all field names to make it use in PTL
225
   * Can be override by descendants to make more convinient, clear and robust indexes
226
   *
227
   * @return array
228
   */
229
  public function ptlArray() {
230
    $result = [];
231
    foreach ($this->values as $key => $value) {
232
      $result[strtoupper(\HelperString::camelToUnderscore($key))] = $value;
233
    }
234
235
    return $result;
236
  }
237
238
  /**
239
   * Get default value for field
240
   *
241
   * @param string $propertyName
242
   *
243
   * @return mixed
244
   */
245 2
  public function getDefault($propertyName) {
246 2
    $fieldName = self::getFieldName($propertyName);
247
248
    return
249 2
      isset(static::dbGetFieldsDescription()[$fieldName]->Default)
250 2
        ? static::dbGetFieldsDescription()[$fieldName]->Default
251 2
        : null;
252
  }
253
254
  /**
255
   * Returns default value if original value not set
256
   *
257
   * @param string $propertyName
258
   *
259
   * @return mixed
260
   */
261 1
  public function __get($propertyName) {
262 1
    return $this->__isset($propertyName) ? parent::__get($propertyName) : $this->getDefault($propertyName);
263
  }
264
265 1
  public function __set($propertyName, $value) {
266 1
    $this->shieldName($propertyName);
267 1
    parent::__set($propertyName, $value);
268 1
  }
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
  /**
285
   * Calculate table name by class name and fills internal property
286
   *
287
   * Namespaces does not count - only class name taken into account
288
   * Class name converted from CamelCase to underscore_name
289
   * Prefix "Record" is ignored - can be override
290
   *
291
   * Examples:
292
   * Class \Namespace\ClassName will map to table `class_name`
293
   * Class \NameSpace\RecordLongName will map to table `long_name`
294
   *
295
   * Can be overriden to provide different name
296
   *
297
   * @return string - table name in DB
298
   *
299
   */
300 1
  protected static function calcTableName() {
301 1
    $temp = explode('\\', get_called_class());
302 1
    $className = end($temp);
303 1
    if (strpos($className, static::IGNORE_PREFIX) === 0) {
304 1
      $className = substr($className, strlen(static::IGNORE_PREFIX));
305 1
    }
306
307 1
    return \HelperString::camelToUnderscore($className);
308
  }
309
310
  /**
311
   * Get table fields description
312
   *
313
   * @return DbFieldDescription[]
314
   */
315
  protected static function dbGetFieldsDescription() {
316
    return static::db()->schema()->getTableSchema(static::tableName())->fieldsObject;
317
  }
318
319
  /**
320
   * Prepares DbQuery object for further operations
321
   *
322
   * @return DbQuery
323
   */
324 1
  protected static function dbPrepareQuery() {
325 1
    return DbQuery::build(static::db())->setTable(static::tableName());
326
  }
327
328
  /**
329
   * Is there translation for this field name to property name
330
   *
331
   * @param string $fieldName
332
   *
333
   * @return bool
334
   */
335 1
  protected static function haveTranslationToProperty($fieldName) {
336 1
    return !empty(static::$_fieldsToProperties[$fieldName]);
337
  }
338
339
  /**
340
   * Check if field exists
341
   *
342
   * @param string $fieldName
343
   *
344
   * @return bool
345
   */
346 1
  protected static function haveField($fieldName) {
347 1
    return !empty(static::dbGetFieldsDescription()[$fieldName]);
348
  }
349
350
  /**
351
   * Returns field name by property name
352
   *
353
   * @param string $propertyName
354
   *
355
   * @return string Field name for property or '' if not field
0 ignored issues
show
Documentation introduced by
Should the return type not be integer|string?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
356
   */
357 4
  protected static function getFieldName($propertyName) {
358 4
    $fieldName = array_search($propertyName, static::$_fieldsToProperties);
359
    if (
360
      // No translation found for property name
361
      $fieldName === false
362 4
      &&
363
      // AND Property name is not among translatable field names
364 3
      !static::haveTranslationToProperty($propertyName)
365 4
      &&
366
      // AND field name exists
367 2
      static::haveField($propertyName)
368 4
    ) {
369
      // Returning property name as field name
370 1
      $fieldName = $propertyName;
371 1
    }
372
373 4
    return $fieldName === false ? '' : $fieldName;
374
  }
375
376
  /**
377
   * Does property exists?
378
   *
379
   * @param string $propertyName
380
   *
381
   * @return bool
382
   */
383 1
  protected static function haveProperty($propertyName) {
384 1
    return !empty(static::getFieldName($propertyName));
385
  }
386
387
  /**
388
   * Translate field name to property name
389
   *
390
   * @param string $fieldName
391
   *
392
   * @return string Property name for field if field exists or '' otherwise
393
   */
394 4
  protected static function getPropertyName($fieldName) {
395
    return
396
      // If there translation of field name = returning translation result
397 4
      static::haveTranslationToProperty($fieldName)
398 4
        ? static::$_fieldsToProperties[$fieldName]
399
        // No, there is no translation
400
        // Is field exists in table? If yes - returning field name as property name
401 4
        : (static::haveField($fieldName) ? $fieldName : '');
402
  }
403
404
  /**
405
   * Converts property-indexed value array to field-indexed via translation table
406
   *
407
   * @param array $names
408
   * @param bool  $fieldToProperties - translation direction:
409
   *    - self::FIELDS_TO_PROPERTIES - field to props.
410
   *    - self::PROPERTIES_TO_FIELDS - prop to fields
411
   *
412
   * @return array
413
   */
414
  // TODO - Throw exception on incorrect field
415 1
  protected static function translateNames(array $names, $fieldToProperties = self::FIELDS_TO_PROPERTIES) {
416 1
    $result = [];
417 1
    foreach ($names as $name => $value) {
418 1
      $exists = $fieldToProperties == self::FIELDS_TO_PROPERTIES ? static::haveField($name) : static::haveProperty($name);
419 1
      if (!$exists) {
420 1
        continue;
421
      }
422
423
      $name =
424
        $fieldToProperties == self::FIELDS_TO_PROPERTIES
425 1
          ? static::getPropertyName($name)
426 1
          : static::getFieldName($name);
427 1
      $result[$name] = $value;
428 1
    }
429
430 1
    return $result;
431
  }
432
433
  /**
434
   * Makes array of object from field/property list array
435
   *
436
   * Empty records and non-records (non-subarrays) are ignored
437
   * Function maintains record indexes
438
   *
439
   * @param array[] $records - array of DB records [(int) => [$name => $value]]
440
   * @param bool  $fieldToProperties - should names be translated (true - for field records, false - for property records)
441
   *
442
   * @return array|static[]
443
   */
444 1
  protected static function fromRecordList($records, $fieldToProperties = self::FIELDS_TO_PROPERTIES) {
445 1
    $result = [];
446 1
    if (is_array($records) && !empty($records)) {
447 1
      foreach ($records as $key => $recordArray) {
448 1
        if (!is_array($recordArray) || empty($recordArray)) {
449 1
          continue;
450
        }
451
452
        $fieldToProperties === self::FIELDS_TO_PROPERTIES
453 1
          ? $recordArray = static::translateNames($recordArray, self::FIELDS_TO_PROPERTIES)
454 1
          : false;
455
456 1
        $theRecord = static::build($recordArray);
457 1
        if (is_object($theRecord)) {
458 1
          $theRecord->_isNew = false;
459
//          if(!empty($theRecord->id)) {
460
//            $key = $theRecord->id;
461
//          }
462 1
          $result[$key] = $theRecord;
463 1
        }
464 1
      }
465 1
    }
466
467 1
    return $result;
468
  }
469
470 1
  protected function defaultValues() {
471 1
    foreach (static::dbGetFieldsDescription() as $fieldName => $fieldData) {
472 1
      if (array_key_exists($propertyName = static::getPropertyName($fieldName), $this->values)) {
473 1
        continue;
474
      }
475
476
      // Skipping auto increment fields
477 1
      if (strpos($fieldData->Extra, SN_SQL_EXTRA_AUTO_INCREMENT) !== false) {
478 1
        continue;
479
      }
480
481 1
      if ($fieldData->Type == SN_SQL_TYPE_NAME_TIMESTAMP && $fieldData->Default == SN_SQL_DEFAULT_CURRENT_TIMESTAMP) {
482 1
        $this->__set($propertyName, SN_TIME_SQL);
483 1
        continue;
484
      }
485
486 1
      $this->__set($propertyName, $fieldData->Default);
487 1
    }
488 1
  }
489
490
  /**
491
   * Set AR properties from array of PROPERTIES
492
   *
493
   * DOES NOT override existing values
494
   * DOES set default values for empty fields
495
   *
496
   * @param array $properties
497
   */
498 1
  protected function fromProperties(array $properties) {
499 1
    foreach ($properties as $name => $value) {
500 1
      $this->__set($name, $value);
501 1
    }
502
503 1
    $this->defaultValues();
504 1
  }
505
506
  /**
507
   * Set AR properties from array of FIELDS
508
   *
509
   * DOES NOT override existing values
510
   * DOES set default values for empty fields
511
   *
512
   * @param array $fields List of field values [$fieldName => $fieldValue]
513
   */
514 1
  protected function fromFields(array $fields) {
515 1
    $this->fromProperties(static::translateNames($fields, self::FIELDS_TO_PROPERTIES));
516 1
  }
517
518
  /**
519
   * @return bool
520
   */
521
  protected function dbInsert() {
522
    return
523
      static::dbPrepareQuery()
524
        ->setValues(static::translateNames($this->values, self::PROPERTIES_TO_FIELDS))
525
        ->doInsert();
526
  }
527
528
  /**
529
   * Protects object from setting non-existing property
530
   *
531
   * @param string $propertyName
532
   */
533 2
  protected function shieldName($propertyName) {
534 2
    if (!self::haveProperty($propertyName)) {
535 1
      throw new DbalFieldInvalidException(sprintf(
536 1
        '{{{ Свойство \'%1$s\' не существует в ActiveRecord \'%2$s\' }}}', $propertyName, get_called_class()
537 1
      ));
538
    }
539 2
  }
540
541
}
542