Passed
Push — develop ( 33032a...666b96 )
by Henry
02:03
created

ActiveRecord::_setFieldValue()   F

Complexity

Conditions 36
Paths 385

Size

Total Lines 112
Code Lines 63

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 36
eloc 63
c 2
b 0
f 0
nc 385
nop 2
dl 0
loc 112
rs 1.0208

How to fix   Long Method    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 JsonSerializable;
15
use Divergence\IO\Database\SQL as SQL;
16
use Divergence\IO\Database\MySQL as DB;
17
use Divergence\IO\Database\Query\Delete;
18
use Divergence\IO\Database\Query\Insert;
19
use Divergence\IO\Database\Query\Update;
20
use Divergence\Models\Interfaces\FieldSetMapper;
21
use Divergence\Models\SetMappers\DefaultSetMapper;
22
23
/**
24
 * ActiveRecord
25
 *
26
 * @package Divergence
27
 * @author  Henry Paradiz <[email protected]>
28
 * @author  Chris Alfano <[email protected]>
29
 *
30
 * @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.
31
 * @property-read bool $isPhantom    True if this object was instantiated as a brand new object and isn't yet saved.
32
 * @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.
33
 * @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.
34
 * @property-read bool $isNew        False by default. Set to true only when an object that isPhantom is saved.
35
 * @property-read bool $isUpdated    False by default. Set to true when an object that already existed in the data store is saved.
36
 *
37
 * @property int        $ID Default primary key field. Part of Divergence\Models\Model but used in this file as a default.
38
 * @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.
39
 * @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.
40
 * @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.
41
 *
42
 * @property-read array $validationErrors    An empty string by default. Returns validation errors as an array.
43
 * @property-read array $data                A plain PHP array of the fields and values for this model object.
44
 * @property-read array $originalValues      A plain PHP array of the fields and values for this model object when it was instantiated.
45
 *
46
 * @property array $versioningFields
47
 */
