Passed
Push — release ( e4b72f...07d039 )
by Henry
09:15 queued 06:21
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 1
    public function __isset($name)
361
    {
362 1
        $value = $this->getValue($name);
363 1
        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
      
408 125
        $className::$rootClass = $className::$rootClass ?? $className;
0 ignored issues
show
Bug introduced by
The property rootClass does not exist on string.
Loading history...
409 125
        $className::$defaultClass = $className::$defaultClass ?? $className;
0 ignored issues
show
Bug introduced by
The property defaultClass does not exist on string.
Loading history...
410 125
        $className::$subClasses = $className::$subClasses ?? [$className];
0 ignored issues
show
Bug introduced by
The property subClasses does not exist on string.
Loading history...
411
412 125
        if (empty(static::$_fieldsDefined[$className])) {
413 7
            static::_defineFields();
414 7
            static::_initFields();
415
416 7
            static::$_fieldsDefined[$className] = true;
417
        }
418 125
        if (empty(static::$_relationshipsDefined[$className]) && static::isRelational()) {
419 3
            static::_defineRelationships();
420 3
            static::_initRelationships();
421
422 3
            static::$_relationshipsDefined[$className] = true;
423
        }
424
425 125
        if (empty(static::$_eventsDefined[$className])) {
426 7
            static::_defineEvents();
427
428 7
            static::$_eventsDefined[$className] = true;
429
        }
430
    }
431
432
    /**
433
     * getValue Pass thru for __get
434
     *
435
     * @param string $name The name of the field you want to get.
436
     *
437
     * @return mixed Value of the field you wanted if it exists or null otherwise.
438
     */
439 83
    public function getValue($name)
440
    {
441
        switch ($name) {
442 83
            case 'isDirty':
443 23
                return $this->_isDirty;
444
445 79
            case 'isPhantom':
446 24
                return $this->_isPhantom;
447
448 77
            case 'wasPhantom':
449 3
                return $this->_wasPhantom;
450
451 77
            case 'isValid':
452 3
                return $this->_isValid;
453
454 77
            case 'isNew':
455 3
                return $this->_isNew;
456
457 77
            case 'isUpdated':
458 3
                return $this->_isUpdated;
459
460 77
            case 'validationErrors':
461 38
                return array_filter($this->_validationErrors);
462
463 71
            case 'data':
464 25
                return $this->getData();
465
466 56
            case 'originalValues':
467 3
                return $this->_originalValues;
468
469
            default:
470
                {
471
                    // handle field
472 54
                    if (static::fieldExists($name)) {
473 48
                        return $this->_getFieldValue($name);
474
                    }
475
                    // handle relationship
476 12
                    elseif (static::isRelational()) {
477 10
                        if (static::_relationshipExists($name)) {
478 10
                            return $this->_getRelationshipValue($name);
479
                        }
480
                    }
481
                    // default Handle to ID if not caught by fieldExists
482 2
                    elseif ($name == static::$handleField) {
483 1
                        return $this->_getFieldValue('ID');
484
                    }
485
                }
486
        }
487
        // undefined
488 2
        return null;
489
    }
490
491
    /**
492
     * Sets a value on this model.
493
     *
494
     * @param string $name
495
     * @param mixed $value
496
     * @return void|false False if the field does not exist. Void otherwise.
497
     */
498 7
    public function setValue($name, $value)
499
    {
500
        // handle field
501 7
        if (static::fieldExists($name)) {
502 7
            $this->_setFieldValue($name, $value);
503
        }
504
        // undefined
505
        else {
506 1
            return false;
507
        }
508
    }
509
510
    /**
511
     * Checks if this model is versioned.
512
     *
513
     * @return boolean Returns true if this class is defined with Divergence\Models\Versioning as a trait.
514
     */
515 112
    public static function isVersioned()
516
    {
517 112
        return in_array('Divergence\\Models\\Versioning', class_uses(get_called_class()));
518
    }
519
520
    /**
521
     * Checks if this model is ready for relationships.
522
     *
523
     * @return boolean Returns true if this class is defined with Divergence\Models\Relations as a trait.
524
     */
525 125
    public static function isRelational()
526
    {
527 125
        return in_array('Divergence\\Models\\Relations', class_uses(get_called_class()));
528
    }
529
530
    /**
531
     * Create a new object from this model.
532
     *
533
     * @param array $values Array of keys as fields and values.
534
     * @param boolean $save If the object should be immediately saved to database before being returned.
535
     * @return static An object of this model.
536
     */
