Passed
Branch develop (ee754c)
by Henry
13:32
created

ActiveRecord::_getFieldValue()   C

Complexity

Conditions 16
Paths 15

Size

Total Lines 46
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 29
CRAP Score 16.0094

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 16
eloc 32
c 1
b 0
f 0
nc 15
nop 2
dl 0
loc 46
ccs 29
cts 30
cp 0.9667
crap 16.0094
rs 5.5666

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * This file is part of the Divergence package.
4
 *
5
 * (c) Henry Paradiz <[email protected]>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
11
namespace Divergence\Models;
12
13
use Exception;
14
use ReflectionClass;
15
use JsonSerializable;
16
use Divergence\IO\Database\SQL;
17
use Divergence\Models\Mapping\Column;
18
use Divergence\Models\RecordValidator;
19
use Divergence\IO\Database\MySQL as DB;
20
use Divergence\Models\Mapping\Relation;
21
use Divergence\IO\Database\Query\Delete;
22
use Divergence\IO\Database\Query\Insert;
23
use Divergence\IO\Database\Query\Update;
24
use Divergence\Models\Mapping\DefaultGetMapper;
25
use Divergence\Models\Mapping\DefaultSetMapper;
26
27
/**
28
 * ActiveRecord
29
 *
30
 * @package Divergence
31
 * @author  Henry Paradiz <[email protected]>
32
 * @author  Chris Alfano <[email protected]>
33
 *
34
 * @property-read bool $isDirty      False by default. Set to true only when an object has had any field change from it's state when it was instantiated.
35
 * @property-read bool $isPhantom    True if this object was instantiated as a brand new object and isn't yet saved.
36
 * @property-read bool $wasPhantom   True if this object was originally instantiated as a brand new object. Will stay true even if saved during that PHP runtime.
37
 * @property-read bool $isValid      True if this object is valid. This value is true by default and will only be set to false if the validator is executed first and finds a validation problem.
38
 * @property-read bool $isNew        False by default. Set to true only when an object that isPhantom is saved.
39
 * @property-read bool $isUpdated    False by default. Set to true when an object that already existed in the data store is saved.
40
 *
41
 * @property int        $ID Default primary key field. Part of Divergence\Models\Model but used in this file as a default.
42
 * @property string     $Class Name of this fully qualified PHP class for use with subclassing to explicitly specify which class to instantiate a record as when pulling from datastore. Part of Divergence\Models\Model but used in this file as a default.
43
 * @property mixed      $Created Timestamp of when this record was created. Supports Unix timestamp as well as any format accepted by PHP's strtotime as well as MySQL standard. Part of Divergence\Models\Model but used in this file as a default.
44
 * @property int        $CreatorID A standard user ID field for use by your login & authentication system. Part of Divergence\Models\Model but used in this file as a default.
45
 *
46
 * @property-read array $validationErrors    An empty string by default. Returns validation errors as an array.
47
 * @property-read array $data                A plain PHP array of the fields and values for this model object.
48
 * @property-read array $originalValues      A plain PHP array of the fields and values for this model object when it was instantiated.
49
 *
50
 * @property array $versioningFields
51
 * @property-read array $_relatedObjects Relationship cache
52
 *
53
 * @method static void _defineRelationships()
54
 * @method static void _initRelationships()
55
 * @method static bool _relationshipExists(string $value)
56
 * @method static array<ActiveRecord>|ActiveRecord|null _getRelationshipValue(string $value)
57
 * @method void beforeVersionedSave()
58
 * @method void afterVersionedSave()
59
 * @method static string getHistoryTable()
60
 */
