Passed
Branch release (bd7374)
by Henry
03:48
created

ActiveRecord::addValidationErrors()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2

Importance

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

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

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

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

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

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

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

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

776
                DB::nonQuery((new Insert())->setTable(static::/** @scrutinizer ignore-call */ getHistoryTable())->set($set), null, [static::class,'handleException']);
Loading history...
777
            }
778
        }
779
780 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

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

1005
                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

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

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

1524
                $set[] = sprintf('`%s` = "%s"', $fieldConfig['columnName'], /** @scrutinizer ignore-type */ DB::escape($value));
Loading history...
1525
            }
1526
        }
1527
1528 22
        return $set;
1529
    }
1530
1531 16
    protected static function _mapFieldOrder($order)
1532
    {
1533 16
        if (is_string($order)) {
1534 4
            return [$order];
1535 14
        } elseif (is_array($order)) {
1536 14
            $r = [];
1537
1538 14
            foreach ($order as $key => $value) {
1539 14
                if (is_string($key)) {
1540 13
                    $columnName = static::_cn($key);
1541 13
                    $direction = strtoupper($value)=='DESC' ? 'DESC' : 'ASC';
1542
                } else {
1543 2
                    $columnName = static::_cn($value);
1544 1
                    $direction = 'ASC';
1545
                }
1546
1547 13
                $r[] = sprintf('`%s` %s', $columnName, $direction);
1548
            }
1549
1550 13
            return $r;
1551
        }
1552
    }
1553
1554 69
    protected static function _mapConditions($conditions)
1555
    {
1556 69
        foreach ($conditions as $field => &$condition) {
1557 69
            if (is_string($field)) {
1558 67
                if (isset(static::$_classFields[get_called_class()][$field])) {
1559 66
                    $fieldOptions = static::$_classFields[get_called_class()][$field];
1560
                }
1561
1562 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...
1563 1
                    $condition = sprintf('`%s` IS NULL', static::_cn($field));
1564 67
                } elseif (is_array($condition)) {
1565 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

1565
                    $condition = sprintf('`%s` %s "%s"', static::_cn($field), $condition['operator'], /** @scrutinizer ignore-type */ DB::escape($condition['value']));
Loading history...
1566
                } else {
1567 67
                    $condition = sprintf('`%s` = "%s"', static::_cn($field), DB::escape($condition));
1568
                }
1569
            }
1570
        }
1571
1572 69
        return $conditions;
1573
    }
1574
1575 19
    protected function finishValidation()
1576
    {
1577 19
        $this->_isValid = $this->_isValid && !$this->_validator->hasErrors();
1578
1579 19
        if (!$this->_isValid) {
1580
            $this->_validationErrors = array_merge($this->_validationErrors, $this->_validator->getErrors());
1581
        }
1582
1583 19
        return $this->_isValid;
1584
    }
1585
}
1586