48
class ActiveRecord implements JsonSerializable
49
{
50
    /**
51
     * @var bool $autoCreateTables Set this to true if you want the table(s) to automatically be created when not found.
52
     */
53
    public static $autoCreateTables = true;
54
55
    /**
56
     * @var string $tableName Name of table
57
     */
58
    public static $tableName = 'records';
59
60
    /**
61
     *
62
     * @var string $singularNoun Noun to describe singular object
63
     */
64
    public static $singularNoun = 'record';
65
66
    /**
67
     *
68
     * @var string $pluralNoun Noun to describe a plurality of objects
69
     */
70
    public static $pluralNoun = 'records';
71
72
    /**
73
     *
74
     * @var array $fieldDefaults Defaults values for field definitions
75
     */
76
    public static $fieldDefaults = [
77
        'type' => 'string',
78
        'notnull' => true,
79
    ];
80
81
    /**
82
     * @var array $fields Field definitions
83
     */
84
    public static $fields = [];
85
86
    /**
87
     * @var array $indexes Index definitions
88
     */
89
    public static $indexes = [];
90
91
92
    /**
93
    * @var array $validators Validation checks
94
    */
95
    public static $validators = [];
96
97
    /**
98
     * @var array $relationships Relationship definitions
99
     */
100
    public static $relationships = [];
101
102
103
    /**
104
     * Class names of possible contexts
105
     * @var array
106
     */
107
    public static $contextClasses;
108
109
    /**
110
     *  @var string|null $primaryKey The primary key for this model. Optional. Defaults to ID
111
     */
112
    public static $primaryKey = null;
113
114
    /**
115
     *  @var string $handleField Field which should be treated as unique but generated automatically.
116
     */
117
    public static $handleField = 'Handle';
118
119
    /**
120
     *  @var null|string $rootClass The root class.
121
     */
122
    public static $rootClass = null;
123
124
    /**
125
     *  @var null|string $defaultClass The default class to be used when creating a new object.
126
     */
127
    public static $defaultClass = null;
128
129
    /**
130
     *  @var array $subClasses Array of class names providing valid classes you can use for this model.
131
     */
132
    public static $subClasses = [];
133
134
    /**
135
     *  @var callable $beforeSave Runs inside the save() method before save actually happens.
136
     */
137
    public static $beforeSave;
138
139
    /**
140
     *  @var callable $afterSave Runs inside the save() method after save if no exception was thrown.
141
     */
142
    public static $afterSave;
143
144
145
    // versioning
146
    public static $historyTable;
147
    public static $createRevisionOnDestroy = true;
148
    public static $createRevisionOnSave = true;
149
150
    /**
151
     * Internal registry of fields that comprise this class. The setting of this variable of every parent derived from a child model will get merged.
152
     *
153
     * @var array $_classFields
154
     */
155
    protected static $_classFields = [];
156
157
    /**
158
     * 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.
159
     *
160
     * @var array $_classFields
161
     */
162
    protected static $_classRelationships = [];
163
164
    /**
165
     * 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.
166
     *
167
     * @var array $_classBeforeSave
168
     */
169
    protected static $_classBeforeSave = [];
170
171
    /**
172
     * 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.
173
     *
174
     * @var array $_classAfterSave
175
     */
176
    protected static $_classAfterSave = [];
177
178
    /**
179
     * 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.
180
     *
181
     * @used-by ActiveRecord::init()
182
     *
183
     * @var array $_fieldsDefined
184
     */
185
    protected static $_fieldsDefined = [];
186
187
    /**
188
     * 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.
189
     *
190
     * @used-by ActiveRecord::init()
191
     *
192
     * @var array $_relationshipsDefined
193
     */
194
    protected static $_relationshipsDefined = [];
195
    protected static $_eventsDefined = [];
196
197
    /**
198
     * @var array $_record Raw array data for this model.
199
     */
200
    protected $_record;
201
202
    /**
203
     * @var array $_convertedValues Raw array data for this model of data normalized for it's field type.
204
     */
205
    protected $_convertedValues;
206
207
    /**
208
     * @var RecordValidator $_validator Instance of a RecordValidator object.
209
     */
210
    protected $_validator;
211
212
    /**
213
     * @var array $_validationErrors Array of validation errors if there are any.
214
     */
215
    protected $_validationErrors;
216
217
    /**
218
     * @var array $_originalValues If any values have been changed the initial value is stored here.
219
     */
220
    protected $_originalValues;
221
222
223
    /**
224
     * False by default. Set to true only when an object has had any field change from it's state when it was instantiated.
225
     *
226
     * @var bool $_isDirty
227
     *
228
     * @used-by $this->save()
229
     * @used-by $this->__get()
230
     */
231
    protected $_isDirty;
232
233
    /**
234
     * True if this object was instantiated as a brand new object and isn't yet saved.
235
     *
236
     * @var bool $_isPhantom
237
     *
238
     * @used-by $this->save()
239
     * @used-by $this->__get()
240
     */
241
    protected $_isPhantom;
242
243
    /**
244
     * True if this object was originally instantiated as a brand new object. Will stay true even if saved during that PHP runtime.
245
     *
246
     * @var bool $_wasPhantom
247
     *
248
     * @used-by $this->__get()
249
     */
250
    protected $_wasPhantom;
251
252
    /**
253
     * 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.
254
     *
255
     * @var bool $_isValid
256
     *
257
     * @used-by $this->__get()
258
     */
259
    protected $_isValid;
260
261
    /**
262
     * False by default. Set to true only when an object that isPhantom is saved.
263
     *
264
     * @var bool $_isNew
265
     *
266
     * @used-by $this->save()
267
     * @used-by $this->__get()
268
     */
269
    protected $_isNew;
270
271
    /**
272
     * False by default. Set to true when an object that already existed in the data store is saved.
273
     *
274
     * @var bool $_isUpdated
275
     *
276
     * @used-by $this->__get()
277
     */
278
    protected $_isUpdated;
279
280
    /** Field Mapper */
281
    protected ?FieldSetMapper $fieldSetMapper;
282
283
    /**
284
     * __construct Instantiates a Model and returns.
285
     *
286
     * @param array $record Raw array data to start off the model.
287
     * @param boolean $isDirty Whether or not to treat this object as if it was modified from the start.
288
     * @param boolean $isPhantom Whether or not to treat this object as a brand new record not yet in the database.
289
     *
290
     * @uses static::init
291
     *
292
     * @return static Instance of the value of $this->Class
293
     */
294
    public function __construct($record = [], $isDirty = false, $isPhantom = null)
295
    {
296
        $this->_record = $record;
297
        $this->_isPhantom = isset($isPhantom) ? $isPhantom : empty($record);
298
        $this->_wasPhantom = $this->_isPhantom;
299
        $this->_isDirty = $this->_isPhantom || $isDirty;
300
        $this->_isNew = false;
301
        $this->_isUpdated = false;
302
303
        $this->_isValid = true;
304
        $this->_validationErrors = [];
305
        $this->_originalValues = [];
306
307
        static::init();
308
309
        // set Class
310
        if (static::fieldExists('Class') && !$this->Class) {
311
            $this->Class = get_class($this);
312
        }
313
    }
314
315
    /**
316
     * __get Passthru to getValue($name)
317
     *
318
     * @param string $name Name of the magic field you want.
319
     *
320
     * @return mixed The return of $this->getValue($name)
321
     */
322
    public function __get($name)
323
    {
324
        return $this->getValue($name);
325
    }
326
327
    /**
328
     * Passthru to setValue($name,$value)
329
     *
330
     * @param string $name Name of the magic field to set.
331
     * @param mixed $value Value to set.
332
     *
333
     * @return mixed The return of $this->setValue($name,$value)
334
     */
335
    public function __set($name, $value)
336
    {
337
        return $this->setValue($name, $value);
338
    }
339
340
    /**
341
     * Tests if a magic class attribute is set or not.
342
     *
343
     * @param string $name Name of the magic field to set.
344
     *
345
     * @return bool Returns true if a value was returned by $this->getValue($name), false otherwise.
346
     */
347
    public function __isset($name)
348
    {
349
        $value = $this->getValue($name);
350
        return isset($value);
351
    }
352
353
    /**
354
     * Gets the primary key field for his model.
355
     *
356
     * @return string ID by default or static::$primaryKey if it's set.
357
     */
358
    public static function getPrimaryKey()
359
    {
360
        return isset(static::$primaryKey) ? static::$primaryKey : 'ID';
361
    }
362
363
    /**
364
     * Gets the primary key value for his model.
365
     *
366
     * @return mixed The primary key value for this object.
367
     */
368
    public function getPrimaryKeyValue()
369
    {
370
        if (isset(static::$primaryKey)) {
371
            return $this->__get(static::$primaryKey);
372
        } else {
373
            return $this->ID;
374
        }
375
    }
376
377
    /**
378
     * init Initializes the model by checking the ancestor tree for the existence of various config fields and merges them.
379
     *
380
     * @uses static::$_fieldsDefined Sets static::$_fieldsDefined[get_called_class()] to true after running.
381
     * @uses static::$_relationshipsDefined Sets static::$_relationshipsDefined[get_called_class()] to true after running.
382
     * @uses static::$_eventsDefined Sets static::$_eventsDefined[get_called_class()] to true after running.
383
     *
384
     * @used-by static::__construct()
385
     * @used-by static::fieldExists()
386
     * @used-by static::getClassFields()
387
     * @used-by static::getColumnName()
388
     *
389
     * @return void
390
     */
391
    public static function init()
392
    {
393
        $className = get_called_class();
394
        if (empty(static::$_fieldsDefined[$className])) {
395
            static::_defineFields();
396
            static::_initFields();
397
398
            static::$_fieldsDefined[$className] = true;
399
        }
400
        if (empty(static::$_relationshipsDefined[$className]) && static::isRelational()) {
401
            static::_defineRelationships();
0 ignored issues
show
Bug introduced by
The method _defineRelationships() does not exist on Divergence\Models\ActiveRecord. It seems like you code against a sub-type of Divergence\Models\ActiveRecord such as Divergence\Tests\MockSite\Models\Forum\TagPost or Divergence\Tests\MockSite\Models\Forum\Thread or Divergence\Tests\MockSite\Models\Forum\Category or Divergence\Tests\MockSite\Models\Forum\Post or Divergence\Models\Auth\Session or Divergence\Tests\Models\Testables\relationalCanary or Divergence\Tests\Models\Testables\relationalTag. ( Ignorable by Annotation )

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

401
            static::/** @scrutinizer ignore-call */ 
402
                    _defineRelationships();
Loading history...
402
            static::_initRelationships();
0 ignored issues
show
Bug introduced by
The method _initRelationships() does not exist on Divergence\Models\ActiveRecord. It seems like you code against a sub-type of Divergence\Models\ActiveRecord such as Divergence\Tests\MockSite\Models\Forum\TagPost or Divergence\Tests\MockSite\Models\Forum\Thread or Divergence\Tests\MockSite\Models\Forum\Category or Divergence\Tests\MockSite\Models\Forum\Post or Divergence\Models\Auth\Session or Divergence\Tests\Models\Testables\relationalCanary or Divergence\Tests\Models\Testables\relationalTag. ( Ignorable by Annotation )

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

402
            static::/** @scrutinizer ignore-call */ 
403
                    _initRelationships();
Loading history...
403
404
            static::$_relationshipsDefined[$className] = true;
405
        }
406
407
        if (empty(static::$_eventsDefined[$className])) {
408
            static::_defineEvents();
409
410
            static::$_eventsDefined[$className] = true;
411
        }
412
    }
413
414
    /**
415
     * getValue Pass thru for __get
416
     *
417
     * @param string $name The name of the field you want to get.
418
     *
419
     * @return mixed Value of the field you wanted if it exists or null otherwise.
420
     */
421
    public function getValue($name)
422
    {
423
        switch ($name) {
424
            case 'isDirty':
425
                return $this->_isDirty;
426
427
            case 'isPhantom':
428
                return $this->_isPhantom;
429
430
            case 'wasPhantom':
431
                return $this->_wasPhantom;
432
433
            case 'isValid':
434
                return $this->_isValid;
435
436
            case 'isNew':
437
                return $this->_isNew;
438
439
            case 'isUpdated':
440
                return $this->_isUpdated;
441
442
            case 'validationErrors':
443
                return array_filter($this->_validationErrors);
444
445
            case 'data':
446
                return $this->getData();
447
448
            case 'originalValues':
449
                return $this->_originalValues;
450
451
            default:
452
                {
453
                    // handle field
454
                    if (static::fieldExists($name)) {
455
                        return $this->_getFieldValue($name);
456
                    }
457
                    // handle relationship
458
                    elseif (static::isRelational()) {
459
                        if (static::_relationshipExists($name)) {
0 ignored issues
show
Bug introduced by
The method _relationshipExists() does not exist on Divergence\Models\ActiveRecord. It seems like you code against a sub-type of Divergence\Models\ActiveRecord such as Divergence\Tests\MockSite\Models\Forum\TagPost or Divergence\Tests\MockSite\Models\Forum\Thread or Divergence\Tests\MockSite\Models\Forum\Category or Divergence\Tests\MockSite\Models\Forum\Post or Divergence\Models\Auth\Session or Divergence\Tests\Models\Testables\relationalCanary or Divergence\Tests\Models\Testables\relationalTag. ( Ignorable by Annotation )

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

459
                        if (static::/** @scrutinizer ignore-call */ _relationshipExists($name)) {
Loading history...
460
                            return $this->_getRelationshipValue($name);
0 ignored issues
show
Bug introduced by
The method _getRelationshipValue() does not exist on Divergence\Models\ActiveRecord. It seems like you code against a sub-type of Divergence\Models\ActiveRecord such as Divergence\Tests\MockSite\Models\Forum\TagPost or Divergence\Tests\MockSite\Models\Forum\Thread or Divergence\Tests\MockSite\Models\Forum\Category or Divergence\Tests\MockSite\Models\Forum\Post or Divergence\Models\Auth\Session or Divergence\Tests\Models\Testables\relationalCanary or Divergence\Tests\Models\Testables\relationalTag. ( Ignorable by Annotation )

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

460
                            return $this->/** @scrutinizer ignore-call */ _getRelationshipValue($name);
Loading history...
461
                        }
462
                    }
463
                    // default Handle to ID if not caught by fieldExists
464
                    elseif ($name == static::$handleField) {
465
                        return $this->ID;
466
                    }
467
                }
468
        }
469
        // undefined
470
        return null;
471
    }
472
473
    /**
474
     * Sets a value on this model.
475
     *
476
     * @param string $name
477
     * @param mixed $value
478
     * @return void|false False if the field does not exist. Void otherwise.
479
     */
480
    public function setValue($name, $value)
481
    {
482
        // handle field
483
        if (static::fieldExists($name)) {
484
            $this->_setFieldValue($name, $value);
485
        }
486
        // undefined
487
        else {
488
            return false;
489
        }
490
    }
491
492
    /**
493
     * Checks if this model is versioned.
494
     *
495
     * @return boolean Returns true if this class is defined with Divergence\Models\Versioning as a trait.
496
     */
497
    public static function isVersioned()
498
    {
499
        return in_array('Divergence\\Models\\Versioning', class_uses(get_called_class()));
500
    }
501
502
    /**
503
     * Checks if this model is ready for relationships.
504
     *
505
     * @return boolean Returns true if this class is defined with Divergence\Models\Relations as a trait.
506
     */
507
    public static function isRelational()
508
    {
509
        return in_array('Divergence\\Models\\Relations', class_uses(get_called_class()));
510
    }
511
512
    /**
513
     * Create a new object from this model.
514
     *
515
     * @param array $values Array of keys as fields and values.
516
     * @param boolean $save If the object should be immediately saved to database before being returned.
517
     * @return static An object of this model.
518
     */
519
    public static function create($values = [], $save = false)
520
    {
521
        $className = get_called_class();
522
523
        // create class
524
        /** @var ActiveRecord */
525
        $ActiveRecord = new $className();
526
        $ActiveRecord->setFields($values);
527
528
        if ($save) {
529
            $ActiveRecord->save();
530
        }
531
532
        return $ActiveRecord;
533
    }
534
535
    /**
536
     * Checks if a model is a certain class.
537
     * @param string $class Check if the model matches this class.
538
     * @return boolean True if model matches the class provided. False otherwise.
539
     */
540
    public function isA($class)
541
    {
542
        return is_a($this, $class);
543
    }
544
545
    /**
546
     * 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.
547
     *
548
     * @param string $className If you leave this blank the return will be $this
549
     * @param array $fieldValues Optional. Any field values you want to override.
550
     * @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.
551
     */
552
    public function changeClass($className = false, $fieldValues = false)
553
    {
554
        if (!$className) {
555
            return $this;
556
        }
557
558
        $this->_record[static::_cn('Class')] = $className;
559
        $ActiveRecord = new $className($this->_record, true, $this->isPhantom);
560
561
        if ($fieldValues) {
562
            $ActiveRecord->setFields($fieldValues);
563
        }
564
565
        if (!$this->isPhantom) {
566
            $ActiveRecord->save();
567
        }
568
569
        return $ActiveRecord;
570
    }
571
572
    /**
573
     * Change multiple fields in the model with an array.
574
     *
575
     * @param array $values Field/values array to change multiple fields in this model.
576
     * @return void
577
     */
578
    public function setFields($values)
579
    {
580
        foreach ($values as $field => $value) {
581
            $this->_setFieldValue($field, $value);
582
        }
583
    }
584
585
    /**
586
     * Change one field in the model.
587
     *
588
     * @param string $field
589
     * @param mixed $value
590
     * @return void
591
     */
592
    public function setField($field, $value)
593
    {
594
        $this->_setFieldValue($field, $value);
595
    }
596
597
    /**
598
     * Implements JsonSerializable for this class.
599
     *
600
     * @return array Return for extension JsonSerializable
601
     */
602
    public function jsonSerialize(): array
603
    {
604
        return $this->getData();
605
    }
606
607
    /**
608
     *  Gets normalized object data.
609
     *
610
     *  @return array The model's data as a normal array with any validation errors included.
611
     */
612
    public function getData()
613
    {
614
        $data = [];
615
616
        foreach (static::$_classFields[get_called_class()] as $field => $options) {
617
            $data[$field] = $this->_getFieldValue($field);
618
        }
619
620
        if ($this->validationErrors) {
621
            $data['validationErrors'] = $this->validationErrors;
622
        }
623
624
        return $data;
625
    }
626
627
    /**
628
     * Checks if a field has been changed from it's value when this object was created.
629
     *
630
     * @param string $field
631
     * @return boolean
632
     */
633
    public function isFieldDirty($field)
634
    {
635
        return $this->isPhantom || array_key_exists($field, $this->_originalValues);
636
    }
637
638
    /**
639
     * Gets values that this model was instantiated with for a given field.
640
     *
641
     * @param string $field Field name
642
     * @return mixed
643
     */
644
    public function getOriginalValue($field)
645
    {
646
        return $this->_originalValues[$field];
647
    }
648
649
    /**
650
     * Fires a DB::clearCachedRecord a key static::$tableName.'/'.static::getPrimaryKey()
651
     *
652
     * @return void
653
     */
654
    public function clearCaches()
655
    {
656
        foreach ($this->getClassFields() as $field => $options) {
657
            if (!empty($options['unique']) || !empty($options['primary'])) {
658
                $key = sprintf('%s/%s', static::$tableName, $field);
659
                DB::clearCachedRecord($key);
660
            }
661
        }
662
    }
663
664
    /**
665
     * Runs the before save event function one at a time for any class that had $beforeSave configured in the ancestor tree.
666
     */
667
    public function beforeSave()
668
    {
669
        foreach (static::$_classBeforeSave as $beforeSave) {
670
            if (is_callable($beforeSave)) {
671
                $beforeSave($this);
672
            }
673
        }
674
    }
675
676
    /**
677
     * 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.
678
     */
679
    public function afterSave()
680
    {
681
        foreach (static::$_classAfterSave as $afterSave) {
682
            if (is_callable($afterSave)) {
683
                $afterSave($this);
684
            }
685
        }
686
    }
687
688
    /**
689
     * Saves this object to the database currently in use.
690
     *
691
     * @param bool $deep Default is true. When true will try to save any dirty models in any defined and initialized relationships.
692
     *
693
     * @uses $this->_isPhantom
694
     * @uses $this->_isDirty
695
     */
696
    public function save($deep = true)
697
    {
698
        // run before save
699
        $this->beforeSave();
700
701
        if (static::isVersioned()) {
702
            $this->beforeVersionedSave();
0 ignored issues
show
Bug introduced by
The method beforeVersionedSave() does not exist on Divergence\Models\ActiveRecord. It seems like you code against a sub-type of Divergence\Models\ActiveRecord such as Divergence\Tests\MockSite\Models\Forum\TagPost or Divergence\Tests\MockSite\Models\Canary or Divergence\Tests\MockSite\Models\Forum\Thread or Divergence\Tests\MockSite\Models\Forum\Category or Divergence\Tests\MockSite\Models\Forum\Post. ( Ignorable by Annotation )

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

702
            $this->/** @scrutinizer ignore-call */ 
703
                   beforeVersionedSave();
Loading history...
703
        }
704
705
        // set created
706
        if (static::fieldExists('Created') && (!$this->Created || ($this->Created == 'CURRENT_TIMESTAMP'))) {
707
            $this->Created = time();
708
        }
709
710
        // validate
711
        if (!$this->validate($deep)) {
712
            throw new Exception('Cannot save invalid record');
713
        }
714
715
        $this->clearCaches();
716
717
        if ($this->isDirty) {
718
            // prepare record values
719
            $recordValues = $this->_prepareRecordValues();
720
721
            // transform record to set array
722
            $set = static::_mapValuesToSet($recordValues);
723
724
            // create new or update existing
725
            if ($this->_isPhantom) {
726
                DB::nonQuery((new Insert())->setTable(static::$tableName)->set($set), null, [static::class,'handleException']);
727
                $this->_record[$this->getPrimaryKey()] = DB::insertID();
728
                $this->_isPhantom = false;
729
                $this->_isNew = true;
730
            } elseif (count($set)) {
731
                DB::nonQuery((new Update())->setTable(static::$tableName)->set($set)->where(
732
                    sprintf('`%s` = %u', static::_cn($this->getPrimaryKey()), $this->getPrimaryKeyValue())
0 ignored issues
show
Bug introduced by
It seems like $this->getPrimaryKeyValue() can also be of type array and 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

732
                    sprintf('`%s` = %u', static::_cn($this->getPrimaryKey()), /** @scrutinizer ignore-type */ $this->getPrimaryKeyValue())
Loading history...
733
                ), null, [static::class,'handleException']);
734
735
                $this->_isUpdated = true;
736
            }
737
738
            // update state
739
            $this->_isDirty = false;
740
            if (static::isVersioned()) {
741
                $this->afterVersionedSave();
0 ignored issues
show
Bug introduced by
The method afterVersionedSave() does not exist on Divergence\Models\ActiveRecord. It seems like you code against a sub-type of Divergence\Models\ActiveRecord such as Divergence\Tests\MockSite\Models\Forum\TagPost or Divergence\Tests\MockSite\Models\Canary or Divergence\Tests\MockSite\Models\Forum\Thread or Divergence\Tests\MockSite\Models\Forum\Category or Divergence\Tests\MockSite\Models\Forum\Post. ( Ignorable by Annotation )

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

741
                $this->/** @scrutinizer ignore-call */ 
742
                       afterVersionedSave();
Loading history...
742
            }
743
        }
744
        $this->afterSave();
745
    }
746
747
748
    /**
749
     * Deletes this object.
750
     *
751
     * @return bool True if database returns number of affected rows above 0. False otherwise.
752
     */
753
    public function destroy()
754
    {
755
        if (static::isVersioned()) {
756
            if (static::$createRevisionOnDestroy) {
757
                // save a copy to history table
758
                if ($this->fieldExists('Created')) {
759
                    $this->Created = time();
760
                }
761
762
                $recordValues = $this->_prepareRecordValues();
763
                $set = static::_mapValuesToSet($recordValues);
764
765
                DB::nonQuery((new Insert())->setTable(static::getHistoryTable())->set($set), null, [static::class,'handleException']);
0 ignored issues
show
Bug introduced by
The method getHistoryTable() does not exist on Divergence\Models\ActiveRecord. It seems like you code against a sub-type of Divergence\Models\ActiveRecord such as Divergence\Tests\MockSite\Models\Forum\TagPost or Divergence\Tests\MockSite\Models\Canary or Divergence\Tests\MockSite\Models\Forum\Thread or Divergence\Tests\MockSite\Models\Forum\Category or Divergence\Tests\MockSite\Models\Forum\Post. ( Ignorable by Annotation )

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

765
                DB::nonQuery((new Insert())->setTable(static::/** @scrutinizer ignore-call */ getHistoryTable())->set($set), null, [static::class,'handleException']);
Loading history...
766
            }
767
        }
768
769
        return static::delete($this->getPrimaryKeyValue());
770
    }
771
772
    /**
773
     * Delete by ID
774
     *
775
     * @param int $id
776
     * @return bool True if database returns number of affected rows above 0. False otherwise.
777
     */
778
    public static function delete($id)
779
    {
780
        DB::nonQuery((new Delete())->setTable(static::$tableName)->where(sprintf('`%s` = %u', static::_cn(static::$primaryKey ? static::$primaryKey : 'ID'), $id)), null, [static::class,'handleException']);
781
782
        return DB::affectedRows() > 0;
783
    }
784
785
    /**
786
     * Checks of a field exists for this model in the fields config.
787
     *
788
     * @param string $field Name of the field
789
     * @return bool True if the field exists. False otherwise.
790
     */
791
    public static function fieldExists($field)
792
    {
793
        static::init();
794
        return array_key_exists($field, static::$_classFields[get_called_class()]);
795
    }
796
797
    /**
798
     * Returns the current configuration of class fields for the called class.
799
     *
800
     * @return array Current configuration of class fields for the called class.
801
     */
802
    public static function getClassFields()
803
    {
804
        static::init();
805
        return static::$_classFields[get_called_class()];
806
    }
807
808
    /**
809
     * Returns either a field option or an array of all the field options.
810
     *
811
     * @param string $field Name of the field.
812
     * @param boolean $optionKey
813
     * @return void
814
     */
815
    public static function getFieldOptions($field, $optionKey = false)
816
    {
817
        if ($optionKey) {
818
            return static::$_classFields[get_called_class()][$field][$optionKey];
819
        } else {
820
            return static::$_classFields[get_called_class()][$field];
821
        }
822
    }
823
824
    /**
825
     * Returns columnName for given field
826
     * @param string $field name of field
827
     * @return string column name
828
     */
829
    public static function getColumnName($field)
830
    {
831
        static::init();
832
        if (!static::fieldExists($field)) {
833
            throw new Exception('getColumnName called on nonexisting column: ' . get_called_class().'->'.$field);
834
        }
835
836
        return static::$_classFields[get_called_class()][$field]['columnName'];
837
    }
838
839
    public static function mapFieldOrder($order)
840
    {
841
        return static::_mapFieldOrder($order);
842
    }
843
844
    public static function mapConditions($conditions)
845
    {
846
        return static::_mapConditions($conditions);
847
    }
848
849
    /**
850
     * Returns static::$rootClass for the called class.
851
     *
852
     * @return string static::$rootClass for the called class.
853
     */
854
    public function getRootClass()
855
    {
856
        return static::$rootClass;
857
    }
858
859
    /**
860
     * Sets an array of validation errors for this object.
861
     *
862
     * @param array $array Validation errors in the form Field Name => error message
863
     * @return void
864
     */
865
    public function addValidationErrors($array)
866
    {
867
        foreach ($array as $field => $errorMessage) {
868
            $this->addValidationError($field, $errorMessage);
869
        }
870
    }
871
872
    /**
873
     * Sets a validation error for this object. Sets $this->_isValid to false.
874
     *
875
     * @param string $field
876
     * @param string $errorMessage
877
     * @return void
878
     */
879
    public function addValidationError($field, $errorMessage)
880
    {
881
        $this->_isValid = false;
882
        $this->_validationErrors[$field] = $errorMessage;
883
    }
884
885
    /**
886
     * Get a validation error for a given field.
887
     *
888
     * @param string $field Name of the field.
889
     * @return string|null A validation error for the field. Null is no validation error found.
890
     */
891
    public function getValidationError($field)
892
    {
893
        // break apart path
894
        $crumbs = explode('.', $field);
895
896
        // resolve path recursively
897
        $cur = &$this->_validationErrors;
898
        while ($crumb = array_shift($crumbs)) {
899
            if (array_key_exists($crumb, $cur)) {
900
                $cur = &$cur[$crumb];
901
            } else {
902
                return null;
903
            }
904
        }
905
906
        // return current value
907
        return $cur;
908
    }
909
910
    /**
911
     * Validates the model. Instantiates a new RecordValidator object and sets it to $this->_validator.
912
     * Then validates against the set validators in this model. Returns $this->_isValid
913
     *
914
     * @param boolean $deep If true will attempt to validate any already loaded relationship members.
915
     * @return bool $this->_isValid which could be set to true or false depending on what happens with the RecordValidator.
916
     */
917
    public function validate($deep = true)
918
    {
919
        $this->_isValid = true;
920
        $this->_validationErrors = [];
921
922
        if (!isset($this->_validator)) {
923
            $this->_validator = new RecordValidator($this->_record);
924
        } else {
925
            $this->_validator->resetErrors();
926
        }
927
928
        foreach (static::$validators as $validator) {
929
            $this->_validator->validate($validator);
930
        }
931
932
        $this->finishValidation();
933
934
        if ($deep) {
935
            // validate relationship objects
936
            if (!empty(static::$_classRelationships[get_called_class()])) {
937
                foreach (static::$_classRelationships[get_called_class()] as $relationship => $options) {
938
                    if (empty($this->_relatedObjects[$relationship])) {
939
                        continue;
940
                    }
941
942
943
                    if ($options['type'] == 'one-one') {
944
                        if ($this->_relatedObjects[$relationship]->isDirty) {
945
                            $this->_relatedObjects[$relationship]->validate();
946
                            $this->_isValid = $this->_isValid && $this->_relatedObjects[$relationship]->isValid;
947
                            $this->_validationErrors[$relationship] = $this->_relatedObjects[$relationship]->validationErrors;
948
                        }
949
                    } elseif ($options['type'] == 'one-many') {
950
                        foreach ($this->_relatedObjects[$relationship] as $i => $object) {
951
                            if ($object->isDirty) {
952
                                $object->validate();
953
                                $this->_isValid = $this->_isValid && $object->isValid;
954
                                $this->_validationErrors[$relationship][$i] = $object->validationErrors;
955
                            }
956
                        }
957
                    }
958
                } // foreach
959
            } // if
960
        } // if ($deep)
961
962
        return $this->_isValid;
963
    }
964
965
    /**
966
     * Handle any errors that come from the database client in the process of running a query.
967
     * 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.
968
     * Other errors will be routed through to DB::handleException
969
     *
970
     * @param Exception $exception
971
     * @param string $query
972
     * @param array $queryLog
973
     * @param array|string $parameters
974
     * @return mixed Retried query result or the return from DB::handleException
975
     */
976
    public static function handleException(\Exception $e, $query = null, $queryLog = null, $parameters = null)
977
    {
978
        $Connection = DB::getConnection(); 
979
        if ($Connection->errorCode() == '42S02' && static::$autoCreateTables) {
980
            $CreateTable = SQL::getCreateTable(static::$rootClass);
981
982
            // history versions table
983
            if (static::isVersioned()) {
984
                $CreateTable .= SQL::getCreateTable(static::$rootClass, true);
985
            }
986
987
            $Statement = $Connection->query($CreateTable);
988
989
            // check for errors
990
            $ErrorInfo = $Statement->errorInfo();
991
992
            // handle query error
993
            if ($ErrorInfo[0] != '00000') {
994
                self::handleException($query, $queryLog);
0 ignored issues
show
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

994
                self::handleException(/** @scrutinizer ignore-type */ $query, $queryLog);
Loading history...
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

994
                self::handleException($query, /** @scrutinizer ignore-type */ $queryLog);
Loading history...
995
            }
996
997
            // clear buffer (required for the next query to work without running fetchAll first
998
            $Statement->closeCursor();
999
1000
            return $Connection->query((string)$query); // now the query should finish with no error
1001
        } else {
1002
            return DB::handleException($e, $query, $queryLog);
1003
        }
1004
    }
1005
1006
    /**
1007
     * Iterates through all static::$beforeSave and static::$afterSave in this class and any of it's parent classes.
1008
     * Checks if they are callables and if they are adds them to static::$_classBeforeSave[] and static::$_classAfterSave[]
1009
     *
1010
     * @return void
1011
     *
1012
     * @uses static::$beforeSave
1013
     * @uses static::$afterSave
1014
     * @uses static::$_classBeforeSave
1015
     * @uses static::$_classAfterSave
1016
     */
1017
    protected static function _defineEvents()
1018
    {
1019
        // run before save
1020
        $className = get_called_class();
1021
1022
        // merge fields from first ancestor up
1023
        $classes = class_parents($className);
1024
        array_unshift($classes, $className);
1025
1026
        while ($class = array_pop($classes)) {
1027
            if (is_callable($class::$beforeSave)) {
1028
                if (!empty($class::$beforeSave)) {
1029
                    if (!in_array($class::$beforeSave, static::$_classBeforeSave)) {
1030
                        static::$_classBeforeSave[] = $class::$beforeSave;
1031
                    }
1032
                }
1033
            }
1034
1035
            if (is_callable($class::$afterSave)) {
1036
                if (!empty($class::$afterSave)) {
1037
                    if (!in_array($class::$afterSave, static::$_classAfterSave)) {
1038
                        static::$_classAfterSave[] = $class::$afterSave;
1039
                    }
1040
                }
1041
            }
1042
        }
1043
    }
1044
1045
    /**
1046
     * Merges all static::$_classFields in this class and any of it's parent classes.
1047
     * Sets the merged value to static::$_classFields[get_called_class()]
1048
     *
1049
     * @return void
1050
     *
1051
     * @uses static::$_classFields
1052
     * @uses static::$_classFields
1053
     */
1054
    protected static function _defineFields()
1055
    {
1056
        $className = get_called_class();
1057
1058
        // skip if fields already defined
1059
        if (isset(static::$_classFields[$className])) {
1060
            return;
1061
        }
1062
1063
        // merge fields from first ancestor up
1064
        $classes = class_parents($className);
1065
        array_unshift($classes, $className);
1066
1067
        static::$_classFields[$className] = [];
1068
        while ($class = array_pop($classes)) {
1069
            if (!empty($class::$fields)) {
1070
                static::$_classFields[$className] = array_merge(static::$_classFields[$className], $class::$fields);
1071
            }
1072
        }
1073
1074
        // versioning
1075
        if (static::isVersioned()) {
1076
            static::$_classFields[$className] = array_merge(static::$_classFields[$className], static::$versioningFields);
1077
        }
1078
    }
1079
1080
1081
    /**
1082
     * Called after _defineFields to initialize and apply defaults to the fields property
1083
     * Must be idempotent as it may be applied multiple times up the inheritance chain
1084
     * @return void
1085
     *
1086
     * @uses static::$_classFields
1087
     */
1088
    protected static function _initFields()
1089
    {
1090
        $className = get_called_class();
1091
        $optionsMask = [
1092
            'type' => null,
1093
            'length' => null,
1094
            'primary' => null,
1095
            'unique' => null,
1096
            'autoincrement' => null,
1097
            'notnull' => null,
1098
            'unsigned' => null,
1099
            'default' => null,
1100
            'values' => null,
1101
        ];
1102
1103
        // apply default values to field definitions
1104
        if (!empty(static::$_classFields[$className])) {
1105
            $fields = [];
1106
1107
            foreach (static::$_classFields[$className] as $field => $options) {
1108
                if (is_string($field)) {
1109
                    if (is_array($options)) {
1110
                        $fields[$field] = array_merge($optionsMask, static::$fieldDefaults, ['columnName' => $field], $options);
1111
                    } elseif (is_string($options)) {
1112
                        $fields[$field] = array_merge($optionsMask, static::$fieldDefaults, ['columnName' => $field, 'type' => $options]);
1113
                    } elseif ($options == null) {
1114
                        continue;
1115
                    }
1116
                } elseif (is_string($options)) {
1117
                    $field = $options;
1118
                    $fields[$field] = array_merge($optionsMask, static::$fieldDefaults, ['columnName' => $field]);
1119
                }
1120
1121
                if ($field == 'Class') {
1122
                    // apply Class enum values
1123
                    $fields[$field]['values'] = static::$subClasses;
1124
                }
1125
1126
                if (!isset($fields[$field]['blankisnull']) && empty($fields[$field]['notnull'])) {
1127
                    $fields[$field]['blankisnull'] = true;
1128
                }
1129
1130
                if ($fields[$field]['autoincrement']) {
1131
                    $fields[$field]['primary'] = true;
1132
                }
1133
            }
1134
1135
            static::$_classFields[$className] = $fields;
1136
        }
1137
    }
1138
1139
1140
    /**
1141
     * Returns class name for instantiating given record
1142
     * @param array $record record
1143
     * @return string class name
1144
     */
1145
    protected static function _getRecordClass($record)
1146
    {
1147
        $static = get_called_class();
1148
1149
        if (!static::fieldExists('Class')) {
1150
            return $static;
1151
        }
1152
1153
        $columnName = static::_cn('Class');
1154
1155
        if (!empty($record[$columnName]) && is_subclass_of($record[$columnName], $static)) {
1156
            return $record[$columnName];
1157
        } else {
1158
            return $static;
1159
        }
1160
    }
1161
1162
    /**
1163
     * Shorthand alias for _getColumnName
1164
     * @param string $field name of field
1165
     * @return string column name
1166
     */
1167
    protected static function _cn($field)
1168
    {
1169
        return static::getColumnName($field);
1170
    }
1171
1172
1173
    /**
1174
     * Retrieves given field's value
1175
     * @param string $field Name of field
1176
     * @return mixed value
1177
     */
1178
    protected function _getFieldValue($field, $useDefault = true)
1179
    {
1180
        $fieldOptions = static::$_classFields[get_called_class()][$field];
1181
1182
        if (isset($this->_record[$fieldOptions['columnName']])) {
1183
            $value = $this->_record[$fieldOptions['columnName']];
1184
1185
            // apply type-dependent transformations
1186
            switch ($fieldOptions['type']) {
1187
                case 'password':
1188
                    {
1189
                        return $value;
1190
                    }
1191
1192
                case 'timestamp':
1193
                    {
1194
                        if (!isset($this->_convertedValues[$field])) {
1195
                            if ($value && $value != '0000-00-00 00:00:00') {
1196
                                $this->_convertedValues[$field] = strtotime($value);
1197
                            } else {
1198
                                $this->_convertedValues[$field] = null;
1199
                            }
1200
                        }
1201
1202
                        return $this->_convertedValues[$field];
1203
                    }
1204
                case 'serialized':
1205
                    {
1206
                        if (!isset($this->_convertedValues[$field])) {
1207
                            $this->_convertedValues[$field] = is_string($value) ? unserialize($value) : $value;
1208
                        }
1209
1210
                        return $this->_convertedValues[$field];
1211
                    }
1212
                case 'set':
1213
                case 'list':
1214
                    {
1215
                        if (!isset($this->_convertedValues[$field])) {
1216
                            $delim = empty($fieldOptions['delimiter']) ? ',' : $fieldOptions['delimiter'];
1217
                            $this->_convertedValues[$field] = array_filter(preg_split('/\s*'.$delim.'\s*/', $value));
1218
                        }
1219
1220
                        return $this->_convertedValues[$field];
1221
                    }
1222
1223
                case 'int':
1224
                case 'integer':
1225
                case 'uint':
1226
                    if (!isset($this->_convertedValues[$field])) {
1227
                        if (!$fieldOptions['notnull'] && is_null($value)) {
1228
                            $this->_convertedValues[$field] = $value;
1229
                        } else {
1230
                            $this->_convertedValues[$field] = intval($value);
1231
                        }
1232
                    }
1233
                    return $this->_convertedValues[$field];
1234
1235
                case 'boolean':
1236
                    {
1237
                        if (!isset($this->_convertedValues[$field])) {
1238
                            $this->_convertedValues[$field] = (bool)$value;
1239
                        }
1240
1241
                        return $this->_convertedValues[$field];
1242
                    }
1243
1244
                default:
1245
                    {
1246
                        return $value;
1247
                    }
1248
            }
1249
        } elseif ($useDefault && isset($fieldOptions['default'])) {
1250
            // return default
1251
            return $fieldOptions['default'];
1252
        } else {
1253
            switch ($fieldOptions['type']) {
1254
                case 'set':
1255
                case 'list':
1256
                    {
1257
                        return [];
1258
                    }
1259
                default:
1260
                    {
1261
                        return null;
1262
                    }
1263
            }
1264
        }
1265
    }
1266
1267
    /**
1268
     * Sets given field's value
1269
     * @param string $field Name of field
1270
     * @param mixed $value New value
1271
     * @return mixed value
1272
     */
1273
    protected function _setFieldValue($field, $value)
1274
    {
1275
        // ignore setting versioning fields
1276
        if (static::isVersioned()) {
1277
            if (array_key_exists($field, static::$versioningFields)) {
1278
                return false;
1279
            }
1280
        }
1281
1282
        if (!static::fieldExists($field)) {
1283
            return false;
1284
        }
1285
        $fieldOptions = static::$_classFields[get_called_class()][$field];
1286
1287
        // no overriding autoincrements
1288
        if ($fieldOptions['autoincrement']) {
1289
            return false;
1290
        }
1291
1292
        if (!isset($this->fieldSetMapper)) {
1293
            $this->fieldSetMapper = new DefaultSetMapper();
1294
        }
1295
1296
        // pre-process value
1297
        $forceDirty = false;
1298
        switch ($fieldOptions['type']) {
1299
            case 'clob':
1300
            case 'string':
1301
                {
1302
                    $value = $this->fieldSetMapper->setStringValue($value);
0 ignored issues
show
Bug introduced by
The method setStringValue() does not exist on null. ( Ignorable by Annotation )

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

1302
                    /** @scrutinizer ignore-call */ 
1303
                    $value = $this->fieldSetMapper->setStringValue($value);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
1303
                    if (!$fieldOptions['notnull'] && $fieldOptions['blankisnull'] && ($value === '' || $value === null)) {
1304
                        $value = null;
1305
                    }
1306
                    break;
1307
                }
1308
1309
            case 'boolean':
1310
                {
1311
                    $value = $this->fieldSetMapper->setBooleanValue($value);
1312
                    break;
1313
                }
1314
1315
            case 'decimal':
1316
                {
1317
                    $value = $this->fieldSetMapper->setDecimalValue($value);
1318
                    break;
1319
                }
1320
1321
            case 'int':
1322
            case 'uint':
1323
            case 'integer':
1324
                {
1325
                    $value = $this->fieldSetMapper->setIntegerValue($value);
1326
                    if (!$fieldOptions['notnull'] && ($value === '' || is_null($value))) {
1327
                        $value = null;
1328
                    }
1329
                    break;
1330
                }
1331
1332
            case 'timestamp':
1333
                {
1334
                    $value = $this->fieldSetMapper->setTimestampValue($value);
1335
                    break;
1336
                }
1337
1338
            case 'date':
1339
                {
1340
                    $value = $this->fieldSetMapper->setDateValue($value);
1341
                    break;
1342
                }
1343
1344
                // these types are converted to strings from another PHP type on save
1345
            case 'serialized':
1346
                {
1347
                    $this->_convertedValues[$field] = $value;
1348
                    $value = $this->fieldSetMapper->setSerializedValue($value);
1349
                    break;
1350
                }
1351
            case 'enum':
1352
                {
1353
                    $value = $this->fieldSetMapper->setEnumValue($fieldOptions['values'], $value);
1354
                    break;
1355
                }
1356
            case 'set':
1357
            case 'list':
1358
                {
1359
                    $value = $this->fieldSetMapper->setListValue($value, isset($fieldOptions['delimiter']) ? $fieldOptions['delimiter'] : null);
1360
                    $this->_convertedValues[$field] = $value;
1361
                    $forceDirty = true;
1362
                    break;
1363
                }
1364
        }
1365
1366
        if ($forceDirty || (empty($this->_record[$field]) && isset($value)) || ($this->_record[$field] !== $value)) {
1367
            $columnName = static::_cn($field);
1368
            if (isset($this->_record[$columnName])) {
1369
                $this->_originalValues[$field] = $this->_record[$columnName];
1370
            }
1371
            $this->_record[$columnName] = $value;
1372
            $this->_isDirty = true;
1373
1374
            // unset invalidated relationships
1375
            if (!empty($fieldOptions['relationships']) && static::isRelational()) {
1376
                foreach ($fieldOptions['relationships'] as $relationship => $isCached) {
1377
                    if ($isCached) {
1378
                        unset($this->_relatedObjects[$relationship]);
0 ignored issues
show
Bug Best Practice introduced by
The property _relatedObjects does not exist on Divergence\Models\ActiveRecord. Since you implemented __get, consider adding a @property annotation.
Loading history...
1379
                    }
1380
                }
1381
            }
1382
            return true;
1383
        } else {
1384
            return false;
1385
        }
1386
    }
1387
1388
    protected function _prepareRecordValues()
1389
    {
1390
        $record = [];
1391
1392
        foreach (static::$_classFields[get_called_class()] as $field => $options) {
1393
            $columnName = static::_cn($field);
1394
1395
            if (array_key_exists($columnName, $this->_record)) {
1396
                $value = $this->_record[$columnName];
1397
1398
                if (!$value && !empty($options['blankisnull'])) {
1399
                    $value = null;
1400
                }
1401
            } elseif (isset($options['default'])) {
1402
                $value = $options['default'];
1403
            } else {
1404
                continue;
1405
            }
1406
1407
            if (($options['type'] == 'date') && ($value == '0000-00-00') && !empty($options['blankisnull'])) {
1408
                $value = null;
1409
            }
1410
            if (($options['type'] == 'timestamp')) {
1411
                if (is_numeric($value)) {
1412
                    $value = date('Y-m-d H:i:s', $value);
1413
                } elseif ($value == null && !$options['notnull']) {
1414
                    $value = null;
1415
                }
1416
            }
1417
1418
            if (($options['type'] == 'serialized') && !is_string($value)) {
1419
                $value = serialize($value);
1420
            }
1421
1422
            if (($options['type'] == 'list') && is_array($value)) {
1423
                $delim = empty($options['delimiter']) ? ',' : $options['delimiter'];
1424
                $value = implode($delim, $value);
1425
            }
1426
1427
            $record[$field] = $value;
1428
        }
1429
1430
        return $record;
1431
    }
1432
1433
    protected static function _mapValuesToSet($recordValues)
1434
    {
1435
        $set = [];
1436
1437
        foreach ($recordValues as $field => $value) {
1438
            $fieldConfig = static::$_classFields[get_called_class()][$field];
1439
1440
            if ($value === null) {
1441
                $set[] = sprintf('`%s` = NULL', $fieldConfig['columnName']);
1442
            } elseif ($fieldConfig['type'] == 'timestamp' && $value == 'CURRENT_TIMESTAMP') {
1443
                $set[] = sprintf('`%s` = CURRENT_TIMESTAMP', $fieldConfig['columnName']);
1444
            } elseif ($fieldConfig['type'] == 'set' && is_array($value)) {
1445
                $set[] = sprintf('`%s` = "%s"', $fieldConfig['columnName'], DB::escape(join(',', $value)));
1446
            } elseif ($fieldConfig['type'] == 'boolean') {
1447
                $set[] = sprintf('`%s` = %u', $fieldConfig['columnName'], $value ? 1 : 0);
1448
            } else {
1449
                $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

1449
                $set[] = sprintf('`%s` = "%s"', $fieldConfig['columnName'], /** @scrutinizer ignore-type */ DB::escape($value));
Loading history...
1450
            }
1451
        }
1452
1453
        return $set;
1454
    }
1455
1456
    protected static function _mapFieldOrder($order)
1457
    {
1458
        if (is_string($order)) {
1459
            return [$order];
1460
        } elseif (is_array($order)) {
1461
            $r = [];
1462
1463
            foreach ($order as $key => $value) {
1464
                if (is_string($key)) {
1465
                    $columnName = static::_cn($key);
1466
                    $direction = strtoupper($value)=='DESC' ? 'DESC' : 'ASC';
1467
                } else {
1468
                    $columnName = static::_cn($value);
1469
                    $direction = 'ASC';
1470
                }
1471
1472
                $r[] = sprintf('`%s` %s', $columnName, $direction);
1473
            }
1474
1475
            return $r;
1476
        }
1477
    }
1478
1479
    protected static function _mapConditions($conditions)
1480
    {
1481
        foreach ($conditions as $field => &$condition) {
1482
            if (is_string($field)) {
1483
                if (isset(static::$_classFields[get_called_class()][$field])) {
1484
                    $fieldOptions = static::$_classFields[get_called_class()][$field];
1485
                }
1486
1487
                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...
1488
                    $condition = sprintf('`%s` IS NULL', static::_cn($field));
1489
                } elseif (is_array($condition)) {
1490
                    $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

1490
                    $condition = sprintf('`%s` %s "%s"', static::_cn($field), $condition['operator'], /** @scrutinizer ignore-type */ DB::escape($condition['value']));
Loading history...
1491
                } else {
1492
                    $condition = sprintf('`%s` = "%s"', static::_cn($field), DB::escape($condition));
1493
                }
1494
            }
1495
        }
1496
1497
        return $conditions;
1498
    }
1499
1500
    protected function finishValidation()
1501
    {
1502
        $this->_isValid = $this->_isValid && !$this->_validator->hasErrors();
1503
1504
        if (!$this->_isValid) {
1505
            $this->_validationErrors = array_merge($this->_validationErrors, $this->_validator->getErrors());
1506
        }
1507
1508
        return $this->_isValid;
1509
    }
1510
}
1511