61
class ActiveRecord implements JsonSerializable
62
{
63
    /**
64
     * @var bool $autoCreateTables Set this to true if you want the table(s) to automatically be created when not found.
65
     */
66
    public static $autoCreateTables = true;
67
68
    /**
69
     * @var string $tableName Name of table
70
     */
71
    public static $tableName = 'records';
72
73
    /**
74
     *
75
     * @var string $singularNoun Noun to describe singular object
76
     */
77
    public static $singularNoun = 'record';
78
79
    /**
80
     *
81
     * @var string $pluralNoun Noun to describe a plurality of objects
82
     */
83
    public static $pluralNoun = 'records';
84
85
    /**
86
     *
87
     * @var array $fieldDefaults Defaults values for field definitions
88
     */
89
    public static $fieldDefaults = [
90
        'type' => 'string',
91
        'notnull' => true,
92
    ];
93
94
    /**
95
     * @var array $fields Field definitions
96
     */
97
    public static $fields = [];
98
99
    /**
100
     * @var array $indexes Index definitions
101
     */
102
    public static $indexes = [];
103
104
105
    /**
106
    * @var array $validators Validation checks
107
    */
108
    public static $validators = [];
109
110
    /**
111
     * @var array $relationships Relationship definitions
112
     */
113
    public static $relationships = [];
114
115
116
    /**
117
     * Class names of possible contexts
118
     * @var array
119
     */
120
    public static $contextClasses;
121
122
    /**
123
     *  @var string|null $primaryKey The primary key for this model. Optional. Defaults to ID
124
     */
125
    public static $primaryKey = null;
126
127
    /**
128
     *  @var string $handleField Field which should be treated as unique but generated automatically.
129
     */
130
    public static $handleField = 'Handle';
131
132
    /**
133
     *  @var null|string $rootClass The root class.
134
     */
135
    public static $rootClass = null;
136
137
    /**
138
     *  @var null|string $defaultClass The default class to be used when creating a new object.
139
     */
140
    public static $defaultClass = null;
141
142
    /**
143
     *  @var array $subClasses Array of class names providing valid classes you can use for this model.
144
     */
145
    public static $subClasses = [];
146
147
    /**
148
     *  @var callable $beforeSave Runs inside the save() method before save actually happens.
149
     */
150
    public static $beforeSave;
151
152
    /**
153
     *  @var callable $afterSave Runs inside the save() method after save if no exception was thrown.
154
     */
155
    public static $afterSave;
156
157
158
    // versioning
159
    public static $historyTable;
160
    public static $createRevisionOnDestroy = true;
161
    public static $createRevisionOnSave = true;
162
163
    /**
164
     * Internal registry of fields that comprise this class. The setting of this variable of every parent derived from a child model will get merged.
165
     *
166
     * @var array $_classFields
167
     */
168
    protected static $_classFields = [];
169
170
    /**
171
     * Internal registry of relationships that are part of this class. The setting of this variable of every parent derived from a child model will get merged.
172
     *
173
     * @var array $_classFields
174
     */
175
    protected static $_classRelationships = [];
176
177
    /**
178
     * Internal registry of before save PHP callables that are part of this class. The setting of this variable of every parent derived from a child model will get merged.
179
     *
180
     * @var array $_classBeforeSave
181
     */
182
    protected static $_classBeforeSave = [];
183
184
    /**
185
     * Internal registry of after save PHP callables that are part of this class. The setting of this variable of every parent derived from a child model will get merged.
186
     *
187
     * @var array $_classAfterSave
188
     */
189
    protected static $_classAfterSave = [];
190
191
    /**
192
     * Global registry of booleans that check if a given model has had it's fields defined in a static context. The key is a class name and the value is a simple true / false.
193
     *
194
     * @used-by ActiveRecord::init()
195
     *
196
     * @var array $_fieldsDefined
197
     */
198
    protected static $_fieldsDefined = [];
199
200
    /**
201
     * Global registry of booleans that check if a given model has had it's relationships defined in a static context. The key is a class name and the value is a simple true / false.
202
     *
203
     * @used-by ActiveRecord::init()
204
     *
205
     * @var array $_relationshipsDefined
206
     */
207
    protected static $_relationshipsDefined = [];
208
    protected static $_eventsDefined = [];
209
210
    /**
211
     * @var array $_record Raw array data for this model.
212
     */
213
    protected $_record;
214
215
    /**
216
     * @var array $_convertedValues Raw array data for this model of data normalized for it's field type.
217
     */
218
    protected $_convertedValues;
219
220
    /**
221
     * @var RecordValidator $_validator Instance of a RecordValidator object.
222
     */
223
    protected $_validator;
224
225
    /**
226
     * @var array $_validationErrors Array of validation errors if there are any.
227
     */
228
    protected $_validationErrors;
229
230
    /**
231
     * @var array $_originalValues If any values have been changed the initial value is stored here.
232
     */
233
    protected $_originalValues;
234
235
236
    /**
237
     * False by default. Set to true only when an object has had any field change from it's state when it was instantiated.
238
     *
239
     * @var bool $_isDirty
240
     *
241
     * @used-by $this->save()
242
     * @used-by $this->__get()
243
     */
244
    protected $_isDirty;
245
246
    /**
247
     * True if this object was instantiated as a brand new object and isn't yet saved.
248
     *
249
     * @var bool $_isPhantom
250
     *
251
     * @used-by $this->save()
252
     * @used-by $this->__get()
253
     */
254
    protected $_isPhantom;
255
256
    /**
257
     * True if this object was originally instantiated as a brand new object. Will stay true even if saved during that PHP runtime.
258
     *
259
     * @var bool $_wasPhantom
260
     *
261
     * @used-by $this->__get()
262
     */
263
    protected $_wasPhantom;
264
265
    /**
266
     * True if this object is valid. This value is true by default and will only be set to false if the validator is executed first and finds a validation problem.
267
     *
268
     * @var bool $_isValid
269
     *
270
     * @used-by $this->__get()
271
     */
272
    protected $_isValid;
273
274
    /**
275
     * False by default. Set to true only when an object that isPhantom is saved.
276
     *
277
     * @var bool $_isNew
278
     *
279
     * @used-by $this->save()
280
     * @used-by $this->__get()
281
     */
282
    protected $_isNew;
283
284
    /**
285
     * False by default. Set to true when an object that already existed in the data store is saved.
286
     *
287
     * @var bool $_isUpdated
288
     *
289
     * @used-by $this->__get()
290
     */
291
    protected $_isUpdated;
292
293
294
    public const defaultSetMapper = DefaultSetMapper::class;
295
    public const defaultGetMapper = DefaultGetMapper::class;
296
    /**
297
     * __construct Instantiates a Model and returns.
298
     *
299
     * @param array $record Raw array data to start off the model.
300
     * @param boolean $isDirty Whether or not to treat this object as if it was modified from the start.
301
     * @param boolean $isPhantom Whether or not to treat this object as a brand new record not yet in the database.
302
     *
303
     * @uses static::init
304
     *
305
     * @return static Instance of the value of $this->Class
306
     */
307 109
    public function __construct($record = [], $isDirty = false, $isPhantom = null)
308
    {
309 109
        $this->_record = $record;
310 109
        $this->_isPhantom = isset($isPhantom) ? $isPhantom : empty($record);
311 109
        $this->_wasPhantom = $this->_isPhantom;
312 109
        $this->_isDirty = $this->_isPhantom || $isDirty;
313 109
        $this->_isNew = false;
314 109
        $this->_isUpdated = false;
315
316 109
        $this->_isValid = true;
317 109
        $this->_validationErrors = [];
318 109
        $this->_originalValues = [];
319
320 109
        static::init();
321
322
        // set Class
323 109
        if (static::fieldExists('Class') && !$this->Class) {
324 109
            $this->_setFieldValue('Class', get_class($this));
325
        }
326
    }
327
328
    /**
329
     * __get Passthru to getValue($name)
330
     *
331
     * @param string $name Name of the magic field you want.
332
     *
333
     * @return mixed The return of $this->getValue($name)
334
     */
335 82
    public function __get($name)
336
    {
337 82
        return $this->getValue($name);
338
    }
339
340
    /**
341
     * Passthru to setValue($name,$value)
342
     *
343
     * @param string $name Name of the magic field to set.
344
     * @param mixed $value Value to set.
345
     *
346
     * @return mixed The return of $this->setValue($name,$value)
347
     */
348 4
    public function __set($name, $value)
349
    {
350 4
        return $this->setValue($name, $value);
351
    }
352
353
    /**
354
     * Tests if a magic class attribute is set or not.
355
     *
356
     * @param string $name Name of the magic field to set.
357
     *
358
     * @return bool Returns true if a value was returned by $this->getValue($name), false otherwise.
359
     */
360 47
    public function __isset($name)
361
    {
362 47
        $value = $this->getValue($name);
363 47
        return isset($value);
364
    }
365
366
    /**
367
     * Gets the primary key field for his model.
368
     *
369
     * @return string ID by default or static::$primaryKey if it's set.
370
     */
371 28
    public static function getPrimaryKey()
372
    {
373 28
        return isset(static::$primaryKey) ? static::$primaryKey : 'ID';
374
    }
375
376
    /**
377
     * Gets the primary key value for his model.
378
     *
379
     * @return mixed The primary key value for this object.
380
     */
381 17
    public function getPrimaryKeyValue()
382
    {
383 17
        if (isset(static::$primaryKey)) {
384 2
            return $this->{static::$primaryKey} ?? $this->_getFieldValue(static::$primaryKey);
385
        } else {
386 17
            return $this->_getFieldValue('ID');
387
        }
388
    }
389
390
    /**
391
     * init Initializes the model by checking the ancestor tree for the existence of various config fields and merges them.
392
     *
393
     * @uses static::$_fieldsDefined Sets static::$_fieldsDefined[get_called_class()] to true after running.
394
     * @uses static::$_relationshipsDefined Sets static::$_relationshipsDefined[get_called_class()] to true after running.
395
     * @uses static::$_eventsDefined Sets static::$_eventsDefined[get_called_class()] to true after running.
396
     *
397
     * @used-by static::__construct()
398
     * @used-by static::fieldExists()
399
     * @used-by static::getClassFields()
400
     * @used-by static::getColumnName()
401
     *
402
     * @return void
403
     */
404 125
    public static function init()
405
    {
406 125
        $className = get_called_class();
407 125
        if (empty(static::$_fieldsDefined[$className])) {
408 7
            static::_defineFields();
409 7
            static::_initFields();
410
411 7
            static::$_fieldsDefined[$className] = true;
412
        }
413 125
        if (empty(static::$_relationshipsDefined[$className]) && static::isRelational()) {
414 3
            static::_defineRelationships();
415 3
            static::_initRelationships();
416
417 3
            static::$_relationshipsDefined[$className] = true;
418
        }
419
420 125
        if (empty(static::$_eventsDefined[$className])) {
421 7
            static::_defineEvents();
422
423 7
            static::$_eventsDefined[$className] = true;
424
        }
425
    }
426
427
    /**
428
     * getValue Pass thru for __get
429
     *
430
     * @param string $name The name of the field you want to get.
431
     *
432
     * @return mixed Value of the field you wanted if it exists or null otherwise.
433
     */
434 93
    public function getValue($name)
435
    {
436
        switch ($name) {
437 93
            case 'isDirty':
438 23
                return $this->_isDirty;
439
440 93
            case 'isPhantom':
441 24
                return $this->_isPhantom;
442
443 93
            case 'wasPhantom':
444 3
                return $this->_wasPhantom;
445
446 93
            case 'isValid':
447 3
                return $this->_isValid;
448
449 93
            case 'isNew':
450 3
                return $this->_isNew;
451
452 93
            case 'isUpdated':
453 3
                return $this->_isUpdated;
454
455 93
            case 'validationErrors':
456 38
                return array_filter($this->_validationErrors);
457
458 91
            case 'data':
459 25
                return $this->getData();
460
461 79
            case 'originalValues':
462 3
                return $this->_originalValues;
463
464
            default:
465
                {
466
                    // handle field
467 79
                    if (static::fieldExists($name)) {
468 48
                        return $this->_getFieldValue($name);
469
                    }
470
                    // handle relationship
471 52
                    elseif (static::isRelational()) {
472 13
                        if (static::_relationshipExists($name)) {
473 13
                            return $this->_getRelationshipValue($name);
474
                        }
475
                    }
476
                    // default Handle to ID if not caught by fieldExists
477 39
                    elseif ($name == static::$handleField) {
478 1
                        return $this->_getFieldValue('ID');
479
                    }
480
                }
481
        }
482
        // undefined
483 47
        return null;
484
    }
485
486
    /**
487
     * Sets a value on this model.
488
     *
489
     * @param string $name
490
     * @param mixed $value
491
     * @return void|false False if the field does not exist. Void otherwise.
492
     */
493 7
    public function setValue($name, $value)
494
    {
495
        // handle field
496 7
        if (static::fieldExists($name)) {
497 7
            $this->_setFieldValue($name, $value);
498
        }
499
        // undefined
500
        else {
501 1
            return false;
502
        }
503
    }
504
505
    /**
506
     * Checks if this model is versioned.
507
     *
508
     * @return boolean Returns true if this class is defined with Divergence\Models\Versioning as a trait.
509
     */
510 112
    public static function isVersioned()
511
    {
512 112
        return in_array('Divergence\\Models\\Versioning', class_uses(get_called_class()));
513
    }
514
515
    /**
516
     * Checks if this model is ready for relationships.
517
     *
518
     * @return boolean Returns true if this class is defined with Divergence\Models\Relations as a trait.
519
     */
520 126
    public static function isRelational()
521
    {
522 126
        return in_array('Divergence\\Models\\Relations', class_uses(get_called_class()));
523
    }
524
525
    /**
526
     * Create a new object from this model.
527
     *
528
     * @param array $values Array of keys as fields and values.
529
     * @param boolean $save If the object should be immediately saved to database before being returned.
530
     * @return static An object of this model.
531
     */
532 26
    public static function create($values = [], $save = false)
533
    {
534 26
        $className = get_called_class();
535
536
        // create class
537
        /** @var ActiveRecord */
538 26
        $ActiveRecord = new $className();
539 26
        $ActiveRecord->setFields($values);
540
541 26
        if ($save) {
542 4
            $ActiveRecord->save();
543
        }
544
545 26
        return $ActiveRecord;
546
    }
547
548
    /**
549
     * Checks if a model is a certain class.
550
     * @param string $class Check if the model matches this class.
551
     * @return boolean True if model matches the class provided. False otherwise.
552
     */
553 1
    public function isA($class): bool
554
    {
555 1
        return is_a($this, $class);
556
    }
557
558
    /**
559
     * Used to instantiate a new model of a different class with this model's field's. Useful when you have similar classes or subclasses with the same parent.
560
     *
561
     * @param string $className If you leave this blank the return will be $this
562
     * @param array $fieldValues Optional. Any field values you want to override.
563
     * @return static A new model of a different class with this model's field's. Useful when you have similar classes or subclasses with the same parent.
564
     */
565 2
    public function changeClass($className = false, $fieldValues = false)
566
    {
567 2
        if (!$className) {
568 1
            return $this;
569
        }
570
571 2
        $this->_record[static::_cn('Class')] = $className;
572 2
        $ActiveRecord = new $className($this->_record, true, $this->isPhantom);
573
574 2
        if ($fieldValues) {
575 1
            $ActiveRecord->setFields($fieldValues);
576
        }
577
578 2
        if (!$this->isPhantom) {
579
            $ActiveRecord->save();
580
        }
581
582 2
        return $ActiveRecord;
583
    }
584
585
    /**
586
     * Change multiple fields in the model with an array.
587
     *
588
     * @param array $values Field/values array to change multiple fields in this model.
589
     * @return void
590
     */
591 37
    public function setFields($values)
592
    {
593 37
        foreach ($values as $field => $value) {
594 34
            $this->_setFieldValue($field, $value);
595
        }
596
    }
597
598
    /**
599
     * Change one field in the model.
600
     *
601
     * @param string $field
602
     * @param mixed $value
603
     * @return void
604
     */
605 1
    public function setField($field, $value)
606
    {
607 1
        $this->_setFieldValue($field, $value);
608
    }
609
610
    /**
611
     * Implements JsonSerializable for this class.
612
     *
613
     * @return array Return for extension JsonSerializable
614
     */
615 6
    public function jsonSerialize(): array
616
    {
617 6
        return $this->getData();
618
    }
619
620
    /**
621
     *  Gets normalized object data.
622
     *
623
     *  @return array The model's data as a normal array with any validation errors included.
624
     */
625 38
    public function getData(): array
626
    {
627 38
        $data = [];
628
629 38
        foreach (static::$_classFields[get_called_class()] as $field => $options) {
630 38
            $data[$field] = $this->_getFieldValue($field);
631
        }
632
633 38
        if ($this->validationErrors) {
634 1
            $data['validationErrors'] = $this->validationErrors;
635
        }
636
637 38
        return $data;
638
    }
639
640
    /**
641
     * Checks if a field has been changed from it's value when this object was created.
642
     *
643
     * @param string $field
644
     * @return boolean
645
     */
646 1
    public function isFieldDirty($field): bool
647
    {
648 1
        return $this->isPhantom || array_key_exists($field, $this->_originalValues);
649
    }
650
651
    /**
652
     * Gets values that this model was instantiated with for a given field.
653
     *
654
     * @param string $field Field name
655
     * @return mixed
656
     */
657 1
    public function getOriginalValue($field)
658
    {
659 1
        return $this->_originalValues[$field];
660
    }
661
662
    /**
663
     * Fires a DB::clearCachedRecord a key static::$tableName.'/'.static::getPrimaryKey()
664
     *
665
     * @return void
666
     */
667 20
    public function clearCaches()
668
    {
669 20
        foreach ($this->getClassFields() as $field => $options) {
670 20
            if (!empty($options['unique']) || !empty($options['primary'])) {
671 20
                $key = sprintf('%s/%s', static::$tableName, $field);
672 20
                DB::clearCachedRecord($key);
673
            }
674
        }
675
    }
676
677
    /**
678
     * Runs the before save event function one at a time for any class that had $beforeSave configured in the ancestor tree.
679
     */
680 21
    public function beforeSave()
681
    {
682 21
        foreach (static::$_classBeforeSave as $beforeSave) {
683 1
            if (is_callable($beforeSave)) {
684 1
                $beforeSave($this);
685
            }
686
        }
687
    }
688
689
    /**
690
     * Runs the after save event function one at a time for any class that had $beforeSave configured in the ancestor tree. Will only fire if save was successful.
691
     */
692 19
    public function afterSave()
693
    {
694 19
        foreach (static::$_classAfterSave as $afterSave) {
695 1
            if (is_callable($afterSave)) {
696 1
                $afterSave($this);
697
            }
698
        }
699
    }
700
701
    /**
702
     * Saves this object to the database currently in use.
703
     *
704
     * @param bool $deep Default is true. When true will try to save any dirty models in any defined and initialized relationships.
705
     *
706
     * @uses $this->_isPhantom
707
     * @uses $this->_isDirty
708
     */
709 20
    public function save($deep = true)
710
    {
711
        // run before save
712 20
        $this->beforeSave();
713
714 20
        if (static::isVersioned()) {
715 13
            $this->beforeVersionedSave();
716
        }
717
718
        // set created
719 20
        if (static::fieldExists('Created') && (!$this->Created || ($this->Created == 'CURRENT_TIMESTAMP'))) {
720 8
            $this->Created = $this->_record['Created'] = time();
721 8
            unset($this->_convertedValues['Created']);
722
        }
723
724
        // validate
725 20
        if (!$this->validate($deep)) {
726
            throw new Exception('Cannot save invalid record');
727
        }
728
729 20
        $this->clearCaches();
730
731 20
        if ($this->isDirty) {
732
            // prepare record values
733 19
            $recordValues = $this->_prepareRecordValues();
734
735
            // transform record to set array
736 19
            $set = static::_mapValuesToSet($recordValues);
737
738
            // create new or update existing
739 19
            if ($this->_isPhantom) {
740 15
                DB::nonQuery((new Insert())->setTable(static::$tableName)->set($set), null, [static::class,'handleException']);
741 14
                $primaryKey = $this->getPrimaryKey();
742 14
                $insertID = DB::insertID();
743 14
                $fields = static::getClassFields();
744 14
                if (($fields[$primaryKey]['type'] ?? false) === 'integer') {
745 14
                    $insertID = intval($insertID);
746
                }
747 14
                $this->_record[$primaryKey] = $insertID;
748 14
                $this->$primaryKey = $insertID;
749 14
                $this->_isPhantom = false;
750 14
                $this->_isNew = true;
751 5
            } elseif (count($set)) {
752 5
                DB::nonQuery((new Update())->setTable(static::$tableName)->set($set)->where(
753 5
                    sprintf('`%s` = %u', static::_cn($this->getPrimaryKey()), (string)$this->getPrimaryKeyValue())
754 5
                ), null, [static::class,'handleException']);
755
756 4
                $this->_isUpdated = true;
757
            }
758
759
            // update state
760 17
            $this->_isDirty = false;
761 17
            if (static::isVersioned()) {
762 10
                $this->afterVersionedSave();
763
            }
764
        }
765 18
        $this->afterSave();
766
    }
767
768
769
    /**
770
     * Deletes this object.
771
     *
772
     * @return bool True if database returns number of affected rows above 0. False otherwise.
773
     */
774 9
    public function destroy(): bool
775
    {
776 9
        if (static::isVersioned()) {
777 5
            if (static::$createRevisionOnDestroy) {
778
                // save a copy to history table
779 5
                if ($this->fieldExists('Created')) {
780 5
                    $this->Created = time();
781
                }
782
783 5
                $recordValues = $this->_prepareRecordValues();
784 5
                $set = static::_mapValuesToSet($recordValues);
785
786 5
                DB::nonQuery((new Insert())->setTable(static::getHistoryTable())->set($set), null, [static::class,'handleException']);
787
            }
788
        }
789
790 8
        return static::delete((string)$this->getPrimaryKeyValue());
791
    }
792
793
    /**
794
     * Delete by ID
795
     *
796
     * @param int|string $id
797
     * @return bool True if database returns number of affected rows above 0. False otherwise.
798
     */
799 8
    public static function delete($id): bool
800
    {
801 8
        DB::nonQuery((new Delete())->setTable(static::$tableName)->where(sprintf('`%s` = %u', static::_cn(static::$primaryKey ? static::$primaryKey : 'ID'), $id)), null, [static::class,'handleException']);
802
803 8
        return DB::affectedRows() > 0;
804
    }
805
806
    /**
807
     * Checks of a field exists for this model in the fields config.
808
     *
809
     * @param string $field Name of the field
810
     * @return bool True if the field exists. False otherwise.
811
     */
812 125
    public static function fieldExists($field): bool
813
    {
814 125
        static::init();
815 125
        return array_key_exists($field, static::$_classFields[get_called_class()]);
816
    }
817
818
    /**
819
     * Returns the current configuration of class fields for the called class.
820
     *
821
     * @return array Current configuration of class fields for the called class.
822
     */
823 21
    public static function getClassFields(): array
824
    {
825 21
        static::init();
826 21
        return static::$_classFields[get_called_class()];
827
    }
828
829
    /**
830
     * Returns either a field option or an array of all the field options.
831
     *
832
     * @param string $field Name of the field.
833
     * @param boolean $optionKey
834
     * @return array|mixed
835
     */
836 1
    public static function getFieldOptions($field, $optionKey = false)
837
    {
838 1
        if ($optionKey) {
839 1
            return static::$_classFields[get_called_class()][$field][$optionKey];
840
        } else {
841 1
            return static::$_classFields[get_called_class()][$field];
842
        }
843
    }
844
845
    /**
846
     * Returns columnName for given field
847
     * @param string $field name of field
848
     * @return string column name
849
     */
850 122
    public static function getColumnName($field)
851
    {
852 122
        static::init();
853 122
        if (!static::fieldExists($field)) {
854 2
            throw new Exception('getColumnName called on nonexisting column: ' . get_called_class().'->'.$field);
855
        }
856
857 121
        return static::$_classFields[get_called_class()][$field]['columnName'];
858
    }
859
860 2
    public static function mapFieldOrder($order)
861
    {
862 2
        return static::_mapFieldOrder($order);
863
    }
864
865 1
    public static function mapConditions($conditions)
866
    {
867 1
        return static::_mapConditions($conditions);
868
    }
869
870
    /**
871
     * Returns static::$rootClass for the called class.
872
     *
873
     * @return string static::$rootClass for the called class.
874
     */
875 1
    public function getRootClass(): string
876
    {
877 1
        return static::$rootClass;
0 ignored issues
show
Bug Best Practice introduced by
The expression return static::rootClass could return the type null which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
878
    }
879
880
    /**
881
     * Sets an array of validation errors for this object.
882
     *
883
     * @param array $array Validation errors in the form Field Name => error message
884
     * @return void
885
     */
886 2
    public function addValidationErrors($array)
887
    {
888 2
        foreach ($array as $field => $errorMessage) {
889 2
            $this->addValidationError($field, $errorMessage);
890
        }
891
    }
892
893
    /**
894
     * Sets a validation error for this object. Sets $this->_isValid to false.
895
     *
896
     * @param string $field
897
     * @param string $errorMessage
898
     * @return void
899
     */
900 2
    public function addValidationError($field, $errorMessage)
901
    {
902 2
        $this->_isValid = false;
903 2
        $this->_validationErrors[$field] = $errorMessage;
904
    }
905
906
    /**
907
     * Get a validation error for a given field.
908
     *
909
     * @param string $field Name of the field.
910
     * @return string|null A validation error for the field. Null is no validation error found.
911
     */
912 1
    public function getValidationError($field)
913
    {
914
        // break apart path
915 1
        $crumbs = explode('.', $field);
916
917
        // resolve path recursively
918 1
        $cur = &$this->_validationErrors;
919 1
        while ($crumb = array_shift($crumbs)) {
920 1
            if (array_key_exists($crumb, $cur)) {
921 1
                $cur = &$cur[$crumb];
922
            } else {
923 1
                return null;
924
            }
925
        }
926
927
        // return current value
928 1
        return $cur;
929
    }
930
931
    /**
932
     * Validates the model. Instantiates a new RecordValidator object and sets it to $this->_validator.
933
     * Then validates against the set validators in this model. Returns $this->_isValid
934
     *
935
     * @param boolean $deep If true will attempt to validate any already loaded relationship members.
936
     * @return bool $this->_isValid which could be set to true or false depending on what happens with the RecordValidator.
937
     */
938 20
    public function validate($deep = true)
939
    {
940 20
        $this->_isValid = true;
941 20
        $this->_validationErrors = [];
942
943 20
        if (!isset($this->_validator)) {
944 20
            $this->_validator = new RecordValidator($this->_record);
945
        } else {
946 6
            $this->_validator->resetErrors();
947
        }
948
949 20
        foreach (static::$validators as $validator) {
950
            $this->_validator->validate($validator);
951
        }
952
953 20
        $this->finishValidation();
954
955 20
        if ($deep) {
956
            // validate relationship objects
957 20
            if (!empty(static::$_classRelationships[get_called_class()])) {
958 1
                foreach (static::$_classRelationships[get_called_class()] as $relationship => $options) {
959 1
                    if (empty($this->_relatedObjects[$relationship])) {
960 1
                        continue;
961
                    }
962
963
964 1
                    if ($options['type'] == 'one-one') {
965
                        if ($this->_relatedObjects[$relationship]->isDirty) {
966
                            $this->_relatedObjects[$relationship]->validate();
967
                            $this->_isValid = $this->_isValid && $this->_relatedObjects[$relationship]->isValid;
968
                            $this->_validationErrors[$relationship] = $this->_relatedObjects[$relationship]->validationErrors;
969
                        }
970 1
                    } elseif ($options['type'] == 'one-many') {
971 1
                        foreach ($this->_relatedObjects[$relationship] as $i => $object) {
972 1
                            if ($object->isDirty) {
973
                                $object->validate();
974
                                $this->_isValid = $this->_isValid && $object->isValid;
975
                                $this->_validationErrors[$relationship][$i] = $object->validationErrors;
976
                            }
977
                        }
978
                    }
979
                } // foreach
980
            } // if
981
        } // if ($deep)
982
983 20
        return $this->_isValid;
984
    }
985
986
    /**
987
     * Handle any errors that come from the database client in the process of running a query.
988
     * If the error code from MySQL 42S02 (table not found) is thrown this method will attempt to create the table before running the original query and returning.
989
     * Other errors will be routed through to DB::handleException
990
     *
991
     * @param Exception $exception
992
     * @param string $query
993
     * @param array $queryLog
994
     * @param array|string $parameters
995
     * @return mixed Retried query result or the return from DB::handleException
996
     */
997 7
    public static function handleException(\Exception $e, $query = null, $queryLog = null, $parameters = null)
998
    {
999 7
        $Connection = DB::getConnection();
1000 7
        if ($Connection->errorCode() == '42S02' && static::$autoCreateTables) {
1001 2
            $CreateTable = SQL::getCreateTable(static::$rootClass);
1002
1003
            // history versions table
1004 2
            if (static::isVersioned()) {
1005 1
                $CreateTable .= SQL::getCreateTable(static::$rootClass, true);
1006
            }
1007
1008 2
            $Statement = $Connection->query($CreateTable);
1009
1010
            // check for errors
1011 2
            $ErrorInfo = $Statement->errorInfo();
1012
1013
            // handle query error
1014 2
            if ($ErrorInfo[0] != '00000') {
1015
                self::handleException($query, $queryLog);
0 ignored issues
show
Bug introduced by
It seems like $queryLog can also be of type array; however, parameter $query of Divergence\Models\ActiveRecord::handleException() does only seem to accept string, 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

1015
                self::handleException($query, /** @scrutinizer ignore-type */ $queryLog);
Loading history...
Bug introduced by
$query of type null|string is incompatible with the type Exception expected by parameter $e of Divergence\Models\ActiveRecord::handleException(). ( Ignorable by Annotation )

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

1015
                self::handleException(/** @scrutinizer ignore-type */ $query, $queryLog);
Loading history...
1016
            }
1017
1018
            // clear buffer (required for the next query to work without running fetchAll first
1019 2
            $Statement->closeCursor();
1020
1021 2
            return $Connection->query((string)$query); // now the query should finish with no error
1022
        } else {
1023 5
            return DB::handleException($e, $query, $queryLog);
1024
        }
1025
    }
1026
1027
    /**
1028
     * Iterates through all static::$beforeSave and static::$afterSave in this class and any of it's parent classes.
1029
     * Checks if they are callables and if they are adds them to static::$_classBeforeSave[] and static::$_classAfterSave[]
1030
     *
1031
     * @return void
1032
     *
1033
     * @uses static::$beforeSave
1034
     * @uses static::$afterSave
1035
     * @uses static::$_classBeforeSave
1036
     * @uses static::$_classAfterSave
1037
     */
1038 7
    protected static function _defineEvents()
1039
    {
1040
        // run before save
1041 7
        $className = get_called_class();
1042
1043
        // merge fields from first ancestor up
1044 7
        $classes = class_parents($className);
1045 7
        array_unshift($classes, $className);
1046
1047 7
        while ($class = array_pop($classes)) {
1048 7
            if (is_callable($class::$beforeSave)) {
1049
                if (!empty($class::$beforeSave)) {
1050
                    if (!in_array($class::$beforeSave, static::$_classBeforeSave)) {
1051
                        static::$_classBeforeSave[] = $class::$beforeSave;
1052
                    }
1053
                }
1054
            }
1055
1056 7
            if (is_callable($class::$afterSave)) {
1057
                if (!empty($class::$afterSave)) {
1058
                    if (!in_array($class::$afterSave, static::$_classAfterSave)) {
1059
                        static::$_classAfterSave[] = $class::$afterSave;
1060
                    }
1061
                }
1062
            }
1063
        }
1064
    }
1065
1066
    /**
1067
     * Merges all static::$_classFields in this class and any of it's parent classes.
1068
     * Sets the merged value to static::$_classFields[get_called_class()]
1069
     *
1070
     * @return void
1071
     *
1072
     * @uses static::$_classFields
1073
     * @uses static::$_classFields
1074
     */
1075 7
    protected static function _defineFields()
1076
    {
1077 7
        $className = get_called_class();
1078
1079
        // skip if fields already defined
1080 7
        if (isset(static::$_classFields[$className])) {
1081
            return;
1082
        }
1083
1084
        // merge fields from first ancestor up
1085 7
        $classes = class_parents($className);
1086 7
        array_unshift($classes, $className);
1087
1088 7
        static::$_classFields[$className] = [];
1089 7
        while ($class = array_pop($classes)) {
1090 7
            if (!empty($class::$fields)) {
1091
                static::$_classFields[$className] = array_merge(static::$_classFields[$className], $class::$fields);
1092
            }
1093 7
            $attributeFields = $class::_definedAttributeFields();
1094 7
            if (!empty($attributeFields['fields'])) {
1095 6
                static::$_classFields[$className] = array_merge(static::$_classFields[$className], $attributeFields['fields']);
1096
            }
1097 7
            if (!empty($attributeFields['relations'])) {
1098 1
                $class::$relationships = $attributeFields['relations'];
1099
            }
1100
        }
1101
    }
1102
1103
    /**
1104
     * This function grabs all protected fields on the model and uses that as the basis for what constitutes a mapped field
1105
     * It skips a certain list of protected fields that are built in for ORM operation
1106
     *
1107
     * @return array
1108
     */
1109 7
    public static function _definedAttributeFields(): array
1110
    {
1111 7
        $fields = [];
1112 7
        $relations = [];
1113 7
        $properties = (new ReflectionClass(static::class))->getProperties();
1114 7
        if (!empty($properties)) {
1115 7
            foreach ($properties as $property) {
1116 7
                if ($property->isProtected()) {
1117
1118
                    // skip these because they are built in
1119 7
                    if (in_array($property->getName(), [
1120 7
                        '_classFields','_classRelationships','_classBeforeSave','_classAfterSave','_fieldsDefined','_relationshipsDefined','_eventsDefined','_record','_validator'
1121 7
                        ,'_validationErrors','_isDirty','_isValid','_convertedValues','_originalValues','_isPhantom','_wasPhantom','_isNew','_isUpdated','_relatedObjects'
1122 7
                    ])) {
1123 7
                        continue;
1124
                    }
1125
1126 6
                    $isRelationship = false;
1127
1128 6
                    if ($attributes = $property->getAttributes()) {
1129 6
                        foreach ($attributes as $attribute) {
1130 6
                            $attributeName = $attribute->getName();
1131 6
                            if ($attributeName === Column::class) {
1132 6
                                $fields[$property->getName()] = array_merge($attribute->getArguments(), ['attributeField'=>true]);
1133
                            }
1134
1135 6
                            if ($attributeName === Relation::class) {
1136 1
                                $isRelationship = true;
1137 1
                                $relations[$property->getName()] = $attribute->getArguments();
1138
                            }
1139
                        }
1140
                    } else {
1141
                        // default
1142 4
                        if (!$isRelationship) {
1143 4
                            $fields[$property->getName()] = [];
1144
                        }
1145
                    }
1146
                }
1147
            }
1148
        }
1149 7
        return [
1150 7
            'fields' => $fields,
1151 7
            'relations' => $relations
1152 7
        ];
1153
    }
1154
1155
1156
    /**
1157
     * Called after _defineFields to initialize and apply defaults to the fields property
1158
     * Must be idempotent as it may be applied multiple times up the inheritance chain
1159
     * @return void
1160
     *
1161
     * @uses static::$_classFields
1162
     */
1163 7
    protected static function _initFields()
1164
    {
1165 7
        $className = get_called_class();
1166 7
        $optionsMask = [
1167 7
            'type' => null,
1168 7
            'length' => null,
1169 7
            'primary' => null,
1170 7
            'unique' => null,
1171 7
            'autoincrement' => null,
1172 7
            'notnull' => null,
1173 7
            'unsigned' => null,
1174 7
            'default' => null,
1175 7
            'values' => null,
1176 7
        ];
1177
1178
        // apply default values to field definitions
1179 7
        if (!empty(static::$_classFields[$className])) {
1180 6
            $fields = [];
1181
1182 6
            foreach (static::$_classFields[$className] as $field => $options) {
1183 6
                if (is_string($field)) {
1184 6
                    if (is_array($options)) {
1185 6
                        $fields[$field] = array_merge($optionsMask, static::$fieldDefaults, ['columnName' => $field], $options);
1186
                    } elseif (is_string($options)) {
1187
                        $fields[$field] = array_merge($optionsMask, static::$fieldDefaults, ['columnName' => $field, 'type' => $options]);
1188
                    } elseif ($options == null) {
1189 6
                        continue;
1190
                    }
1191
                } elseif (is_string($options)) {
1192
                    $field = $options;
1193
                    $fields[$field] = array_merge($optionsMask, static::$fieldDefaults, ['columnName' => $field]);
1194
                }
1195
1196 6
                if ($field == 'Class') {
1197
                    // apply Class enum values
1198 6
                    $fields[$field]['values'] = static::$subClasses;
1199
                }
1200
1201 6
                if (!isset($fields[$field]['blankisnull']) && empty($fields[$field]['notnull'])) {
1202 6
                    $fields[$field]['blankisnull'] = true;
1203
                }
1204
1205 6
                if ($fields[$field]['autoincrement']) {
1206 6
                    $fields[$field]['primary'] = true;
1207
                }
1208
            }
1209
1210 6
            static::$_classFields[$className] = $fields;
1211
        }
1212
    }
1213
1214
1215
    /**
1216
     * Returns class name for instantiating given record
1217
     * @param array $record record
1218
     * @return string class name
1219
     */
1220 98
    protected static function _getRecordClass($record)
1221
    {
1222 98
        $static = get_called_class();
1223
1224 98
        if (!static::fieldExists('Class')) {
1225
            return $static;
1226
        }
1227
1228 98
        $columnName = static::_cn('Class');
1229
1230 98
        if (!empty($record[$columnName]) && is_subclass_of($record[$columnName], $static)) {
1231 19
            return $record[$columnName];
1232
        } else {
1233 79
            return $static;
1234
        }
1235
    }
1236
1237
    /**
1238
     * Shorthand alias for _getColumnName
1239
     * @param string $field name of field
1240
     * @return string column name
1241
     */
1242 121
    protected static function _cn($field)
1243
    {
1244 121
        return static::getColumnName($field);
1245
    }
1246
1247
1248 65
    private function applyNewValue($type, $field, $value)
1249
    {
1250 65
        if (!isset($this->_convertedValues[$field])) {
1251 65
            if (is_null($value) && !in_array($type, ['set','list'])) {
1252
                unset($this->_convertedValues[$field]);
1253
                return null;
1254
            }
1255 65
            $this->_convertedValues[$field] = $value;
1256
        }
1257 65
        return $this->_convertedValues[$field];
1258
    }
1259
1260
    /**
1261
     * Applies type-dependent transformations to the value in $this->_record[$fieldOptions['columnName']]
1262
     * Caches to $this->_convertedValues[$field] and returns the value in there.
1263
     * @param string $field Name of field
1264
     * @return mixed value
1265
     */
1266 81
    protected function _getFieldValue($field, $useDefault = true)
1267
    {
1268 81
        $fieldOptions = static::$_classFields[get_called_class()][$field];
1269
1270 81
        if (isset($this->_record[$fieldOptions['columnName']])) {
1271 81
            $value = $this->_record[$fieldOptions['columnName']];
1272
1273 81
            $defaultGetMapper = static::defaultGetMapper;
1274
1275
            // apply type-dependent transformations
1276 81
            switch ($fieldOptions['type']) {
1277 81
                case 'timestamp':
1278 33
                        return $this->applyNewValue($fieldOptions['type'], $field, $defaultGetMapper::getTimestampValue($value));
1279
1280 81
                case 'serialized':
1281 16
                        return $this->applyNewValue($fieldOptions['type'], $field, $defaultGetMapper::getSerializedValue($value));
1282
1283 81
                case 'set':
1284 81
                case 'list':
1285 16
                        return $this->applyNewValue($fieldOptions['type'], $field, $defaultGetMapper::getListValue($value, $fieldOptions['delimiter']));
1286
1287 81
                case 'int':
1288 81
                case 'integer':
1289 66
                case 'uint':
1290 64
                    return $this->applyNewValue($fieldOptions['type'], $field, $defaultGetMapper::getIntegerValue($value));
1291
1292 66
                case 'boolean':
1293 16
                    return $this->applyNewValue($fieldOptions['type'], $field, $defaultGetMapper::getBooleanValue($value));
1294
1295 66
                case 'decimal':
1296 19
                    return $this->applyNewValue($fieldOptions['type'], $field, $defaultGetMapper::getDecimalValue($value));
1297
1298 66
                case 'password':
1299
                default:
1300 66
                    return $value;
1301
            }
1302 41
        } elseif ($useDefault && isset($fieldOptions['default'])) {
1303
            // return default
1304 16
            return $fieldOptions['default'];
1305
        } else {
1306 41
            switch ($fieldOptions['type']) {
1307 41
                case 'set':
1308 41
                case 'list':
1309
                    return [];
1310
                default:
1311 41
                    return null;
1312
            }
1313
        }
1314
    }
1315
1316
    /**
1317
     * Sets given field's value
1318
     * @param string $field Name of field
1319
     * @param mixed $value New value
1320
     * @return mixed value
1321
     */
1322 109
    protected function _setFieldValue($field, $value)
1323
    {
1324
        // ignore setting versioning fields
1325 109
        if (static::isVersioned()) {
1326 53
            if ($field === 'RevisionID') {
1327 1
                return false;
1328
            }
1329
        }
1330
1331 109
        if (!static::fieldExists($field)) {
1332
            return false;
1333
        }
1334 109
        $fieldOptions = static::$_classFields[get_called_class()][$field];
1335
1336
        // no overriding autoincrements
1337 109
        if ($fieldOptions['autoincrement']) {
1338 2
            return false;
1339
        }
1340
1341 109
        $setMapper = static::defaultSetMapper;
1342
1343
        // pre-process value
1344 109
        $forceDirty = false;
1345 109
        switch ($fieldOptions['type']) {
1346 109
            case 'clob':
1347 109
            case 'string':
1348
                {
1349 36
                    $value = $setMapper::setStringValue($value);
1350 36
                    if (!$fieldOptions['notnull'] && $fieldOptions['blankisnull'] && ($value === '' || $value === null)) {
1351 1
                        $value = null;
1352
                    }
1353 36
                    break;
1354
                }
1355
1356 109
            case 'boolean':
1357
                {
1358 12
                    $value = $setMapper::setBooleanValue($value);
1359 12
                    break;
1360
                }
1361
1362 109
            case 'decimal':
1363
                {
1364 15
                    $value = $setMapper::setDecimalValue($value);
1365 15
                    break;
1366
                }
1367
1368 109
            case 'int':
1369 109
            case 'uint':
1370 109
            case 'integer':
1371
                {
1372 15
                    $value = $setMapper::setIntegerValue($value);
1373 15
                    if (!$fieldOptions['notnull'] && ($value === '' || is_null($value))) {
1374 2
                        $value = null;
1375
                    }
1376 15
                    break;
1377
                }
1378
1379 109
            case 'timestamp':
1380
                {
1381 14
                    unset($this->_convertedValues[$field]);
1382 14
                    $value = $setMapper::setTimestampValue($value);
1383 14
                    break;
1384
                }
1385
1386 109
            case 'date':
1387
                {
1388 13
                    unset($this->_convertedValues[$field]);
1389 13
                    $value = $setMapper::setDateValue($value);
1390 13
                    break;
1391
                }
1392
1393 109
            case 'serialized':
1394
                {
1395 12
                    if (!is_string($value)) {
1396 1
                        $value = $setMapper::setSerializedValue($value);
1397
                    }
1398 12
                    break;
1399
                }
1400 109
            case 'enum':
1401
                {
1402 109
                    $value = $setMapper::setEnumValue($fieldOptions['values'], $value);
1403 109
                    break;
1404
                }
1405 12
            case 'set':
1406 12
            case 'list':
1407
                {
1408 12
                    $value = $setMapper::setListValue($value, isset($fieldOptions['delimiter']) ? $fieldOptions['delimiter'] : null);
1409 12
                    $this->_convertedValues[$field] = $value;
1410 12
                    $forceDirty = true;
1411 12
                    break;
1412
                }
1413
        }
1414
1415 109
        if ($forceDirty || (empty($this->_record[$field]) && isset($value)) || ($this->_record[$field] !== $value)) {
1416 47
            $this->_setDirtyValue($field, $value);
1417 47
            return true;
1418
        } else {
1419 77
            return false;
1420
        }
1421
    }
1422
1423 47
    protected function _setDirtyValue($field, $value)
1424
    {
1425 47
        $columnName = static::_cn($field);
1426 47
        if (isset($this->_record[$columnName])) {
1427 16
            $this->_originalValues[$field] = $this->_record[$columnName];
1428
        }
1429 47
        $this->_record[$columnName] = $value;
1430
        // only set value if this is an attribute mapped field
1431 47
        if (isset($this->_classFields[get_called_class()][$columnName]['attributeField'])) {
1432
            $this->$columnName = $value;
1433
        }
1434 47
        $this->_isDirty = true;
1435
1436
        // unset invalidated relationships
1437 47
        if (!empty($fieldOptions['relationships']) && static::isRelational()) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $fieldOptions seems to never exist and therefore empty should always be true.
Loading history...
1438
            foreach ($fieldOptions['relationships'] as $relationship => $isCached) {
1439
                if ($isCached) {
1440
                    unset($this->_relatedObjects[$relationship]);
1441
                }
1442
            }
1443
        }
1444
    }
