Passed
Branch trunk (7dc288)
by SuperNova.WS
06:07
created

ActiveRecordAbstract::setForUpdate()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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