Passed
Push — develop ( 3046e8...f3e3d3 )
by Henry
13:14
created

ActiveRecord::getValidationError()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 17
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 3

Importance

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

402
            static::/** @scrutinizer ignore-call */ 
403
                    _defineRelationships();
Loading history...
403 3
            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

403
            static::/** @scrutinizer ignore-call */ 
404
                    _initRelationships();
Loading history...
404
405 3
            static::$_relationshipsDefined[$className] = true;
406
        }
407
408 109
        if (empty(static::$_eventsDefined[$className])) {
409 6
            static::_defineEvents();
410
411 6
            static::$_eventsDefined[$className] = true;
412
        }
413
    }
414
415
    /**
416
     * getValue Pass thru for __get
417
     *
418
     * @param string $name The name of the field you want to get.
419
     *
420
     * @return mixed Value of the field you wanted if it exists or null otherwise.
421
     */
422 68
    public function getValue($name)
423
    {
424
        switch ($name) {
425 68
            case 'isDirty':
426 22
                return $this->_isDirty;
427
428 64
            case 'isPhantom':
429 9
                return $this->_isPhantom;
430
431 62
            case 'wasPhantom':
432 3
                return $this->_wasPhantom;
433
434 62
            case 'isValid':
435 3
                return $this->_isValid;
436
437 62
            case 'isNew':
438 3
                return $this->_isNew;
439
440 62
            case 'isUpdated':
441 3
                return $this->_isUpdated;
442
443 62
            case 'validationErrors':
444 37
                return array_filter($this->_validationErrors);
445
446 56
            case 'data':
447 25
                return $this->getData();
448
449 41
            case 'originalValues':
450 3
                return $this->_originalValues;
451
452
            default:
453
                {
454
                    // handle field
455 39
                    if (static::fieldExists($name)) {
456 33
                        return $this->_getFieldValue($name);
457
                    }
458
                    // handle relationship
459 12
                    elseif (static::isRelational()) {
460 10
                        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

460
                        if (static::/** @scrutinizer ignore-call */ _relationshipExists($name)) {
Loading history...
461 10
                            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

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

703
            $this->/** @scrutinizer ignore-call */ 
704
                   beforeVersionedSave();
Loading history...
704
        }
705
706
        // set created
707 19
        if (static::fieldExists('Created') && (!$this->Created || ($this->Created == 'CURRENT_TIMESTAMP'))) {
708 7
            $this->Created = $this->_record['Created'] = time();
709 7
            unset($this->_convertedValues['Created']);
710
        }
711
712
        // validate
713 19
        if (!$this->validate($deep)) {
714
            throw new Exception('Cannot save invalid record');
715
        }
716
717 19
        $this->clearCaches();
718
719 19
        if ($this->isDirty) {
720
            // prepare record values
721 18
            $recordValues = $this->_prepareRecordValues();
722
723
            // transform record to set array
724 18
            $set = static::_mapValuesToSet($recordValues);
725
726
            // create new or update existing
727 18
            if ($this->_isPhantom) {
728 14
                DB::nonQuery((new Insert())->setTable(static::$tableName)->set($set), null, [static::class,'handleException']);
729 13
                $primaryKey = $this->getPrimaryKey();
730 13
                $insertID = DB::insertID();
731 13
                $fields = static::getClassFields();
732 13
                if ( ($fields[$primaryKey]['type'] ?? false) === 'integer') {
733 13
                    $insertID = intval($insertID);
734
                }
735 13
                $this->_record[$primaryKey] = $insertID;
736 13
                $this->$primaryKey = $insertID; 
737 13
                $this->_isPhantom = false;
738 13
                $this->_isNew = true;
739 5
            } elseif (count($set)) {
740 5
                DB::nonQuery((new Update())->setTable(static::$tableName)->set($set)->where(
741 5
                    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; 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

741
                    sprintf('`%s` = %u', static::_cn($this->getPrimaryKey()), /** @scrutinizer ignore-type */ $this->getPrimaryKeyValue())
Loading history...
742 5
                ), null, [static::class,'handleException']);
743
744 4
                $this->_isUpdated = true;
745
            }
746
747
            // update state
748 16
            $this->_isDirty = false;
749 16
            if (static::isVersioned()) {
750 10
                $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

750
                $this->/** @scrutinizer ignore-call */ 
751
                       afterVersionedSave();
Loading history...
751
            }
752
        }
753 17
        $this->afterSave();
754
    }
755
756
757
    /**
758
     * Deletes this object.
759
     *
760
     * @return bool True if database returns number of affected rows above 0. False otherwise.
761
     */
762 9
    public function destroy()
763
    {
764 9
        if (static::isVersioned()) {
765 5
            if (static::$createRevisionOnDestroy) {
766
                // save a copy to history table
767 5
                if ($this->fieldExists('Created')) {
768 5
                    $this->Created = time();
769
                }
770
771 5
                $recordValues = $this->_prepareRecordValues();
772 5
                $set = static::_mapValuesToSet($recordValues);
773
774 5
                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

774
                DB::nonQuery((new Insert())->setTable(static::/** @scrutinizer ignore-call */ getHistoryTable())->set($set), null, [static::class,'handleException']);
Loading history...
775
            }
776
        }
777
778 8
        return static::delete($this->getPrimaryKeyValue());
0 ignored issues
show
Bug introduced by
It seems like $this->getPrimaryKeyValue() can also be of type array; however, parameter $id of Divergence\Models\ActiveRecord::delete() does only seem to accept integer, 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

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

1003
                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

1003
                self::handleException($query, /** @scrutinizer ignore-type */ $queryLog);
Loading history...
1004
            }
1005
1006
            // clear buffer (required for the next query to work without running fetchAll first
1007 2
            $Statement->closeCursor();
1008
1009 2
            return $Connection->query((string)$query); // now the query should finish with no error
1010
        } else {
1011 5
            return DB::handleException($e, $query, $queryLog);
1012
        }
1013
    }
1014
1015
    /**
1016
     * Iterates through all static::$beforeSave and static::$afterSave in this class and any of it's parent classes.
1017
     * Checks if they are callables and if they are adds them to static::$_classBeforeSave[] and static::$_classAfterSave[]
1018
     *
1019
     * @return void
1020
     *
1021
     * @uses static::$beforeSave
1022
     * @uses static::$afterSave
1023
     * @uses static::$_classBeforeSave
1024
     * @uses static::$_classAfterSave
1025
     */
1026 6
    protected static function _defineEvents()
1027
    {
1028
        // run before save
1029 6
        $className = get_called_class();
1030
1031
        // merge fields from first ancestor up
1032 6
        $classes = class_parents($className);
1033 6
        array_unshift($classes, $className);
1034
1035 6
        while ($class = array_pop($classes)) {
1036 6
            if (is_callable($class::$beforeSave)) {
1037
                if (!empty($class::$beforeSave)) {
1038
                    if (!in_array($class::$beforeSave, static::$_classBeforeSave)) {
1039
                        static::$_classBeforeSave[] = $class::$beforeSave;
1040
                    }
1041
                }
1042
            }
1043
1044 6
            if (is_callable($class::$afterSave)) {
1045
                if (!empty($class::$afterSave)) {
1046
                    if (!in_array($class::$afterSave, static::$_classAfterSave)) {
1047
                        static::$_classAfterSave[] = $class::$afterSave;
1048
                    }
1049
                }
1050
            }
1051
        }
1052
    }
1053
1054
    /**
1055
     * Merges all static::$_classFields in this class and any of it's parent classes.
1056
     * Sets the merged value to static::$_classFields[get_called_class()]
1057
     *
1058
     * @return void
1059
     *
1060
     * @uses static::$_classFields
1061
     * @uses static::$_classFields
1062
     */
1063 6
    protected static function _defineFields()
1064
    {
1065 6
        $className = get_called_class();
1066
1067
        // skip if fields already defined
1068 6
        if (isset(static::$_classFields[$className])) {
1069
            return;
1070
        }
1071
1072
        // merge fields from first ancestor up
1073 6
        $classes = class_parents($className);
1074 6
        array_unshift($classes, $className);
1075
1076 6
        static::$_classFields[$className] = [];
1077 6
        while ($class = array_pop($classes)) {
1078 6
            if (!empty($class::$fields)) {
1079 1
                static::$_classFields[$className] = array_merge(static::$_classFields[$className], $class::$fields);
1080
            }
1081 6
            if ($attributeFields = $class::_getAttributeFields()) {
1082 5
                static::$_classFields[$className] = array_merge(static::$_classFields[$className], $attributeFields);
1083
            }
1084
        }
1085
    }
1086
1087
    /**
1088
     * This function grabs all protected fields on the model and uses that as the basis for what constitutes a mapped field
1089
     * It skips a certain list of protected fields that are built in for ORM operation
1090
     *
1091
     * @return array
1092
     */
1093 46
    public static function _getAttributeFields(): array
1094
    {
1095 46
        $attributeMappedClassFields = [];
1096 46
        $properties = (new ReflectionClass(static::class))->getProperties();
1097 46
        if(!empty($properties)) {
1098 46
            foreach ($properties as $property) {
1099 46
                if ($property->isProtected()) {
1100
1101
                    // skip these because they are built in
1102 46
                    if (in_array($property->getName(),[
1103 46
                        '_classFields','_classRelationships','_classBeforeSave','_classAfterSave','_fieldsDefined','_relationshipsDefined','_eventsDefined','_record','_validator'
1104 46
                        ,'_validationErrors','_isDirty','_isValid','fieldSetMapper','_convertedValues','_originalValues','_isPhantom','_wasPhantom','_isNew','_isUpdated','_relatedObjects'
1105 46
                    ])) {
1106 46
                        continue;
1107
                    }
1108
1109 46
                    if ($attributes = $property->getAttributes()) {
1110 46
                        foreach($attributes as $attribute) {
1111 46
                            $attributeMappedClassFields[$property->getName()] = $attribute->getArguments();
1112
                        }
1113
                    } else {
1114
                        // default
1115 24
                        $attributeMappedClassFields[$property->getName()] = [];
1116
                    }
1117
                }
1118
             }
1119
        }
1120 46
        return $attributeMappedClassFields;
1121
    }
1122
1123
1124
    /**
1125
     * Called after _defineFields to initialize and apply defaults to the fields property
1126
     * Must be idempotent as it may be applied multiple times up the inheritance chain
1127
     * @return void
1128
     *
1129
     * @uses static::$_classFields
1130
     */
1131 6
    protected static function _initFields()
1132
    {
1133 6
        $className = get_called_class();
1134 6
        $optionsMask = [
1135 6
            'type' => null,
1136 6
            'length' => null,
1137 6
            'primary' => null,
1138 6
            'unique' => null,
1139 6
            'autoincrement' => null,
1140 6
            'notnull' => null,
1141 6
            'unsigned' => null,
1142 6
            'default' => null,
1143 6
            'values' => null,
1144 6
        ];
1145
1146
        // apply default values to field definitions
1147 6
        if (!empty(static::$_classFields[$className])) {
1148 5
            $fields = [];
1149
1150 5
            foreach (static::$_classFields[$className] as $field => $options) {
1151 5
                if (is_string($field)) {
1152 5
                    if (is_array($options)) {
1153 5
                        $fields[$field] = array_merge($optionsMask, static::$fieldDefaults, ['columnName' => $field], $options);
1154 1
                    } elseif (is_string($options)) {
1155 1
                        $fields[$field] = array_merge($optionsMask, static::$fieldDefaults, ['columnName' => $field, 'type' => $options]);
1156
                    } elseif ($options == null) {
1157 5
                        continue;
1158
                    }
1159
                } elseif (is_string($options)) {
1160
                    $field = $options;
1161
                    $fields[$field] = array_merge($optionsMask, static::$fieldDefaults, ['columnName' => $field]);
1162
                }
1163
1164 5
                if ($field == 'Class') {
1165
                    // apply Class enum values
1166 5
                    $fields[$field]['values'] = static::$subClasses;
1167
                }
1168
1169 5
                if (!isset($fields[$field]['blankisnull']) && empty($fields[$field]['notnull'])) {
1170 5
                    $fields[$field]['blankisnull'] = true;
1171
                }
1172
1173 5
                if ($fields[$field]['autoincrement']) {
1174 5
                    $fields[$field]['primary'] = true;
1175
                }
1176
            }
1177
1178 5
            static::$_classFields[$className] = $fields;
1179
        }
1180
    }
1181
1182
1183
    /**
1184
     * Returns class name for instantiating given record
1185
     * @param array $record record
1186
     * @return string class name
1187
     */
1188 82
    protected static function _getRecordClass($record)
1189
    {
1190 82
        $static = get_called_class();
1191
1192 82
        if (!static::fieldExists('Class')) {
1193
            return $static;
1194
        }
1195
1196 82
        $columnName = static::_cn('Class');
1197
1198 82
        if (!empty($record[$columnName]) && is_subclass_of($record[$columnName], $static)) {
1199 4
            return $record[$columnName];
1200
        } else {
1201 78
            return $static;
1202
        }
1203
    }
1204
1205
    /**
1206
     * Shorthand alias for _getColumnName
1207
     * @param string $field name of field
1208
     * @return string column name
1209
     */
1210 105
    protected static function _cn($field)
1211
    {
1212 105
        return static::getColumnName($field);
1213
    }
1214
1215
1216
    /**
1217
     * Retrieves given field's value
1218
     * @param string $field Name of field
1219
     * @return mixed value
1220
     */
1221 66
    protected function _getFieldValue($field, $useDefault = true)
1222
    {
1223 66
        $fieldOptions = static::$_classFields[get_called_class()][$field];
1224
1225 66
        if (isset($this->_record[$fieldOptions['columnName']])) {
1226 64
            $value = $this->_record[$fieldOptions['columnName']];
1227
1228
            // apply type-dependent transformations
1229 64
            switch ($fieldOptions['type']) {
1230 64
                case 'password':
1231
                    {
1232 16
                        return $value;
1233
                    }
1234
1235 64
                case 'timestamp':
1236
                    {
1237 32
                        if (!isset($this->_convertedValues[$field])) {
1238 32
                            if ($value && is_string($value) && $value != '0000-00-00 00:00:00') {
1239 32
                                $this->_convertedValues[$field] = strtotime($value);
1240 3
                            } else if (is_integer($value)) {
1241 3
                                $this->_convertedValues[$field] = $value;
1242
                            } else {
1243
                                unset($this->_convertedValues[$field]);
1244
                            }
1245
                        }
1246
1247 32
                        return $this->_convertedValues[$field];
1248
                    }
1249 64
                case 'serialized':
1250
                    {
1251 16
                        if (!isset($this->_convertedValues[$field])) {
1252 16
                            $this->_convertedValues[$field] = is_string($value) ? unserialize($value) : $value;
1253
                        }
1254
1255 16
                        return $this->_convertedValues[$field];
1256
                    }
1257 64
                case 'set':
1258 64
                case 'list':
1259
                    {
1260 16
                        if (!isset($this->_convertedValues[$field])) {
1261 8
                            $delim = empty($fieldOptions['delimiter']) ? ',' : $fieldOptions['delimiter'];
1262 8
                            $this->_convertedValues[$field] = array_filter(preg_split('/\s*'.$delim.'\s*/', $value));
1263
                        }
1264
1265 16
                        return $this->_convertedValues[$field];
1266
                    }
1267
1268 64
                case 'int':
1269 64
                case 'integer':
1270 49
                case 'uint':
1271 49
                    if (!isset($this->_convertedValues[$field])) {
1272 49
                        if (!$fieldOptions['notnull'] && is_null($value)) {
1273
                            $this->_convertedValues[$field] = $value;
1274
                        } else {
1275 49
                            $this->_convertedValues[$field] = intval($value);
1276
                        }
1277
                    }
1278 49
                    return $this->_convertedValues[$field];
1279
1280 49
                case 'boolean':
1281
                    {
1282 16
                        if (!isset($this->_convertedValues[$field])) {
1283 16
                            $this->_convertedValues[$field] = (bool)$value;
1284
                        }
1285
1286 16
                        return $this->_convertedValues[$field];
1287
                    }
1288
1289
                default:
1290
                    {
1291 49
                        return $value;
1292
                    }
1293
            }
1294 40
        } elseif ($useDefault && isset($fieldOptions['default'])) {
1295
            // return default
1296 16
            return $fieldOptions['default'];
1297
        } else {
1298 40
            switch ($fieldOptions['type']) {
1299 40
                case 'set':
1300 40
                case 'list':
1301
                    {
1302
                        return [];
1303
                    }
1304
                default:
1305
                    {
1306 40
                        return null;
1307
                    }
1308
            }
1309
        }
1310
    }
1311
1312
    /**
1313
     * Sets given field's value
1314
     * @param string $field Name of field
1315
     * @param mixed $value New value
1316
     * @return mixed value
1317
     */
1318 94
    protected function _setFieldValue($field, $value)
1319
    {
1320
        // ignore setting versioning fields
1321 94
        if (static::isVersioned()) {
1322 53
            if ($field === 'RevisionID') {
1323 1
                return false;
1324
            }
1325
        }
1326
1327 94
        if (!static::fieldExists($field)) {
1328
            return false;
1329
        }
1330 94
        $fieldOptions = static::$_classFields[get_called_class()][$field];
1331
1332
        // no overriding autoincrements
1333 94
        if ($fieldOptions['autoincrement']) {
1334 2
            return false;
1335
        }
1336
1337 94
        if (!isset($this->fieldSetMapper)) {
1338 94
            $this->fieldSetMapper = new DefaultSetMapper();
1339
        }
1340
1341
        // pre-process value
1342 94
        $forceDirty = false;
1343 94
        switch ($fieldOptions['type']) {
1344 94
            case 'clob':
1345 94
            case 'string':
1346
                {
1347 35
                    $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

1347
                    /** @scrutinizer ignore-call */ 
1348
                    $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...
1348 35
                    if (!$fieldOptions['notnull'] && $fieldOptions['blankisnull'] && ($value === '' || $value === null)) {
1349 1
                        $value = null;
1350
                    }
1351 35
                    break;
1352
                }
1353
1354 94
            case 'boolean':
1355
                {
1356 12
                    $value = $this->fieldSetMapper->setBooleanValue($value);
1357 12
                    break;
1358
                }
1359
1360 94
            case 'decimal':
1361
                {
1362 12
                    $value = $this->fieldSetMapper->setDecimalValue($value);
1363 12
                    break;
1364
                }
1365
1366 94
            case 'int':
1367 94
            case 'uint':
1368 94
            case 'integer':
1369
                {
1370 14
                    $value = $this->fieldSetMapper->setIntegerValue($value);
1371 14
                    if (!$fieldOptions['notnull'] && ($value === '' || is_null($value))) {
1372 2
                        $value = null;
1373
                    }
1374 14
                    break;
1375
                }
1376
1377 94
            case 'timestamp':
1378
                {
1379 14
                    unset($this->_convertedValues[$field]);
1380 14
                    $value = $this->fieldSetMapper->setTimestampValue($value);
1381 14
                    break;
1382
                }
1383
1384 94
            case 'date':
1385
                {
1386 13
                    unset($this->_convertedValues[$field]);
1387 13
                    $value = $this->fieldSetMapper->setDateValue($value);
1388 13
                    break;
1389
                }
1390
1391
                // these types are converted to strings from another PHP type on save
1392 94
            case 'serialized':
1393
                {
1394
                    // if the value is a string we assume it's already serialized data
1395 12
                    if (!is_string($value)) {
1396 1
                        $value = $this->fieldSetMapper->setSerializedValue($value); 
1397
                    }
1398 12
                    break;
1399
                }
1400 94
            case 'enum':
1401
                {
1402 94
                    $value = $this->fieldSetMapper->setEnumValue($fieldOptions['values'], $value);
1403 94
                    break;
1404
                }
1405 14
            case 'set':
1406 14
            case 'list':
1407
                {
1408 12
                    $value = $this->fieldSetMapper->setListValue($value, isset($fieldOptions['delimiter']) ? $fieldOptions['delimiter'] : null);
1409 12
                    $this->_convertedValues[$field] = $value;
1410 12
                    $forceDirty = true;
1411 12
                    break;
1412
                }
1413
        }
1414
1415 94
        if ($forceDirty || (empty($this->_record[$field]) && isset($value)) || ($this->_record[$field] !== $value)) {
1416 46
            $columnName = static::_cn($field);
1417 46
            if (isset($this->_record[$columnName])) {
1418 16
                $this->_originalValues[$field] = $this->_record[$columnName];
1419
            }
1420 46
            $this->_record[$columnName] = $value;
1421
            // only set value if this is an attribute mapped field
1422 46
            if (isset($this->_getAttributeFields()[$columnName])) {
1423 46
                $this->$columnName = $value;
1424
            }
1425 46
            $this->_isDirty = true;
1426
1427
            // unset invalidated relationships
1428 46
            if (!empty($fieldOptions['relationships']) && static::isRelational()) {
1429
                foreach ($fieldOptions['relationships'] as $relationship => $isCached) {
1430
                    if ($isCached) {
1431
                        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...
1432
                    }
1433
                }
1434
            }
1435 46
            return true;
1436
        } else {
1437 62
            return false;
1438
        }
1439
    }
1440
1441 22
    protected function _prepareRecordValues()
1442
    {
1443 22
        $record = [];
1444
1445 22
        foreach (static::$_classFields[get_called_class()] as $field => $options) {
1446 22
            $columnName = static::_cn($field);
1447
1448 22
            if (array_key_exists($columnName, $this->_record) || isset($this->$columnName)) {
1449 22
                $value = $this->_record[$columnName] ?? $this->$columnName;
1450
1451 22
                if (!$value && !empty($options['blankisnull'])) {
1452 22
                    $value = null;
1453
                }
1454 22
            } elseif (isset($options['default'])) {
1455 8
                $value = $options['default'];
1456
            } else {
1457 22
                continue;
1458
            }
1459
1460 22
            if (($options['type'] == 'date') && ($value == '0000-00-00') && !empty($options['blankisnull'])) {
1461
                $value = null;
1462
            }
1463 22
            if (($options['type'] == 'timestamp')) {
1464 22
                if (is_numeric($value)) {
1465 13
                    $value = date('Y-m-d H:i:s', $value);
1466 16
                } elseif ($value == null && !$options['notnull']) {
1467
                    $value = null;
1468
                }
1469
            }
1470
1471 22
            if (($options['type'] == 'serialized') && !is_string($value)) {
1472
                $value = serialize($value);
1473
            }
1474
1475 22
            if (($options['type'] == 'list') && is_array($value)) {
1476 11
                $delim = empty($options['delimiter']) ? ',' : $options['delimiter'];
1477 11
                $value = implode($delim, $value);
1478
            }
1479
1480 22
            $record[$field] = $value;
1481
        }
1482
1483 22
        return $record;
1484
    }
1485
1486 22
    protected static function _mapValuesToSet($recordValues)
1487
    {
1488 22
        $set = [];
1489
1490 22
        foreach ($recordValues as $field => $value) {
1491 22
            $fieldConfig = static::$_classFields[get_called_class()][$field];
1492
1493 22
            if ($value === null) {
1494 10
                $set[] = sprintf('`%s` = NULL', $fieldConfig['columnName']);
1495 22
            } elseif ($fieldConfig['type'] == 'timestamp' && $value == 'CURRENT_TIMESTAMP') {
1496
                $set[] = sprintf('`%s` = CURRENT_TIMESTAMP', $fieldConfig['columnName']);
1497 22
            } elseif ($fieldConfig['type'] == 'set' && is_array($value)) {
1498 11
                $set[] = sprintf('`%s` = "%s"', $fieldConfig['columnName'], DB::escape(join(',', $value)));
1499 22
            } elseif ($fieldConfig['type'] == 'boolean') {
1500 16
                $set[] = sprintf('`%s` = %u', $fieldConfig['columnName'], $value ? 1 : 0);
1501
            } else {
1502 22
                $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

1502
                $set[] = sprintf('`%s` = "%s"', $fieldConfig['columnName'], /** @scrutinizer ignore-type */ DB::escape($value));
Loading history...
1503
            }
1504
        }
1505
1506 22
        return $set;
1507
    }
1508
1509 16
    protected static function _mapFieldOrder($order)
1510
    {
1511 16
        if (is_string($order)) {
1512 4
            return [$order];
1513 14
        } elseif (is_array($order)) {
1514 14
            $r = [];
1515
1516 14
            foreach ($order as $key => $value) {
1517 14
                if (is_string($key)) {
1518 13
                    $columnName = static::_cn($key);
1519 13
                    $direction = strtoupper($value)=='DESC' ? 'DESC' : 'ASC';
1520
                } else {
1521 2
                    $columnName = static::_cn($value);
1522 1
                    $direction = 'ASC';
1523
                }
1524
1525 13
                $r[] = sprintf('`%s` %s', $columnName, $direction);
1526
            }
1527
1528 13
            return $r;
1529
        }
1530
    }
1531
1532 69
    protected static function _mapConditions($conditions)
1533
    {
1534 69
        foreach ($conditions as $field => &$condition) {
1535 69
            if (is_string($field)) {
1536 67
                if (isset(static::$_classFields[get_called_class()][$field])) {
1537 66
                    $fieldOptions = static::$_classFields[get_called_class()][$field];
1538
                }
1539
1540 67
                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...
1541 1
                    $condition = sprintf('`%s` IS NULL', static::_cn($field));
1542 67
                } elseif (is_array($condition)) {
1543 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

1543
                    $condition = sprintf('`%s` %s "%s"', static::_cn($field), $condition['operator'], /** @scrutinizer ignore-type */ DB::escape($condition['value']));
Loading history...
1544
                } else {
1545 67
                    $condition = sprintf('`%s` = "%s"', static::_cn($field), DB::escape($condition));
1546
                }
1547
            }
1548
        }
1549
1550 69
        return $conditions;
1551
    }
1552
1553 19
    protected function finishValidation()
1554
    {
1555 19
        $this->_isValid = $this->_isValid && !$this->_validator->hasErrors();
1556
1557 19
        if (!$this->_isValid) {
1558
            $this->_validationErrors = array_merge($this->_validationErrors, $this->_validator->getErrors());
1559
        }
1560
1561 19
        return $this->_isValid;
1562
    }
1563
}
1564