1445
1446 23
    protected function _prepareRecordValues()
1447
    {
1448 23
        $record = [];
1449
1450 23
        foreach (static::$_classFields[get_called_class()] as $field => $options) {
1451 23
            $columnName = static::_cn($field);
1452
1453 23
            if (array_key_exists($columnName, $this->_record) || isset($this->$columnName)) {
1454 23
                $value = $this->_record[$columnName] ?? $this->$columnName;
1455
1456 23
                if (!$value && !empty($options['blankisnull'])) {
1457 23
                    $value = null;
1458
                }
1459 23
            } elseif (isset($options['default'])) {
1460 8
                $value = $options['default'];
1461
            } else {
1462 23
                continue;
1463
            }
1464
1465 23
            if (($options['type'] == 'date') && ($value == '0000-00-00') && !empty($options['blankisnull'])) {
1466
                $value = null;
1467
            }
1468 23
            if (($options['type'] == 'timestamp')) {
1469 23
                if (is_numeric($value)) {
1470 14
                    $value = date('Y-m-d H:i:s', $value);
1471 16
                } elseif ($value == null && !$options['notnull']) {
1472
                    $value = null;
1473
                }
1474
            }
1475
1476 23
            if (($options['type'] == 'serialized') && !is_string($value)) {
1477
                $value = serialize($value);
1478
            }
1479
1480 23
            if (($options['type'] == 'list') && is_array($value)) {
1481 11
                $delim = empty($options['delimiter']) ? ',' : $options['delimiter'];
1482 11
                $value = implode($delim, $value);
1483
            }
1484
1485 23
            $record[$field] = $value;
1486
        }
1487
1488 23
        return $record;
1489
    }
