GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Completed
Push — master ( 24d155...0f1222 )
by Andreas
03:11
created

AbstractDbEntity::setPrimaryDbValue()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 16
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 4

Importance

Changes 0
Metric Value
dl 0
loc 16
ccs 12
cts 12
cp 1
rs 9.2
c 0
b 0
f 0
cc 4
eloc 10
nc 4
nop 1
crap 4
1
<?php
2
/**
3
 * Starlit Db.
4
 *
5
 * @copyright Copyright (c) 2016 Starweb AB
6
 * @license   BSD 3-Clause
7
 */
8
9
namespace Starlit\Db;
10
11
use Starlit\Utils\Str;
12
use Starlit\Utils\Arr;
13
14
/**
15
 * Abstract class to model a single database row into an object.
16
 *
17
 * @author Andreas Nilsson <http://github.com/jandreasn>
18
 */
19
abstract class AbstractDbEntity implements \Serializable
20
{
21
    /**
22
     * The database table name (meant to be overridden).
23
     *
24
     * @var string
25
     */
26
    protected static $dbTableName;
27
28
    /**
29
     * Entity's database properties and their attributes (meant to be overridden).
30
     * Example format:
31
     *
32
     * $dbProperties = [
33
     *     'productId' => ['type' => 'int'],
34
     *     'otherId'   => ['type' => 'int', 'required' => true,],
35
     *     'name'      => ['type' => 'string', 'maxLength' => 10, 'required' => true, 'default' => 'Some name'],
36
     * ];
37
     *
38
     * 'type' => 'int'     Corresponding PHP type (required).
39
     * 'required' => true  The value have to be set (not '', null, false)
40
     * 'nonEmpty' => true  The value should not be empty ('', 0, null)
41
     *
42
     * Properties correspond to database table's columns but words are
43
     * camel cased instead of separated with underscore (_) as in the database.
44
     *
45
     * @var array
46
     */
47
    protected static $dbProperties = [];
48
49
    /**
50
     * Object database field name that is used for primary key (meant to be overridden).
51
     * Should be camel cased as it maps to the dbFields array.
52
     *
53
     * @var string|array
54
     */
55
    protected static $primaryDbPropertyKey;
56
57
    /**
58
     * @var array
59
     */
60
    private static $cachedDefaultDbData = [];
61
62
    /**
63
     * @var array
64
     */
65
    private static $cachedDbPropertyNames;
66
67
    /**
68
     * @var array
69
     */
70
    private static $cachedDbFieldNames;
71
72
    /**
73
     * @var array
74
     */
75
    private static $typeDefaults = [
76
        'string' => '',
77
        'int'    => 0,
78
        'float'  => 0.0,
79
        'bool'   => false,
80
    ];
81
82
    /**
83
     * Database row data with field names and their values.
84
     *
85
     * @var array
86
     */
87
    private $dbData = [];
88
89
    /**
90
     * Database fields that has had their value modified since init/load.
91
     *
92
     * @var array
93
     */
94
    private $modifiedDbProperties = [];
95
96
    /**
97
     * @var bool
98
     */
99
    private $deleteFromDbOnSave = false;
100
101
    /**
102
     * @var bool
103
     */
104
    private $deleted = false;
105
106
    /**
107
     * @var bool
108
     */
109
    private $forceDbInsertOnSave = false;
110
111
    /**
112
     * Constructor.
113
     *
114
     * @param mixed $primaryDbValueOrRowData
115
     */
116 65
    public function __construct($primaryDbValueOrRowData = null)
117
    {
118 65
        self::checkStaticProperties();
119
120
        // Set default values
121 64
        $this->dbData = $this->getDefaultDbData();
122
123
        // Override default values with provided values
124 64
        if ($primaryDbValueOrRowData !== null) {
125 13
            $this->setPrimaryDbValueOrRowData($primaryDbValueOrRowData);
126 13
        }
127 64
    }
128
129
    /**
130
     * Make sure that class has all necessary static properties set.
131
     */
132 65
    private static function checkStaticProperties()
133
    {
134 65
        static $checkedClasses = [];
135 65
        if (!in_array(static::class, $checkedClasses)) {
136 7
            if (empty(static::$dbTableName)
137 6
                || empty(static::$dbProperties)
138 6
                || empty(static::$primaryDbPropertyKey)
139 6
                || (is_scalar(static::$primaryDbPropertyKey)
140 6
                    && !isset(static::$dbProperties[static::$primaryDbPropertyKey]['type']))
141 6
                || (is_array(static::$primaryDbPropertyKey)
142 6
                    && !Arr::allIn(static::$primaryDbPropertyKey, array_keys(static::$dbProperties)))
143 7
            ) {
144 1
                throw new \LogicException("All db entity's static properties not set");
145
            }
146 6
            $checkedClasses[] = static::class;
147 6
        }
148 64
    }
149
150
    /**
151
     * @param mixed $primaryDbValueOrRowData
152
     */
153 13
    public function setPrimaryDbValueOrRowData($primaryDbValueOrRowData = null)
154
    {
155
        // Row data would ba an associative array (not sequential, that would indicate a multi column primary key)
156 13
        if (is_array($primaryDbValueOrRowData) && !isset($primaryDbValueOrRowData[0])) {
157 1
            $this->setDbDataFromRow($primaryDbValueOrRowData);
158 1
        } else {
159 12
            $this->setPrimaryDbValue($primaryDbValueOrRowData);
160
        }
161 13
    }
162
163
    /**
164
     * Get all default database values.
165
     *
166
     * @return array
167
     */
168 64
    public function getDefaultDbData()
169
    {
170 64
        $class = get_called_class();
171 64
        if (!isset(self::$cachedDefaultDbData[$class])) {
172 6
            self::$cachedDefaultDbData[$class] = [];
173 6
            foreach (array_keys(static::$dbProperties) as $propertyName) {
174 6
                self::$cachedDefaultDbData[$class][$propertyName] = $this->getDefaultDbPropertyValue($propertyName);
175 6
            }
176 6
        }
177
178 64
        return self::$cachedDefaultDbData[$class];
179
    }
180
181
    /**
182
     * Get default db value (can be overridden if non static default values need to be used).
183
     *
184
     * @param string $propertyName
185
     * @return mixed
186
     */
187 6
    public function getDefaultDbPropertyValue($propertyName)
188
    {
189
        // A default value is set
190 6
        if (array_key_exists('default', static::$dbProperties[$propertyName])) {
191 4
            $defaultValue = static::$dbProperties[$propertyName]['default'];
192
        // No default value set, use default for type
193 4
        } else {
194 6
            $defaultValue = self::$typeDefaults[static::$dbProperties[$propertyName]['type']];
195
        }
196
197 6
        return $defaultValue;
198
    }
199
200
    /**
201
     * @return mixed
202
     */
203 21
    public function getPrimaryDbValue()
204
    {
205 21
        if (is_array(static::$primaryDbPropertyKey)) {
206 3
            $primaryValues = [];
207 3
            foreach (static::$primaryDbPropertyKey as $keyPart) {
208 3
                $primaryValues[] = $this->dbData[$keyPart];
209 3
            }
210
211 3
            return $primaryValues;
212
        }
213
214 18
        return $this->dbData[static::$primaryDbPropertyKey];
215
    }
216
217
    /**
218
     * @param mixed $primaryDbValue
219
     */
220 17
    public function setPrimaryDbValue($primaryDbValue)
221
    {
222 17
        if (is_array(static::$primaryDbPropertyKey)) {
223 3
            if (!is_array($primaryDbValue)) {
224 1
                throw new \InvalidArgumentException("Primary db value should be an array");
225
            }
226
227 2
            reset($primaryDbValue);
228 2
            foreach (static::$primaryDbPropertyKey as $keyPart) {
229 2
                $this->dbData[$keyPart] = current($primaryDbValue);
230 2
                next($primaryDbValue);
231 2
            }
232 2
        } else {
233 14
            $this->dbData[static::$primaryDbPropertyKey] = $primaryDbValue;
234
        }
235 16
    }
236
237
    /**
238
     * @return bool
239
     */
240 9
    public function isNewDbEntity()
241
    {
242 9
        if (is_array(static::$primaryDbPropertyKey)) {
243
            // Multiple column keys have to use explicit force insert because we have no way
244
            // to detect if it's a new entity (can't leave more than one primary field empty on insert because
245
            // db can't have two auto increment columns)
246 1
            throw new \LogicException("Can't detect if multi column primary key is a new entity");
247
        }
248
249 8
        return !$this->getPrimaryDbValue();
250
    }
251
252
    /**
253
     * @return bool
254
     */
255 8
    public function shouldInsertOnDbSave()
256
    {
257 8
        return (!is_array(static::$primaryDbPropertyKey) && $this->isNewDbEntity())
258 8
            || $this->shouldForceDbInsertOnSave();
259
    }
260
261
    /**
262
     * Set a row field value.
263
     *
264
     * @param string $property
265
     * @param mixed  $value
266
     * @param bool   $setAsModified
267
     * @param bool   $force
268
     */
269 24
    protected function setDbValue($property, $value, $setAsModified = true, $force = false)
270
    {
271 24
        if (!isset(static::$dbProperties[$property])) {
272 1
            throw new \InvalidArgumentException("No database entity property[{$property}] exists");
273
        }
274
275
        // Don't set type if value is null and allowed (allowed currently indicated by default => null)
276 23
        $nullIsAllowed = (array_key_exists('default', static::$dbProperties[$property])
277 23
            && static::$dbProperties[$property]['default'] === null);
278 23
        if (!($value === null && $nullIsAllowed)) {
279 23
            $type = static::$dbProperties[$property]['type'];
280
            // Set null when empty and default is null
281 23
            if ($value === '' && $nullIsAllowed) {
282 1
                 $value = null;
283 1
            } else {
284 23
                settype($value, $type);
285
            }
286 23
        }
287
288 23
        if ($this->dbData[$property] !== $value || $force) {
289 21
            $this->dbData[$property] = $value;
290
291 21
            if ($setAsModified && !$this->isDbPropertyModified($property)) {
292 14
                $this->modifiedDbProperties[] = $property;
293 14
            }
294 21
        }
295 23
    }
296
297
    /**
298
     * Get a database field value.
299
     *
300
     * @param string $property
301
     * @return mixed
302
     */
303 6
    protected function getDbValue($property)
304
    {
305 6
        return $this->dbData[$property];
306
    }
307
308
    /**
309
     * Get raw (with underscore as word separator as it is formatted in database)
310
     * field name from a object field property name (camelcased).
311
     *
312
     * @param string $propertyName
313
     * @return string
314
     */
315 18
    public static function getDbFieldName($propertyName)
316
    {
317 18
        if (!isset(self::$cachedDbFieldNames[$propertyName])) {
318 5
            self::$cachedDbFieldNames[$propertyName] = Str::camelToSeparator($propertyName);
319 5
        }
320
321 18
        return self::$cachedDbFieldNames[$propertyName];
322
    }
323
324
    /**
325
     * Get object field property name (camelCased) from database field name (underscore separated).
326
     *
327
     * @param string $dbFieldName
328
     * @return string
329
     */
330 7
    public static function getDbPropertyName($dbFieldName)
331
    {
332 7
        if (!isset(self::$cachedDbPropertyNames[$dbFieldName])) {
333 2
            self::$cachedDbPropertyNames[$dbFieldName] = Str::separatorToCamel($dbFieldName);
334 2
        }
335
336 7
        return self::$cachedDbPropertyNames[$dbFieldName];
337
    }
338
339
    /**
340
     * @return bool
341
     */
342 5
    public function hasModifiedDbProperties()
343
    {
344 5
        return !empty($this->modifiedDbProperties);
345
    }
346
347
    /**
348
     * @param string $property
349
     * @return bool
350
     */
351 15
    public function isDbPropertyModified($property)
352
    {
353 15
        return in_array($property, $this->modifiedDbProperties);
354
    }
355
356
    /**
357
     * @return array
358
     */
359 6
    public function getModifiedDbData()
360
    {
361 6
        return array_intersect_key($this->dbData, array_flip($this->modifiedDbProperties));
362
    }
363
364
    /**
365
     * @param string $property
366
     */
367
    public function clearModifiedDbProperty($property)
368
    {
369
        if (($key = array_search($property, $this->modifiedDbProperties))) {
370
            unset($this->modifiedDbProperties[$key]);
371
        }
372
    }
373
374 4
    public function clearModifiedDbProperties()
375
    {
376 4
        $this->modifiedDbProperties = [];
377 4
    }
378
379 1
    public function markAllDbPropertiesAsModified()
380
    {
381 1
        $this->modifiedDbProperties = array_keys(static::$dbProperties);
382 1
    }
383
384
    /**
385
     * Magic method used to automate getters & setters for row data.
386
     *
387
     * @param string $name
388
     * @param array  $arguments
389
     * @return mixed
390
     */
391 17
    public function __call($name, array $arguments = [])
392
    {
393 17
        $propertyName = lcfirst(substr($name, 3));
394
395 17
        if (strpos($name, 'get') === 0 && isset(static::$dbProperties[$propertyName])) {
396 4
            return $this->getDbValue($propertyName);
397 15
        } elseif (strpos($name, 'set') === 0 && isset(static::$dbProperties[$propertyName])) {
398 14
            $argumentCount = count($arguments);
399 14
            if ($argumentCount >= 1 && $argumentCount <= 3) {
400 13
                return $this->setDbValue($propertyName, ...$arguments);
401
            } else {
402 1
                throw new \BadMethodCallException("Invalid argument count[{$argumentCount}] for {$name}()");
403
            }
404
        } else {
405 1
            throw new \BadMethodCallException("No method named {$name}()");
406
        }
407
    }
408
409
    /**
410
     * Set database fields' data.
411
     *
412
     * @param array $data
413
     */
414 2
    public function setDbData(array $data)
415
    {
416 2 View Code Duplication
        foreach (array_keys(static::$dbProperties) as $propertyName) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
417 2
            if (array_key_exists($propertyName, $data)) {
418 2
                $this->setDbValue($propertyName, $data[$propertyName], true);
419 2
            }
420 2
        }
421 2
    }