537 26
    public static function create($values = [], $save = false)
538
    {
539 26
        $className = get_called_class();
540
541
        // create class
542
        /** @var ActiveRecord */
543 26
        $ActiveRecord = new $className();
544 26
        $ActiveRecord->setFields($values);
545
546 26
        if ($save) {
547 4
            $ActiveRecord->save();
548
        }
549
550 26
        return $ActiveRecord;
551
    }
552
553
    /**
554
     * Checks if a model is a certain class.
555
     * @param string $class Check if the model matches this class.
556
     * @return boolean True if model matches the class provided. False otherwise.
557
     */
558 1
    public function isA($class): bool
559
    {
560 1
        return is_a($this, $class);
561
    }
562
563
    /**
564
     * 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.
565
     *
566
     * @param string $className If you leave this blank the return will be $this
567
     * @param array $fieldValues Optional. Any field values you want to override.
568
     * @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.
569
     */
570 2
    public function changeClass($className = false, $fieldValues = false)
571
    {
572 2
        if (!$className) {
573 1
            return $this;
574
        }
575
576 2
        $this->_record[static::_cn('Class')] = $className;
577 2
        $ActiveRecord = new $className($this->_record, true, $this->isPhantom);
578
579 2
        if ($fieldValues) {
580 1
            $ActiveRecord->setFields($fieldValues);
581
        }
582
583 2
        if (!$this->isPhantom) {
584
            $ActiveRecord->save();
585
        }
586
587 2
        return $ActiveRecord;
588
    }
589
590
    /**
591
     * Change multiple fields in the model with an array.
592
     *
593
     * @param array $values Field/values array to change multiple fields in this model.
594
     * @return void
595
     */
596 37
    public function setFields($values)
597
    {
598 37
        foreach ($values as $field => $value) {
599 34
            $this->_setFieldValue($field, $value);
600
        }
601
    }
602
603
    /**
604
     * Change one field in the model.
605
     *
606
     * @param string $field
607
     * @param mixed $value
608
     * @return void
609
     */
610 1
    public function setField($field, $value)
611
    {
612 1
        $this->_setFieldValue($field, $value);
613
    }
614
615
    /**
616
     * Implements JsonSerializable for this class.
617
     *
618
     * @return array Return for extension JsonSerializable
619
     */
620 6
    public function jsonSerialize(): array
621
    {
622 6
        return $this->getData();
623
    }
624
625
    /**
626
     *  Gets normalized object data.
627
     *
628
     *  @return array The model's data as a normal array with any validation errors included.
629
     */
630 38
    public function getData(): array
631
    {
632 38
        $data = [];
633
634 38
        foreach (static::$_classFields[get_called_class()] as $field => $options) {
635 38
            $data[$field] = $this->_getFieldValue($field);
636
        }
637
638 38
        if ($this->validationErrors) {
639 1
            $data['validationErrors'] = $this->validationErrors;
640
        }
641
642 38
        return $data;
643
    }
644
645
    /**
646
     * Checks if a field has been changed from it's value when this object was created.
647
     *
648
     * @param string $field
649
     * @return boolean
650
     */
651 1
    public function isFieldDirty($field): bool
652
    {
653 1
        return $this->isPhantom || array_key_exists($field, $this->_originalValues);
654
    }
655
656
    /**
657
     * Gets values that this model was instantiated with for a given field.
658
     *
659
     * @param string $field Field name
660
     * @return mixed
661
     */
662 1
    public function getOriginalValue($field)
663
    {
664 1
        return $this->_originalValues[$field];
665
    }
666
667
    /**
668
     * Fires a DB::clearCachedRecord a key static::$tableName.'/'.static::getPrimaryKey()
669
     *
670
     * @return void
671
     */
672 20
    public function clearCaches()
673
    {
674 20
        foreach ($this->getClassFields() as $field => $options) {
675 20
            if (!empty($options['unique']) || !empty($options['primary'])) {
676 20
                $key = sprintf('%s/%s', static::$tableName, $field);
677 20
                DB::clearCachedRecord($key);
678
            }
679
        }
680
    }
681
682
    /**
683
     * Runs the before save event function one at a time for any class that had $beforeSave configured in the ancestor tree.
684
     */
685 21
    public function beforeSave()