1490
1491 23
    protected static function _mapValuesToSet($recordValues)
1492
    {
1493 23
        $set = [];
1494
1495 23
        foreach ($recordValues as $field => $value) {
1496 23
            $fieldConfig = static::$_classFields[get_called_class()][$field];
1497
1498 23
            if ($value === null) {
1499 10
                $set[] = sprintf('`%s` = NULL', $fieldConfig['columnName']);
1500 23
            } elseif ($fieldConfig['type'] == 'timestamp' && $value == 'CURRENT_TIMESTAMP') {
1501
                $set[] = sprintf('`%s` = CURRENT_TIMESTAMP', $fieldConfig['columnName']);
1502 23
            } elseif ($fieldConfig['type'] == 'set' && is_array($value)) {
1503 11
                $set[] = sprintf('`%s` = "%s"', $fieldConfig['columnName'], DB::escape(join(',', $value)));
1504 23
            } elseif ($fieldConfig['type'] == 'boolean') {
1505 16
                $set[] = sprintf('`%s` = %u', $fieldConfig['columnName'], $value ? 1 : 0);
1506
            } else {
1507 23
                $set[] = sprintf('`%s` = "%s"', $fieldConfig['columnName'], DB::escape($value));
0 ignored issues
show
Bug introduced by
It seems like Divergence\IO\Database\MySQL::escape($value) can also be of type array and array; however, parameter $values of sprintf() does only seem to accept double|integer|string, 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

1507
                $set[] = sprintf('`%s` = "%s"', $fieldConfig['columnName'], /** @scrutinizer ignore-type */ DB::escape($value));
Loading history...
1508
            }
1509
        }
1510
1511 23
        return $set;
1512
    }