422
423
    /**
424
     * Set db data from raw database row data with field names in database format.
425
     *
426
     * @param array $rowData
427
     */
428 7
    public function setDbDataFromRow(array $rowData)
429
    {
430
        // If there are less row data than properties, use rows as starting point (optimization)
431 7
        if (count($rowData) < count(static::$dbProperties)) {
432 6
            foreach ($rowData as $dbFieldName => $value) {
433 6
                $propertyName = static::getDbPropertyName($dbFieldName);
434 6
                if (isset(static::$dbProperties[$propertyName])) {
435 6
                    $this->setDbValue($propertyName, $value, false);
436 6
                }
437 6
            }
438
        // If there are more row data than properties, use properties as starting point
439 6
        } else {
440 2 View Code Duplication
            foreach (array_keys(static::$dbProperties) as $propertyName) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
441 2
                $fieldName = static::getDbFieldName($propertyName);
442 2
                if (array_key_exists($fieldName, $rowData)) {
443 2
                    $this->setDbValue($propertyName, $rowData[$fieldName], false);
444 2
                }
445 2
            }
446
        }
447 7
    }
448
449
    /**
450
     * @return array
451
     */
452 9
    public function getDbData()
453
    {
454 9
        return $this->dbData;
455
    }
456
457
    /**
458
     * @return array
459
     */