686
    {
687 21
        foreach (static::$_classBeforeSave as $beforeSave) {
688 1
            if (is_callable($beforeSave)) {
689 1
                $beforeSave($this);
690
            }
691
        }
692
    }
693
694
    /**
695
     * 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.
696
     */
697 19
    public function afterSave()
698
    {
699 19
        foreach (static::$_classAfterSave as $afterSave) {
700 1
            if (is_callable($afterSave)) {
701 1
                $afterSave($this);
702
            }
703
        }
704
    }
705
706
    /**
707
     * Saves this object to the database currently in use.
708
     *
709
     * @param bool $deep Default is true. When true will try to save any dirty models in any defined and initialized relationships.
710
     *
711
     * @uses $this->_isPhantom
712
     * @uses $this->_isDirty
713
     */
714 20
    public function save($deep = true)
715
    {
716
        // run before save
717 20
        $this->beforeSave();
718
719 20
        if (static::isVersioned()) {
720 13
            $this->beforeVersionedSave();
721
        }
722
723
        // set created
724 20
        if (static::fieldExists('Created') && (!$this->Created || ($this->Created == 'CURRENT_TIMESTAMP'))) {
725 8
            $this->Created = $this->_record['Created'] = time();
726 8
            unset($this->_convertedValues['Created']);
727
        }
728
729
        // validate
730 20
        if (!$this->validate($deep)) {
731
            throw new Exception('Cannot save invalid record');
732
        }
733
734 20
        $this->clearCaches();
735
736 20
        if ($this->isDirty) {
737
            // prepare record values
738 19
            $recordValues = $this->_prepareRecordValues();
739
740
            // transform record to set array
741 19
            $set = static::_mapValuesToSet($recordValues);
742
743
            // create new or update existing
744 19
            if ($this->_isPhantom) {
745 15
                DB::nonQuery((new Insert())->setTable(static::$tableName)->set($set), null, [static::class,'handleException']);
746 14
                $primaryKey = $this->getPrimaryKey();
747 14
                $insertID = DB::insertID();
748 14
                $fields = static::getClassFields();
749 14
                if (($fields[$primaryKey]['type'] ?? false) === 'integer') {
750 14
                    $insertID = intval($insertID);
751
                }
752 14
                $this->_record[$primaryKey] = $insertID;
753 14
                $this->$primaryKey = $insertID;
754 14
                $this->_isPhantom = false;
755 14
                $this->_isNew = true;
756 5
            } elseif (count($set)) {
757 5
                DB::nonQuery((new Update())->setTable(static::$tableName)->set($set)->where(
758 5
                    sprintf('`%s` = %u', static::_cn($this->getPrimaryKey()), (string)$this->getPrimaryKeyValue())
759 5
                ), null, [static::class,'handleException']);
760
761 4
                $this->_isUpdated = true;
762
            }
763
764
            // update state
765 17
            $this->_isDirty = false;
766 17
            if (static::isVersioned()) {
767 10
                $this->afterVersionedSave();
768
            }
769
        }
770 18
        $this->afterSave();
771
    }
772
773
774
    /**
775
     * Deletes this object.
776
     *
777
     * @return bool True if database returns number of affected rows above 0. False otherwise.
778
     */
779 9
    public function destroy(): bool
780
    {
781 9
        if (static::isVersioned()) {
782 5
            if (static::$createRevisionOnDestroy) {
783
                // save a copy to history table
784 5
                if ($this->fieldExists('Created')) {
785 5
                    $this->Created = time();
786
                }
787
788 5
                $recordValues = $this->_prepareRecordValues();
789 5
                $set = static::_mapValuesToSet($recordValues);
790
791 5
                DB::nonQuery((new Insert())->setTable(static::getHistoryTable())->set($set), null, [static::class,'handleException']);
792
            }
793
        }
794
795 8
        return static::delete((string)$this->getPrimaryKeyValue());
796
    }
797
798
    /**
799
     * Delete by ID
800
     *
801
     * @param int|string $id
802
     * @return bool True if database returns number of affected rows above 0. False otherwise.
803
     */
804 8
    public static function delete($id): bool
805
    {
806 8
        DB::nonQuery((new Delete())->setTable(static::$tableName)->where(sprintf('`%s` = %u', static::_cn(static::$primaryKey ? static::$primaryKey : 'ID'), $id)), null, [static::class,'handleException']);
807
808 8
        return DB::affectedRows() > 0;
809
    }