1513
1514 16
    protected static function _mapFieldOrder($order)
1515
    {
1516 16
        if (is_string($order)) {
1517 4
            return [$order];
1518 14
        } elseif (is_array($order)) {
1519 14
            $r = [];
1520
1521 14
            foreach ($order as $key => $value) {
1522 14
                if (is_string($key)) {
1523 13
                    $columnName = static::_cn($key);
1524 13
                    $direction = strtoupper($value)=='DESC' ? 'DESC' : 'ASC';
1525
                } else {
1526 2
                    $columnName = static::_cn($value);
1527 1
                    $direction = 'ASC';
1528
                }
1529
1530 13
                $r[] = sprintf('`%s` %s', $columnName, $direction);
1531
            }
1532
1533 13
            return $r;
1534
        }
1535
    }
1536
1537
    /**
1538
     * @param array<string,null|string|array{'operator': string, 'value': string}> $conditions
1539
     * @return array
1540
     */
1541 85
    protected static function _mapConditions($conditions)
1542
    {
1543 85
        foreach ($conditions as $field => &$condition) {
1544 85
            if (is_string($field)) {
1545 83
                if (isset(static::$_classFields[get_called_class()][$field])) {
1546 82
                    $fieldOptions = static::$_classFields[get_called_class()][$field];
1547
                }
1548
1549 83
                if ($condition === null || ($condition == '' && $fieldOptions['blankisnull'])) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $fieldOptions does not seem to be defined for all execution paths leading up to this point.
Loading history...
1550 1
                    $condition = sprintf('`%s` IS NULL', static::_cn($field));
1551 83
                } elseif (is_array($condition)) {
1552 1
                    $condition = sprintf('`%s` %s "%s"', static::_cn($field), $condition['operator'], DB::escape($condition['value']));
0 ignored issues
show
Bug introduced by
It seems like Divergence\IO\Database\M...pe($condition['value']) can also be of type array and array; however, parameter $values of sprintf() does only seem to accept double|integer|string, 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

1552
                    $condition = sprintf('`%s` %s "%s"', static::_cn($field), $condition['operator'], /** @scrutinizer ignore-type */ DB::escape($condition['value']));
Loading history...
1553
                } else {
1554 83
                    $condition = sprintf('`%s` = "%s"', static::_cn($field), DB::escape($condition));
1555
                }
1556
            }
1557
        }
1558
1559 85
        return $conditions;
1560
    }
1561
1562 20
    protected function finishValidation()
1563
    {
1564 20
        $this->_isValid = $this->_isValid && !$this->_validator->hasErrors();
1565
1566 20
        if (!$this->_isValid) {
1567
            $this->_validationErrors = array_merge($this->_validationErrors, $this->_validator->getErrors());
1568
        }
1569
1570 20
        return $this->_isValid;
1571
    }
1572
}
1573