460 2
    public function getDbDataWithoutPrimary()
461
    {
462 2
        $dbDataWithoutPrimary = $this->dbData;
463
464 2
        if (is_array(static::$primaryDbPropertyKey)) {
465 1
            foreach (static::$primaryDbPropertyKey as $keyPart) {
466 1
                unset($dbDataWithoutPrimary[$keyPart]);
467 1
            }
468 1
        } else {
469 1
            unset($dbDataWithoutPrimary[static::$primaryDbPropertyKey]);
470
        }
471
472 2
        return $dbDataWithoutPrimary;
473
    }
474
475
    /**
476
     * @param bool $deleteFromDbOnSave
477
     */
478 3
    public function setDeleteFromDbOnSave($deleteFromDbOnSave = true)
479
    {
480 3
        $this->deleteFromDbOnSave = $deleteFromDbOnSave;
481 3
    }
482
483
    /**
484
     * @return bool
485
     */
486 10
    public function shouldBeDeletedFromDbOnSave()
487
    {
488 10
        return $this->deleteFromDbOnSave;
489
    }
490
491
    /**
492
     * @return bool
493
     */
494 1
    public function isDeleted()
495
    {
496 1
        return $this->deleted;
497
    }
498
499
    /**
500
     * @param bool $deleted
501
     */