810
811
    /**
812
     * Checks of a field exists for this model in the fields config.
813
     *
814
     * @param string $field Name of the field
815
     * @return bool True if the field exists. False otherwise.
816
     */
817 125
    public static function fieldExists($field): bool
818
    {
819 125
        static::init();
820 125
        return array_key_exists($field, static::$_classFields[get_called_class()]);
821
    }
822
823
    /**
824
     * Returns the current configuration of class fields for the called class.
825
     *
826
     * @return array Current configuration of class fields for the called class.
827
     */
828 21
    public static function getClassFields(): array
829
    {
830 21
        static::init();
831 21
        return static::$_classFields[get_called_class()];
832
    }
833
834
    /**
835
     * Returns either a field option or an array of all the field options.
836
     *
837
     * @param string $field Name of the field.
838
     * @param boolean $optionKey
839
     * @return array|mixed
840
     */
841 1
    public static function getFieldOptions($field, $optionKey = false)
842
    {
843 1
        if ($optionKey) {
844 1
            return static::$_classFields[get_called_class()][$field][$optionKey];
845
        } else {
846 1
            return static::$_classFields[get_called_class()][$field];
847
        }
848
    }
849
850
    /**
851
     * Returns columnName for given field
852
     * @param string $field name of field
853
     * @return string column name
854
     */
855 122
    public static function getColumnName($field)
856
    {
857 122
        static::init();
858 122
        if (!static::fieldExists($field)) {
859 2
            throw new Exception('getColumnName called on nonexisting column: ' . get_called_class().'->'.$field);
860
        }
861
862 121
        return static::$_classFields[get_called_class()][$field]['columnName'];
863
    }
864
865 2
    public static function mapFieldOrder($order)
866
    {
867 2
        return static::_mapFieldOrder($order);
868
    }
869
870 1
    public static function mapConditions($conditions)
871
    {
872 1
        return static::_mapConditions($conditions);
873
    }
874
875
    /**
876
     * Returns static::$rootClass for the called class.
877
     *
878
     * @return string static::$rootClass for the called class.
879
     */
880 1
    public function getRootClass(): string
881
    {
882 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...
883
    }
884
885
    /**
886
     * Sets an array of validation errors for this object.
887
     *
888
     * @param array $array Validation errors in the form Field Name => error message
889
     * @return void
890
     */
891 2
    public function addValidationErrors($array)
892
    {
893 2
        foreach ($array as $field => $errorMessage) {
894 2
            $this->addValidationError($field, $errorMessage);
895
        }
896
    }
897
898
    /**
899
     * Sets a validation error for this object. Sets $this->_isValid to false.
900
     *
901
     * @param string $field
902
     * @param string $errorMessage
903
     * @return void
904
     */
905 2
    public function addValidationError($field, $errorMessage)
906
    {
907 2
        $this->_isValid = false;
908 2
        $this->_validationErrors[$field] = $errorMessage;
909
    }
910
911
    /**
912
     * Get a validation error for a given field.
913
     *
914
     * @param string $field Name of the field.
915
     * @return string|null A validation error for the field. Null is no validation error found.
916
     */
917 1
    public function getValidationError($field)
918
    {
919
        // break apart path
920 1
        $crumbs = explode('.', $field);
921
922
        // resolve path recursively
923 1
        $cur = &$this->_validationErrors;
924 1
        while ($crumb = array_shift($crumbs)) {
925 1
            if (array_key_exists($crumb, $cur)) {
926 1
                $cur = &$cur[$crumb];
927
            } else {
928 1
                return null;
929
            }
930
        }
931
932
        // return current value
933 1
        return $cur;
934
    }
935
936
    /**
937
     * Validates the model. Instantiates a new RecordValidator object and sets it to $this->_validator.
938
     * Then validates against the set validators in this model. Returns $this->_isValid
939
     *
940
     * @param boolean $deep If true will attempt to validate any already loaded relationship members.
941
     * @return bool $this->_isValid which could be set to true or false depending on what happens with the RecordValidator.
942
     */
943 20
    public function validate($deep = true)