502 3
    public function setDeleted($deleted = true)
503
    {
504 3
        $this->deleted = $deleted;
505 3
    }
506
507
    /**
508
     * @param bool $forceDbInsertOnSave
509
     */
510 5
    public function setForceDbInsertOnSave($forceDbInsertOnSave)
511
    {
512 5
        $this->forceDbInsertOnSave = $forceDbInsertOnSave;
513 5
    }
514
515
    /**
516
     * @return bool
517
     */
518 9
    public function shouldForceDbInsertOnSave()
519
    {
520 9
        return $this->forceDbInsertOnSave;
521
    }
522
523
    /**
524
     * @return array
525
     */
526 1
    public static function getDbProperties()
527
    {
528 1
        return static::$dbProperties;
529
    }
530
531
    /**
532
     * @param string $propertyName
533
     * @return int|null
534
     */
535 6
    public static function getDbPropertyMaxLength($propertyName)
536
    {
537 6
        return isset(static::$dbProperties[$propertyName]['maxLength'])
538 6
            ? static::$dbProperties[$propertyName]['maxLength']
539 6
            : null;
540
    }
541
542
    /**
543
     * @param string $propertyName
544
     * @return bool
545
     */
546 2
    public static function getDbPropertyRequired($propertyName)
547
    {
548 2
        return isset(static::$dbProperties[$propertyName]['required'])
549 2
            ? static::$dbProperties[$propertyName]['required']
550 2
            : false;
551
    }
552
553
    /**
554
     * @param string $propertyName
555
     * @return bool
556
     */
557 3
    public static function getDbPropertyNonEmpty($propertyName)
558
    {
559 3
        return isset(static::$dbProperties[$propertyName]['nonEmpty'])
560 3
            ? static::$dbProperties[$propertyName]['nonEmpty']
561 3
            : false;
562
    }
563
564
    /**
565
     * @return string|array
566
     */
567 17
    public static function getPrimaryDbPropertyKey()
568
    {
569 17
        return static::$primaryDbPropertyKey;
570
    }
571
572
    /**
573
     * @return string|array
574
     */
575 9
    public static function getPrimaryDbFieldKey()
576
    {
577 9
        $primaryDbPropertyKey = static::getPrimaryDbPropertyKey();
578
579 9
        if (is_array($primaryDbPropertyKey)) {
580 2
            $primaryDbFieldKey = [];
581 2
            foreach ($primaryDbPropertyKey as $propertyName) {
582 2
                $primaryDbFieldKey[] = static::getDbFieldName($propertyName);
583 2
            }
584
585 2
            return $primaryDbFieldKey;
586
        } else {
587 7
            return static::getDbFieldName($primaryDbPropertyKey);
588
        }
589
    }
590
591
    /**
592
     * Return array with db property names.
593
     *
594
     * @param array $exclude
595
     * @return array
596
     */
597 1
    public static function getDbPropertyNames(array $exclude = [])
598
    {
599 1
        $dbPropertyNames = array_keys(static::$dbProperties);
600
601 1
        return $exclude ? array_diff($dbPropertyNames, $exclude) : $dbPropertyNames;
602
    }
603
604
    /**
605
     * Return array with raw db field names.
606
     *
607
     * @param array $exclude
608
     * @return array
609
     */