944
    {
945 20
        $this->_isValid = true;
946 20
        $this->_validationErrors = [];
947
948 20
        if (!isset($this->_validator)) {
949 20
            $this->_validator = new RecordValidator($this->_record);
950
        } else {
951 6
            $this->_validator->resetErrors();
952
        }
953
954 20
        foreach (static::$validators as $validator) {
955
            $this->_validator->validate($validator);
956
        }
957
958 20
        $this->finishValidation();
959
960 20
        if ($deep) {
961
            // validate relationship objects
962 20
            if (!empty(static::$_classRelationships[get_called_class()])) {
963 1
                foreach (static::$_classRelationships[get_called_class()] as $relationship => $options) {
964 1
                    if (empty($this->_relatedObjects[$relationship])) {
965 1
                        continue;
966
                    }
967
968
969 1
                    if ($options['type'] == 'one-one') {
970
                        if ($this->_relatedObjects[$relationship]->isDirty) {
971
                            $this->_relatedObjects[$relationship]->validate();
972
                            $this->_isValid = $this->_isValid && $this->_relatedObjects[$relationship]->isValid;
973
                            $this->_validationErrors[$relationship] = $this->_relatedObjects[$relationship]->validationErrors;
974
                        }
975 1
                    } elseif ($options['type'] == 'one-many') {
976 1
                        foreach ($this->_relatedObjects[$relationship] as $i => $object) {
977 1
                            if ($object->isDirty) {
978
                                $object->validate();
979
                                $this->_isValid = $this->_isValid && $object->isValid;
980
                                $this->_validationErrors[$relationship][$i] = $object->validationErrors;
981
                            }
982
                        }
983
                    }
984
                } // foreach
985
            } // if
986
        } // if ($deep)
987
988 20
        return $this->_isValid;
989
    }
990
991
    /**
992
     * Handle any errors that come from the database client in the process of running a query.
993
     * 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.
994
     * Other errors will be routed through to DB::handleException
995
     *
996
     * @param Exception $exception
997
     * @param string $query
998
     * @param array $queryLog
999
     * @param array|string $parameters
1000
     * @return mixed Retried query result or the return from DB::handleException
1001
     */
1002 7
    public static function handleException(\Exception $e, $query = null, $queryLog = null, $parameters = null)
1003
    {
1004 7
        $Connection = DB::getConnection();
1005 7
        if ($Connection->errorCode() == '42S02' && static::$autoCreateTables) {
1006 2
            $CreateTable = SQL::getCreateTable(static::$rootClass);
1007
1008
            // history versions table
1009 2
            if (static::isVersioned()) {
1010 1
                $CreateTable .= SQL::getCreateTable(static::$rootClass, true);
1011
            }
1012
1013 2
            $Statement = $Connection->query($CreateTable);
1014
1015
            // check for errors
1016 2
            $ErrorInfo = $Statement->errorInfo();
1017
1018
            // handle query error
1019 2
            if ($ErrorInfo[0] != '00000') {
1020
                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

1020
                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

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

1513
                $set[] = sprintf('`%s` = "%s"', $fieldConfig['columnName'], /** @scrutinizer ignore-type */ DB::escape($value));
Loading history...
1514
            }
1515
        }
1516
1517 23
        return $set;
1518
    }
1519
1520 16
    protected static function _mapFieldOrder($order)
1521
    {
1522 16
        if (is_string($order)) {
1523 4
            return [$order];
1524 14
        } elseif (is_array($order)) {
1525 14
            $r = [];
1526
1527 14
            foreach ($order as $key => $value) {
1528 14
                if (is_string($key)) {
1529 13
                    $columnName = static::_cn($key);
1530 13
                    $direction = strtoupper($value)=='DESC' ? 'DESC' : 'ASC';
1531
                } else {
1532 2
                    $columnName = static::_cn($value);
1533 1
                    $direction = 'ASC';
1534
                }
1535
1536 13
                $r[] = sprintf('`%s` %s', $columnName, $direction);
1537
            }
1538
1539 13
            return $r;
1540
        }
1541
    }
1542
1543
    /**
1544
     * @param array<string,null|string|array{'operator': string, 'value': string}> $conditions
1545
     * @return array
1546
     */
1547 85
    protected static function _mapConditions($conditions)
1548
    {
1549 85
        foreach ($conditions as $field => &$condition) {
1550 85
            if (is_string($field)) {
1551 83
                if (isset(static::$_classFields[get_called_class()][$field])) {
1552 82
                    $fieldOptions = static::$_classFields[get_called_class()][$field];
1553
                }
1554
1555 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...
1556 1
                    $condition = sprintf('`%s` IS NULL', static::_cn($field));
1557 83
                } elseif (is_array($condition)) {
1558 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

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