610 3
    public static function getDbFieldNames(array $exclude = [])
611
    {
612 3
        $fieldNames = [];
613 3
        foreach (array_keys(static::$dbProperties) as $propertyName) {
614 3
            $fieldNames[] = static::getDbFieldName($propertyName);
615 3
        }
616
617 3
        return $exclude ? array_diff($fieldNames, $exclude) : $fieldNames;
618
    }
619
620
621
    /**
622
     * Get raw database field names prefixed (id, name becomes t.id, t.name etc.).
623
     *
624
     * @param string $dbTableAlias
625
     * @param array  $exclude
626
     * @return array
627
     */
628 1
    public static function getPrefixedDbFieldNames($dbTableAlias, array $exclude = [])
629
    {
630 1
        return Arr::valuesWithPrefix(static::getDbFieldNames($exclude), $dbTableAlias . '.');
631
    }
632
633
    /**
634
     * Get database columns transformed from e.g. "productId, date" to "p.product_id AS p_product_id, p.date AS p_date".
635
     *
636
     * @param string $dbTableAlias
637
     * @param array  $exclude
638
     * @return array
639
     */
640 1
    public static function getAliasedDbFieldNames($dbTableAlias, array $exclude = [])
641
    {
642 1
        $newArray = [];
643 1
        foreach (static::getDbFieldNames($exclude) as $dbFieldName) {
644 1
            $fromCol = $dbTableAlias . '.' . $dbFieldName;
645 1
            $toCol = $dbTableAlias . '_' . $dbFieldName;
646 1
            $newArray[] = $fromCol . ' AS ' . $toCol;
647 1
        }
648
649 1
        return $newArray;
650
    }
651
652
    /**
653
     * Filters a full db item array by it's table alias and the strips the table alias.
654
     *
655
     * @param array  $rowData
656
     * @param string $dbTableAlias
657
     * @param bool   $skipStrip For cases when you want to filter only (no stripping)
658
     * @return array
659
     */
660 1
    public static function filterStripDbRowData(array $rowData, $dbTableAlias, $skipStrip = false)
661
    {
662 1
        $columnPrefix = $dbTableAlias . '_';
663
664 1
        $filteredAndStrippedRowData = [];
665 1
        foreach ($rowData as $key => $val) {
666 1
            if (strpos($key, $columnPrefix) === 0) {
667 1
                $strippedKey = $skipStrip ? $key : Str::stripLeft($key, $columnPrefix);
668 1
                $filteredAndStrippedRowData[$strippedKey] = $val;
669 1
            }
670 1
        }
671
672 1
        return $filteredAndStrippedRowData;
673
    }
674
675
    /**
676
     * @return string
677
     */
678 8
    public static function getDbTableName()
679
    {
680 8
        return static::$dbTableName;
681
    }
682
683
    /**
684
     * Method to handle the serialization of this object.
685
     *
686
     * Implementation of Serializable interface. If descendant private properties
687
     * should be serialized, they need to be visible to this parent (i.e. not private).
688
     *
689
     * @return string
690
     */
691 2
    public function serialize()
692
    {
693 2
        return serialize(get_object_vars($this));
694
    }
695
696
    /**
697
     * Method to handle the unserialization of this object.
698
     *
699
     * Implementation of Serializable interface. If descendant private properties
700
     * should be unserialized, they need to be visible to this parent (i.e. not private).
701
     *
702
     * @param string $serializedObject
703
     */
704 1
    public function unserialize($serializedObject)
705
    {
706 1
        $objectVars = unserialize($serializedObject);
707
708 1
        foreach ($objectVars as $key => $value) {
709 1
            $this->{$key} = $value;
710 1
        }
711 1
    }
712
713
    /**
714
     * Merges other object's modified database data into this object.
715
     *
716
     * @param AbstractDbEntity $otherEntity
717
     */
718 1
    public function mergeWith(AbstractDbEntity $otherEntity)
719
    {
720 1
        $dataToMerge = $otherEntity->getModifiedDbData();
721 1
        $this->setDbData($dataToMerge);
722 1
    }
723
}
724