Passed
Push — release ( e4b72f...07d039 )
by Henry
09:15 queued 06:21
created

src/Models/ActiveRecord.php (1 issue)

Labels
Severity
1
<?php
2
/**
3
 * This file is part of the Divergence package.
4
 *
5
 * (c) Henry Paradiz <[email protected]>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
11
namespace Divergence\Models;
12
13
use Exception;
14
use ReflectionClass;
15
use JsonSerializable;
16
use Divergence\IO\Database\SQL;
17
use Divergence\Models\Mapping\Column;
18
use Divergence\Models\RecordValidator;
19
use Divergence\IO\Database\MySQL as DB;
20
use Divergence\Models\Mapping\Relation;
21
use Divergence\IO\Database\Query\Delete;
22
use Divergence\IO\Database\Query\Insert;
23
use Divergence\IO\Database\Query\Update;
24
use Divergence\Models\Interfaces\FieldSetMapper;
25
use Divergence\Models\SetMappers\DefaultSetMapper;
26
27
/**
28
 * ActiveRecord
29
 *
30
 * @package Divergence
31
 * @author  Henry Paradiz <[email protected]>
32
 * @author  Chris Alfano <[email protected]>
33
 *
34
 * @property-read bool $isDirty      False by default. Set to true only when an object has had any field change from it's state when it was instantiated.
35
 * @property-read bool $isPhantom    True if this object was instantiated as a brand new object and isn't yet saved.
36
 * @property-read bool $wasPhantom   True if this object was originally instantiated as a brand new object. Will stay true even if saved during that PHP runtime.
37
 * @property-read bool $isValid      True if this object is valid. This value is true by default and will only be set to false if the validator is executed first and finds a validation problem.
38
 * @property-read bool $isNew        False by default. Set to true only when an object that isPhantom is saved.
39
 * @property-read bool $isUpdated    False by default. Set to true when an object that already existed in the data store is saved.
40
 *
41
 * @property int        $ID Default primary key field. Part of Divergence\Models\Model but used in this file as a default.
42
 * @property string     $Class Name of this fully qualified PHP class for use with subclassing to explicitly specify which class to instantiate a record as when pulling from datastore. Part of Divergence\Models\Model but used in this file as a default.
43
 * @property mixed      $Created Timestamp of when this record was created. Supports Unix timestamp as well as any format accepted by PHP's strtotime as well as MySQL standard. Part of Divergence\Models\Model but used in this file as a default.
44
 * @property int        $CreatorID A standard user ID field for use by your login & authentication system. Part of Divergence\Models\Model but used in this file as a default.
45
 *
46
 * @property-read array $validationErrors    An empty string by default. Returns validation errors as an array.
47
 * @property-read array $data                A plain PHP array of the fields and values for this model object.
48
 * @property-read array $originalValues      A plain PHP array of the fields and values for this model object when it was instantiated.
49
 *
50
 * @property array $versioningFields
51
 * @property-read array $_relatedObjects Relationship cache
52
 *
53
 * @method static void _defineRelationships()
54
 * @method static void _initRelationships()
55
 * @method static bool _relationshipExists(string $value)
56
 * @method static array<ActiveRecord>|ActiveRecord|null _getRelationshipValue(string $value)
57
 * @method void beforeVersionedSave()
58
 * @method void afterVersionedSave()
59
 * @method static string getHistoryTable()
60
 */
61
class ActiveRecord implements JsonSerializable
62
{
63
    /**
64
     * @var bool $autoCreateTables Set this to true if you want the table(s) to automatically be created when not found.
65
     */
66
    public static $autoCreateTables = true;
67
68
    /**
69
     * @var string $tableName Name of table
70
     */
71
    public static $tableName = 'records';
72
73
    /**
74
     *
75
     * @var string $singularNoun Noun to describe singular object
76
     */
77
    public static $singularNoun = 'record';
78
79
    /**
80
     *
81
     * @var string $pluralNoun Noun to describe a plurality of objects
82
     */
83
    public static $pluralNoun = 'records';
84
85
    /**
86
     *
87
     * @var array $fieldDefaults Defaults values for field definitions
88
     */
89
    public static $fieldDefaults = [
90
        'type' => 'string',
91
        'notnull' => true,
92
    ];
93
94
    /**
95
     * @var array $fields Field definitions
96
     */
97
    public static $fields = [];
98
99
    /**
100
     * @var array $indexes Index definitions
101
     */
102
    public static $indexes = [];
103
104
105
    /**
106
    * @var array $validators Validation checks
107
    */
108
    public static $validators = [];
109
110
    /**
111
     * @var array $relationships Relationship definitions
112
     */
113
    public static $relationships = [];
114
115
116
    /**
117
     * Class names of possible contexts
118
     * @var array
119
     */
120
    public static $contextClasses;
121
122
    /**
123
     *  @var string|null $primaryKey The primary key for this model. Optional. Defaults to ID
124
     */
125
    public static $primaryKey = null;
126
127
    /**
128
     *  @var string $handleField Field which should be treated as unique but generated automatically.
129
     */
130
    public static $handleField = 'Handle';
131
132
    /**
133
     *  @var null|string $rootClass The root class.
134
     */
135
    public static $rootClass = null;
136
137
    /**
138
     *  @var null|string $defaultClass The default class to be used when creating a new object.
139
     */
140
    public static $defaultClass = null;
141
142
    /**
143
     *  @var array $subClasses Array of class names providing valid classes you can use for this model.
144
     */
145
    public static $subClasses = [];
146
147
    /**
148
     *  @var callable $beforeSave Runs inside the save() method before save actually happens.
149
     */
150
    public static $beforeSave;
151
152
    /**
153
     *  @var callable $afterSave Runs inside the save() method after save if no exception was thrown.
154
     */
155
    public static $afterSave;
156
157
158
    // versioning
159
    public static $historyTable;
160
    public static $createRevisionOnDestroy = true;
161
    public static $createRevisionOnSave = true;
162
163
    /**
164
     * Internal registry of fields that comprise this class. The setting of this variable of every parent derived from a child model will get merged.
165
     *
166
     * @var array $_classFields
167
     */
168
    protected static $_classFields = [];
169
170
    /**
171
     * Internal registry of relationships that are part of this class. The setting of this variable of every parent derived from a child model will get merged.
172
     *
173
     * @var array $_classFields
174
     */
175
    protected static $_classRelationships = [];
176
177
    /**
178
     * Internal registry of before save PHP callables that are part of this class. The setting of this variable of every parent derived from a child model will get merged.
179
     *
180
     * @var array $_classBeforeSave
181
     */
182
    protected static $_classBeforeSave = [];
183
184
    /**
185
     * Internal registry of after save PHP callables that are part of this class. The setting of this variable of every parent derived from a child model will get merged.
186
     *
187
     * @var array $_classAfterSave
188
     */
189
    protected static $_classAfterSave = [];
190
191
    /**
192
     * Global registry of booleans that check if a given model has had it's fields defined in a static context. The key is a class name and the value is a simple true / false.
193
     *
194
     * @used-by ActiveRecord::init()
195
     *
196
     * @var array $_fieldsDefined
197
     */
198
    protected static $_fieldsDefined = [];
199
200
    /**
201
     * Global registry of booleans that check if a given model has had it's relationships defined in a static context. The key is a class name and the value is a simple true / false.
202
     *
203
     * @used-by ActiveRecord::init()
204
     *
205
     * @var array $_relationshipsDefined
206
     */
207
    protected static $_relationshipsDefined = [];
208
    protected static $_eventsDefined = [];
209
210
    /**
211
     * @var array $_record Raw array data for this model.
212
     */
213
    protected $_record;
214
215
    /**
216
     * @var array $_convertedValues Raw array data for this model of data normalized for it's field type.
217
     */
218
    protected $_convertedValues;
219
220
    /**
221
     * @var RecordValidator $_validator Instance of a RecordValidator object.
222
     */
223
    protected $_validator;
224
225
    /**
226
     * @var array $_validationErrors Array of validation errors if there are any.
227
     */
228
    protected $_validationErrors;
229
230
    /**
231
     * @var array $_originalValues If any values have been changed the initial value is stored here.
232
     */
233
    protected $_originalValues;
234
235
236
    /**
237
     * False by default. Set to true only when an object has had any field change from it's state when it was instantiated.
238
     *
239
     * @var bool $_isDirty
240
     *
241
     * @used-by $this->save()
242
     * @used-by $this->__get()
243
     */
244
    protected $_isDirty;
245
246
    /**
247
     * True if this object was instantiated as a brand new object and isn't yet saved.
248
     *
249
     * @var bool $_isPhantom
250
     *
251
     * @used-by $this->save()
252
     * @used-by $this->__get()
253
     */
254
    protected $_isPhantom;
255
256
    /**
257
     * True if this object was originally instantiated as a brand new object. Will stay true even if saved during that PHP runtime.
258
     *
259
     * @var bool $_wasPhantom
260
     *
261
     * @used-by $this->__get()
262
     */
263
    protected $_wasPhantom;
264
265
    /**
266
     * True if this object is valid. This value is true by default and will only be set to false if the validator is executed first and finds a validation problem.
267
     *
268
     * @var bool $_isValid
269
     *
270
     * @used-by $this->__get()
271
     */
272
    protected $_isValid;
273
274
    /**
275
     * False by default. Set to true only when an object that isPhantom is saved.
276
     *
277
     * @var bool $_isNew
278
     *
279
     * @used-by $this->save()
280
     * @used-by $this->__get()
281
     */
282
    protected $_isNew;
283
284
    /**
285
     * False by default. Set to true when an object that already existed in the data store is saved.
286
     *
287
     * @var bool $_isUpdated
288
     *
289
     * @used-by $this->__get()
290
     */
291
    protected $_isUpdated;
292
293
    /** Field Mapper */
294
    protected ?FieldSetMapper $fieldSetMapper;
295
296
    /**
297
     * __construct Instantiates a Model and returns.
298
     *
299
     * @param array $record Raw array data to start off the model.
300
     * @param boolean $isDirty Whether or not to treat this object as if it was modified from the start.
301
     * @param boolean $isPhantom Whether or not to treat this object as a brand new record not yet in the database.
302
     *
303
     * @uses static::init
304
     *
305
     * @return static Instance of the value of $this->Class
306
     */
307 109
    public function __construct($record = [], $isDirty = false, $isPhantom = null)
308
    {
309 109
        $this->_record = $record;
310 109
        $this->_isPhantom = isset($isPhantom) ? $isPhantom : empty($record);
311 109
        $this->_wasPhantom = $this->_isPhantom;
312 109
        $this->_isDirty = $this->_isPhantom || $isDirty;
313 109
        $this->_isNew = false;
314 109
        $this->_isUpdated = false;
315
316 109
        $this->_isValid = true;
317 109
        $this->_validationErrors = [];
318 109
        $this->_originalValues = [];
319
320 109
        static::init();
321
322
        // set Class
323 109
        if (static::fieldExists('Class') && !$this->Class) {
324 109
            $this->_setFieldValue('Class', get_class($this));
325
        }
326
    }
327
328
    /**
329
     * __get Passthru to getValue($name)
330
     *
331
     * @param string $name Name of the magic field you want.
332
     *
333
     * @return mixed The return of $this->getValue($name)
334
     */
335 82
    public function __get($name)
336
    {
337 82
        return $this->getValue($name);
338
    }
339
340
    /**
341
     * Passthru to setValue($name,$value)
342
     *
343
     * @param string $name Name of the magic field to set.
344
     * @param mixed $value Value to set.
345
     *
346
     * @return mixed The return of $this->setValue($name,$value)
347
     */
348 4
    public function __set($name, $value)
349
    {
350 4
        return $this->setValue($name, $value);
351
    }
352
353
    /**
354
     * Tests if a magic class attribute is set or not.
355
     *
356
     * @param string $name Name of the magic field to set.
357
     *
358
     * @return bool Returns true if a value was returned by $this->getValue($name), false otherwise.
359
     */
360 47
    public function __isset($name)
361
    {
362 47
        $value = $this->getValue($name);
363 47
        return isset($value);
364
    }
365
366
    /**
367
     * Gets the primary key field for his model.
368
     *
369
     * @return string ID by default or static::$primaryKey if it's set.
370
     */
371 28
    public static function getPrimaryKey()
372
    {
373 28
        return isset(static::$primaryKey) ? static::$primaryKey : 'ID';
374
    }
375
376
    /**
377
     * Gets the primary key value for his model.
378
     *
379
     * @return mixed The primary key value for this object.
380
     */
381 17
    public function getPrimaryKeyValue()
382
    {
383 17
        if (isset(static::$primaryKey)) {
384 2
            return $this->{static::$primaryKey} ?? $this->_getFieldValue(static::$primaryKey);
385
        } else {
386 17
            return $this->_getFieldValue('ID');
387
        }
388
    }
389
390
    /**
391
     * init Initializes the model by checking the ancestor tree for the existence of various config fields and merges them.
392
     *
393
     * @uses static::$_fieldsDefined Sets static::$_fieldsDefined[get_called_class()] to true after running.
394
     * @uses static::$_relationshipsDefined Sets static::$_relationshipsDefined[get_called_class()] to true after running.
395
     * @uses static::$_eventsDefined Sets static::$_eventsDefined[get_called_class()] to true after running.
396
     *
397
     * @used-by static::__construct()
398
     * @used-by static::fieldExists()
399
     * @used-by static::getClassFields()
400
     * @used-by static::getColumnName()
401
     *
402
     * @return void
403
     */
404 125
    public static function init()
405
    {
406 125
        $className = get_called_class();
407 125
        if (empty(static::$_fieldsDefined[$className])) {
408 7
            static::_defineFields();
409 7
            static::_initFields();
410
411 7
            static::$_fieldsDefined[$className] = true;
412
        }
413 125
        if (empty(static::$_relationshipsDefined[$className]) && static::isRelational()) {
414 3
            static::_defineRelationships();
415 3
            static::_initRelationships();
416
417 3
            static::$_relationshipsDefined[$className] = true;
418
        }
419
420 125
        if (empty(static::$_eventsDefined[$className])) {
421 7
            static::_defineEvents();
422
423 7
            static::$_eventsDefined[$className] = true;
424
        }
425
    }
426
427
    /**
428
     * getValue Pass thru for __get
429
     *
430
     * @param string $name The name of the field you want to get.
431
     *
432
     * @return mixed Value of the field you wanted if it exists or null otherwise.
433
     */
434 93
    public function getValue($name)
435
    {
436
        switch ($name) {
437 93
            case 'isDirty':
438 23
                return $this->_isDirty;
439
440 93
            case 'isPhantom':
441 24
                return $this->_isPhantom;
442
443 93
            case 'wasPhantom':
444 3
                return $this->_wasPhantom;
445
446 93
            case 'isValid':
447 3
                return $this->_isValid;
448
449 93
            case 'isNew':
450 3
                return $this->_isNew;
451
452 93
            case 'isUpdated':
453 3
                return $this->_isUpdated;
454
455 93
            case 'validationErrors':
456 38
                return array_filter($this->_validationErrors);
457
458 91
            case 'data':
459 25
                return $this->getData();
460
461 79
            case 'originalValues':
462 3
                return $this->_originalValues;
463
464
            default:
465
                {
466
                    // handle field
467 79
                    if (static::fieldExists($name)) {
468 48
                        return $this->_getFieldValue($name);
469
                    }
470
                    // handle relationship
471 52
                    elseif (static::isRelational()) {
472 13
                        if (static::_relationshipExists($name)) {
473 13
                            return $this->_getRelationshipValue($name);
474
                        }
475
                    }
476
                    // default Handle to ID if not caught by fieldExists
477 39
                    elseif ($name == static::$handleField) {
478 1
                        return $this->_getFieldValue('ID');
479
                    }
480
                }
481
        }
482
        // undefined
483 47
        return null;
484
    }
485
486
    /**
487
     * Sets a value on this model.
488
     *
489
     * @param string $name
490
     * @param mixed $value
491
     * @return void|false False if the field does not exist. Void otherwise.
492
     */
493 7
    public function setValue($name, $value)
494
    {
495
        // handle field
496 7
        if (static::fieldExists($name)) {
497 7
            $this->_setFieldValue($name, $value);
498
        }
499
        // undefined
500
        else {
501 1
            return false;
502
        }
503
    }
504
505
    /**
506
     * Checks if this model is versioned.
507
     *
508
     * @return boolean Returns true if this class is defined with Divergence\Models\Versioning as a trait.
509
     */
510 112
    public static function isVersioned()
511
    {
512 112
        return in_array('Divergence\\Models\\Versioning', class_uses(get_called_class()));
513
    }
514
515
    /**
516
     * Checks if this model is ready for relationships.
517
     *
518
     * @return boolean Returns true if this class is defined with Divergence\Models\Relations as a trait.
519
     */
520 126
    public static function isRelational()
521
    {
522 126
        return in_array('Divergence\\Models\\Relations', class_uses(get_called_class()));
523
    }
524
525
    /**
526
     * Create a new object from this model.
527
     *
528
     * @param array $values Array of keys as fields and values.
529
     * @param boolean $save If the object should be immediately saved to database before being returned.
530
     * @return static An object of this model.
531
     */
532 26
    public static function create($values = [], $save = false)
533
    {
534 26
        $className = get_called_class();
535
536
        // create class
537
        /** @var ActiveRecord */
538 26
        $ActiveRecord = new $className();
539 26
        $ActiveRecord->setFields($values);
540
541 26
        if ($save) {
542 4
            $ActiveRecord->save();
543
        }
544
545 26
        return $ActiveRecord;
546
    }
547
548
    /**
549
     * Checks if a model is a certain class.
550
     * @param string $class Check if the model matches this class.
551
     * @return boolean True if model matches the class provided. False otherwise.
552
     */
553 1
    public function isA($class): bool
554
    {
555 1
        return is_a($this, $class);
556
    }
557
558
    /**
559
     * Used to instantiate a new model of a different class with this model's field's. Useful when you have similar classes or subclasses with the same parent.
560
     *
561
     * @param string $className If you leave this blank the return will be $this
562
     * @param array $fieldValues Optional. Any field values you want to override.
563
     * @return static A new model of a different class with this model's field's. Useful when you have similar classes or subclasses with the same parent.
564
     */
565 2
    public function changeClass($className = false, $fieldValues = false)
566
    {
567 2
        if (!$className) {
568 1
            return $this;
569
        }
570
571 2
        $this->_record[static::_cn('Class')] = $className;
572 2
        $ActiveRecord = new $className($this->_record, true, $this->isPhantom);
573
574 2
        if ($fieldValues) {
575 1
            $ActiveRecord->setFields($fieldValues);
576
        }
577
578 2
        if (!$this->isPhantom) {
579
            $ActiveRecord->save();
580
        }
581
582 2
        return $ActiveRecord;
583
    }
584
585
    /**
586
     * Change multiple fields in the model with an array.
587
     *
588
     * @param array $values Field/values array to change multiple fields in this model.
589
     * @return void
590
     */
591 37
    public function setFields($values)
592
    {
593 37
        foreach ($values as $field => $value) {
594 34
            $this->_setFieldValue($field, $value);
595
        }
596
    }
597
598
    /**
599
     * Change one field in the model.
600
     *
601
     * @param string $field
602
     * @param mixed $value
603
     * @return void
604
     */
605 1
    public function setField($field, $value)
606
    {
607 1
        $this->_setFieldValue($field, $value);
608
    }
609
610
    /**
611
     * Implements JsonSerializable for this class.
612
     *
613
     * @return array Return for extension JsonSerializable
614
     */
615 6
    public function jsonSerialize(): array
616
    {
617 6
        return $this->getData();
618
    }
619
620
    /**
621
     *  Gets normalized object data.
622
     *
623
     *  @return array The model's data as a normal array with any validation errors included.
624
     */
625 38
    public function getData(): array
626
    {
627 38
        $data = [];
628
629 38
        foreach (static::$_classFields[get_called_class()] as $field => $options) {
630 38
            $data[$field] = $this->_getFieldValue($field);
631
        }
632
633 38
        if ($this->validationErrors) {
634 1
            $data['validationErrors'] = $this->validationErrors;
635
        }
636
637 38
        return $data;
638
    }
639
640
    /**
641
     * Checks if a field has been changed from it's value when this object was created.
642
     *
643
     * @param string $field
644
     * @return boolean
645
     */
646 1
    public function isFieldDirty($field): bool
647
    {
648 1
        return $this->isPhantom || array_key_exists($field, $this->_originalValues);
649
    }
650
651
    /**
652
     * Gets values that this model was instantiated with for a given field.
653
     *
654
     * @param string $field Field name
655
     * @return mixed
656
     */
657 1
    public function getOriginalValue($field)
658
    {
659 1
        return $this->_originalValues[$field];
660
    }
661
662
    /**
663
     * Fires a DB::clearCachedRecord a key static::$tableName.'/'.static::getPrimaryKey()
664
     *
665
     * @return void
666
     */
667 20
    public function clearCaches()
668
    {
669 20
        foreach ($this->getClassFields() as $field => $options) {
670 20
            if (!empty($options['unique']) || !empty($options['primary'])) {
671 20
                $key = sprintf('%s/%s', static::$tableName, $field);
672 20
                DB::clearCachedRecord($key);
673
            }
674
        }
675
    }
676
677
    /**
678
     * Runs the before save event function one at a time for any class that had $beforeSave configured in the ancestor tree.
679
     */
680 21
    public function beforeSave()
681
    {
682 21
        foreach (static::$_classBeforeSave as $beforeSave) {
683 1
            if (is_callable($beforeSave)) {
684 1
                $beforeSave($this);
685
            }
686
        }
687
    }
688
689
    /**
690
     * Runs the after save event function one at a time for any class that had $beforeSave configured in the ancestor tree. Will only fire if save was successful.
691
     */
692 19
    public function afterSave()
693
    {
694 19
        foreach (static::$_classAfterSave as $afterSave) {
695 1
            if (is_callable($afterSave)) {
696 1
                $afterSave($this);
697
            }
698
        }
699
    }
700
701
    /**
702
     * Saves this object to the database currently in use.
703
     *
704
     * @param bool $deep Default is true. When true will try to save any dirty models in any defined and initialized relationships.
705
     *
706
     * @uses $this->_isPhantom
707
     * @uses $this->_isDirty
708
     */
709 20
    public function save($deep = true)
710
    {
711
        // run before save
712 20
        $this->beforeSave();
713
714 20
        if (static::isVersioned()) {
715 13
            $this->beforeVersionedSave();
716
        }
717
718
        // set created
719 20
        if (static::fieldExists('Created') && (!$this->Created || ($this->Created == 'CURRENT_TIMESTAMP'))) {
720 8
            $this->Created = $this->_record['Created'] = time();
721 8
            unset($this->_convertedValues['Created']);
722
        }
723
724
        // validate
725 20
        if (!$this->validate($deep)) {
726
            throw new Exception('Cannot save invalid record');
727
        }
728
729 20
        $this->clearCaches();
730
731 20
        if ($this->isDirty) {
732
            // prepare record values
733 19
            $recordValues = $this->_prepareRecordValues();
734
735
            // transform record to set array
736 19
            $set = static::_mapValuesToSet($recordValues);
737
738
            // create new or update existing
739 19
            if ($this->_isPhantom) {
740 15
                DB::nonQuery((new Insert())->setTable(static::$tableName)->set($set), null, [static::class,'handleException']);
741 14
                $primaryKey = $this->getPrimaryKey();
742 14
                $insertID = DB::insertID();
743 14
                $fields = static::getClassFields();
744 14
                if (($fields[$primaryKey]['type'] ?? false) === 'integer') {
745 14
                    $insertID = intval($insertID);
746
                }
747 14
                $this->_record[$primaryKey] = $insertID;
748 14
                $this->$primaryKey = $insertID;
749 14
                $this->_isPhantom = false;
750 14
                $this->_isNew = true;
751 5
            } elseif (count($set)) {
752 5
                DB::nonQuery((new Update())->setTable(static::$tableName)->set($set)->where(
753 5
                    sprintf('`%s` = %u', static::_cn($this->getPrimaryKey()), (string)$this->getPrimaryKeyValue())
754 5
                ), null, [static::class,'handleException']);
755
756 4
                $this->_isUpdated = true;
757
            }
758
759
            // update state
760 17
            $this->_isDirty = false;
761 17
            if (static::isVersioned()) {
762 10
                $this->afterVersionedSave();
763
            }
764
        }
765 18
        $this->afterSave();
766
    }
767
768
769
    /**
770
     * Deletes this object.
771
     *
772
     * @return bool True if database returns number of affected rows above 0. False otherwise.
773
     */
774 9
    public function destroy(): bool
775
    {
776 9
        if (static::isVersioned()) {
777 5
            if (static::$createRevisionOnDestroy) {
778
                // save a copy to history table
779 5
                if ($this->fieldExists('Created')) {
780 5
                    $this->Created = time();
781
                }
782
783 5
                $recordValues = $this->_prepareRecordValues();
784 5
                $set = static::_mapValuesToSet($recordValues);
785
786 5
                DB::nonQuery((new Insert())->setTable(static::getHistoryTable())->set($set), null, [static::class,'handleException']);
787
            }
788
        }
789
790 8
        return static::delete((string)$this->getPrimaryKeyValue());
791
    }
792
793
    /**
794
     * Delete by ID
795
     *
796
     * @param int|string $id
797
     * @return bool True if database returns number of affected rows above 0. False otherwise.
798
     */
799 8
    public static function delete($id): bool
800
    {
801 8
        DB::nonQuery((new Delete())->setTable(static::$tableName)->where(sprintf('`%s` = %u', static::_cn(static::$primaryKey ? static::$primaryKey : 'ID'), $id)), null, [static::class,'handleException']);
802
803 8
        return DB::affectedRows() > 0;
804
    }
805
806
    /**
807
     * Checks of a field exists for this model in the fields config.
808
     *
809
     * @param string $field Name of the field
810
     * @return bool True if the field exists. False otherwise.
811
     */
812 125
    public static function fieldExists($field): bool
813
    {
814 125
        static::init();
815 125
        return array_key_exists($field, static::$_classFields[get_called_class()]);
816
    }
817
818
    /**
819
     * Returns the current configuration of class fields for the called class.
820
     *
821
     * @return array Current configuration of class fields for the called class.
822
     */
823 21
    public static function getClassFields(): array
824
    {
825 21
        static::init();
826 21
        return static::$_classFields[get_called_class()];
827
    }
828
829
    /**
830
     * Returns either a field option or an array of all the field options.
831
     *
832
     * @param string $field Name of the field.
833
     * @param boolean $optionKey
834
     * @return array|mixed
835
     */
836 1
    public static function getFieldOptions($field, $optionKey = false)
837
    {
838 1
        if ($optionKey) {
839 1
            return static::$_classFields[get_called_class()][$field][$optionKey];
840
        } else {
841 1
            return static::$_classFields[get_called_class()][$field];
842
        }
843
    }
844
845
    /**
846
     * Returns columnName for given field
847
     * @param string $field name of field
848
     * @return string column name
849
     */
850 122
    public static function getColumnName($field)
851
    {
852 122
        static::init();
853 122
        if (!static::fieldExists($field)) {
854 2
            throw new Exception('getColumnName called on nonexisting column: ' . get_called_class().'->'.$field);
855
        }
856
857 121
        return static::$_classFields[get_called_class()][$field]['columnName'];
858
    }
859
860 2
    public static function mapFieldOrder($order)
861
    {
862 2
        return static::_mapFieldOrder($order);
863
    }
864
865 1
    public static function mapConditions($conditions)
866
    {
867 1
        return static::_mapConditions($conditions);
868
    }
869
870
    /**
871
     * Returns static::$rootClass for the called class.
872
     *
873
     * @return string static::$rootClass for the called class.
874
     */
875 1
    public function getRootClass(): string
876
    {
877 1
        return static::$rootClass;
878
    }
879
880
    /**
881
     * Sets an array of validation errors for this object.
882
     *
883
     * @param array $array Validation errors in the form Field Name => error message
884
     * @return void
885
     */
886 2
    public function addValidationErrors($array)
887
    {
888 2
        foreach ($array as $field => $errorMessage) {
889 2
            $this->addValidationError($field, $errorMessage);
890
        }
891
    }
892
893
    /**
894
     * Sets a validation error for this object. Sets $this->_isValid to false.
895
     *
896
     * @param string $field
897
     * @param string $errorMessage
898
     * @return void
899
     */
900 2
    public function addValidationError($field, $errorMessage)
901
    {
902 2
        $this->_isValid = false;
903 2
        $this->_validationErrors[$field] = $errorMessage;
904
    }
905
906
    /**
907
     * Get a validation error for a given field.
908
     *
909
     * @param string $field Name of the field.
910
     * @return string|null A validation error for the field. Null is no validation error found.
911
     */
912 1
    public function getValidationError($field)
913
    {
914
        // break apart path
915 1
        $crumbs = explode('.', $field);
916
917
        // resolve path recursively
918 1
        $cur = &$this->_validationErrors;
919 1
        while ($crumb = array_shift($crumbs)) {
920 1
            if (array_key_exists($crumb, $cur)) {
921 1
                $cur = &$cur[$crumb];
922
            } else {
923 1
                return null;
924
            }
925
        }
926
927
        // return current value
928 1
        return $cur;
929
    }
930
931
    /**
932
     * Validates the model. Instantiates a new RecordValidator object and sets it to $this->_validator.
933
     * Then validates against the set validators in this model. Returns $this->_isValid
934
     *
935
     * @param boolean $deep If true will attempt to validate any already loaded relationship members.
936
     * @return bool $this->_isValid which could be set to true or false depending on what happens with the RecordValidator.
937
     */
938 20
    public function validate($deep = true)
939
    {
940 20
        $this->_isValid = true;
941 20
        $this->_validationErrors = [];
942
943 20
        if (!isset($this->_validator)) {
944 20
            $this->_validator = new RecordValidator($this->_record);
945
        } else {
946 6
            $this->_validator->resetErrors();
947
        }
948
949 20
        foreach (static::$validators as $validator) {
950
            $this->_validator->validate($validator);
951
        }
952
953 20
        $this->finishValidation();
954
955 20
        if ($deep) {
956
            // validate relationship objects
957 20
            if (!empty(static::$_classRelationships[get_called_class()])) {
958 1
                foreach (static::$_classRelationships[get_called_class()] as $relationship => $options) {
959 1
                    if (empty($this->_relatedObjects[$relationship])) {
960 1
                        continue;
961
                    }
962
963
964 1
                    if ($options['type'] == 'one-one') {
965
                        if ($this->_relatedObjects[$relationship]->isDirty) {
966
                            $this->_relatedObjects[$relationship]->validate();
967
                            $this->_isValid = $this->_isValid && $this->_relatedObjects[$relationship]->isValid;
968
                            $this->_validationErrors[$relationship] = $this->_relatedObjects[$relationship]->validationErrors;
969
                        }
970 1
                    } elseif ($options['type'] == 'one-many') {
971 1
                        foreach ($this->_relatedObjects[$relationship] as $i => $object) {
972 1
                            if ($object->isDirty) {
973
                                $object->validate();
974
                                $this->_isValid = $this->_isValid && $object->isValid;
975
                                $this->_validationErrors[$relationship][$i] = $object->validationErrors;
976
                            }
977
                        }
978
                    }
979
                } // foreach
980
            } // if
981
        } // if ($deep)
982
983 20
        return $this->_isValid;
984
    }
985
986
    /**
987
     * Handle any errors that come from the database client in the process of running a query.
988
     * If the error code from MySQL 42S02 (table not found) is thrown this method will attempt to create the table before running the original query and returning.
989
     * Other errors will be routed through to DB::handleException
990
     *
991
     * @param Exception $exception
992
     * @param string $query
993
     * @param array $queryLog
994
     * @param array|string $parameters
995
     * @return mixed Retried query result or the return from DB::handleException
996
     */
997 7
    public static function handleException(\Exception $e, $query = null, $queryLog = null, $parameters = null)
998
    {
999 7
        $Connection = DB::getConnection();
1000 7
        if ($Connection->errorCode() == '42S02' && static::$autoCreateTables) {
1001 2
            $CreateTable = SQL::getCreateTable(static::$rootClass);
1002
1003
            // history versions table
1004 2
            if (static::isVersioned()) {
1005 1
                $CreateTable .= SQL::getCreateTable(static::$rootClass, true);
1006
            }
1007
1008 2
            $Statement = $Connection->query($CreateTable);
1009
1010
            // check for errors
1011 2
            $ErrorInfo = $Statement->errorInfo();
1012
1013
            // handle query error
1014 2
            if ($ErrorInfo[0] != '00000') {
1015
                self::handleException($query, $queryLog);
1016
            }
1017
1018
            // clear buffer (required for the next query to work without running fetchAll first
1019 2
            $Statement->closeCursor();
1020
1021 2
            return $Connection->query((string)$query); // now the query should finish with no error
1022
        } else {
1023 5
            return DB::handleException($e, $query, $queryLog);
1024
        }
1025
    }
1026
1027
    /**
1028
     * Iterates through all static::$beforeSave and static::$afterSave in this class and any of it's parent classes.
1029
     * Checks if they are callables and if they are adds them to static::$_classBeforeSave[] and static::$_classAfterSave[]
1030
     *
1031
     * @return void
1032
     *
1033
     * @uses static::$beforeSave
1034
     * @uses static::$afterSave
1035
     * @uses static::$_classBeforeSave
1036
     * @uses static::$_classAfterSave
1037
     */
1038 7
    protected static function _defineEvents()
1039
    {
1040
        // run before save
1041 7
        $className = get_called_class();
1042
1043
        // merge fields from first ancestor up
1044 7
        $classes = class_parents($className);
1045 7
        array_unshift($classes, $className);
1046
1047 7
        while ($class = array_pop($classes)) {
1048 7
            if (is_callable($class::$beforeSave)) {
1049
                if (!empty($class::$beforeSave)) {
1050
                    if (!in_array($class::$beforeSave, static::$_classBeforeSave)) {
1051
                        static::$_classBeforeSave[] = $class::$beforeSave;
1052
                    }
1053
                }
1054
            }
1055
1056 7
            if (is_callable($class::$afterSave)) {
1057
                if (!empty($class::$afterSave)) {
1058
                    if (!in_array($class::$afterSave, static::$_classAfterSave)) {
1059
                        static::$_classAfterSave[] = $class::$afterSave;
1060
                    }
1061
                }
1062
            }
1063
        }
1064
    }
1065
1066
    /**
1067
     * Merges all static::$_classFields in this class and any of it's parent classes.
1068
     * Sets the merged value to static::$_classFields[get_called_class()]
1069
     *
1070
     * @return void
1071
     *
1072
     * @uses static::$_classFields
1073
     * @uses static::$_classFields
1074
     */
1075 7
    protected static function _defineFields()
1076
    {
1077 7
        $className = get_called_class();
1078
1079
        // skip if fields already defined
1080 7
        if (isset(static::$_classFields[$className])) {
1081
            return;
1082
        }
1083
1084
        // merge fields from first ancestor up
1085 7
        $classes = class_parents($className);
1086 7
        array_unshift($classes, $className);
1087
1088 7
        static::$_classFields[$className] = [];
1089 7
        while ($class = array_pop($classes)) {
1090 7
            if (!empty($class::$fields)) {
1091
                static::$_classFields[$className] = array_merge(static::$_classFields[$className], $class::$fields);
1092
            }
1093 7
            $attributeFields = $class::_definedAttributeFields();
1094 7
            if (!empty($attributeFields['fields'])) {
1095 6
                static::$_classFields[$className] = array_merge(static::$_classFields[$className], $attributeFields['fields']);
1096
            }
1097 7
            if (!empty($attributeFields['relations'])) {
1098 1
                $class::$relationships = $attributeFields['relations'];
1099
            }
1100
        }
1101
    }
1102
1103
    /**
1104
     * This function grabs all protected fields on the model and uses that as the basis for what constitutes a mapped field
1105
     * It skips a certain list of protected fields that are built in for ORM operation
1106
     *
1107
     * @return array
1108
     */
1109 7
    public static function _definedAttributeFields(): array
1110
    {
1111 7
        $fields = [];
1112 7
        $relations = [];
1113 7
        $properties = (new ReflectionClass(static::class))->getProperties();
1114 7
        if (!empty($properties)) {
1115 7
            foreach ($properties as $property) {
1116 7
                if ($property->isProtected()) {
1117
1118
                    // skip these because they are built in
1119 7
                    if (in_array($property->getName(), [
1120 7
                        '_classFields','_classRelationships','_classBeforeSave','_classAfterSave','_fieldsDefined','_relationshipsDefined','_eventsDefined','_record','_validator'
1121 7
                        ,'_validationErrors','_isDirty','_isValid','fieldSetMapper','_convertedValues','_originalValues','_isPhantom','_wasPhantom','_isNew','_isUpdated','_relatedObjects'
1122 7
                    ])) {
1123 7
                        continue;
1124
                    }
1125
1126 6
                    $isRelationship = false;
1127
1128 6
                    if ($attributes = $property->getAttributes()) {
1129 6
                        foreach ($attributes as $attribute) {
1130 6
                            $attributeName = $attribute->getName();
1131 6
                            if ($attributeName === Column::class) {
1132 6
                                $fields[$property->getName()] = array_merge($attribute->getArguments(), ['attributeField'=>true]);
1133
                            }
1134
1135 6
                            if ($attributeName === Relation::class) {
1136 1
                                $isRelationship = true;
1137 1
                                $relations[$property->getName()] = $attribute->getArguments();
1138
                            }
1139
                        }
1140
                    } else {
1141
                        // default
1142 4
                        if (!$isRelationship) {
1143 4
                            $fields[$property->getName()] = [];
1144
                        }
1145
                    }
1146
                }
1147
            }
1148
        }
1149 7
        return [
1150 7
            'fields' => $fields,
1151 7
            'relations' => $relations
1152 7
        ];
1153
    }
1154
1155
1156
    /**
1157
     * Called after _defineFields to initialize and apply defaults to the fields property
1158
     * Must be idempotent as it may be applied multiple times up the inheritance chain
1159
     * @return void
1160
     *
1161
     * @uses static::$_classFields
1162
     */
1163 7
    protected static function _initFields()
1164
    {
1165 7
        $className = get_called_class();
1166 7
        $optionsMask = [
1167 7
            'type' => null,
1168 7
            'length' => null,
1169 7
            'primary' => null,
1170 7
            'unique' => null,
1171 7
            'autoincrement' => null,
1172 7
            'notnull' => null,
1173 7
            'unsigned' => null,
1174 7
            'default' => null,
1175 7
            'values' => null,
1176 7
        ];
1177
1178
        // apply default values to field definitions
1179 7
        if (!empty(static::$_classFields[$className])) {
1180 6
            $fields = [];
1181
1182 6
            foreach (static::$_classFields[$className] as $field => $options) {
1183 6
                if (is_string($field)) {
1184 6
                    if (is_array($options)) {
1185 6
                        $fields[$field] = array_merge($optionsMask, static::$fieldDefaults, ['columnName' => $field], $options);
1186
                    } elseif (is_string($options)) {
1187
                        $fields[$field] = array_merge($optionsMask, static::$fieldDefaults, ['columnName' => $field, 'type' => $options]);
1188
                    } elseif ($options == null) {
1189 6
                        continue;
1190
                    }
1191
                } elseif (is_string($options)) {
1192
                    $field = $options;
1193
                    $fields[$field] = array_merge($optionsMask, static::$fieldDefaults, ['columnName' => $field]);
1194
                }
1195
1196 6
                if ($field == 'Class') {
1197
                    // apply Class enum values
1198 6
                    $fields[$field]['values'] = static::$subClasses;
1199
                }
1200
1201 6
                if (!isset($fields[$field]['blankisnull']) && empty($fields[$field]['notnull'])) {
1202 6
                    $fields[$field]['blankisnull'] = true;
1203
                }
1204
1205 6
                if ($fields[$field]['autoincrement']) {
1206 6
                    $fields[$field]['primary'] = true;
1207
                }
1208
            }
1209
1210 6
            static::$_classFields[$className] = $fields;
1211
        }
1212
    }
1213
1214
1215
    /**
1216
     * Returns class name for instantiating given record
1217
     * @param array $record record
1218
     * @return string class name
1219
     */
1220 98
    protected static function _getRecordClass($record)
1221
    {
1222 98
        $static = get_called_class();
1223
1224 98
        if (!static::fieldExists('Class')) {
1225
            return $static;
1226
        }
1227
1228 98
        $columnName = static::_cn('Class');
1229
1230 98
        if (!empty($record[$columnName]) && is_subclass_of($record[$columnName], $static)) {
1231 19
            return $record[$columnName];
1232
        } else {
1233 79
            return $static;
1234
        }
1235
    }
1236
1237
    /**
1238
     * Shorthand alias for _getColumnName
1239
     * @param string $field name of field
1240
     * @return string column name
1241
     */
1242 121
    protected static function _cn($field)
1243
    {
1244 121
        return static::getColumnName($field);
1245
    }
1246
1247
1248
    /**
1249
     * Retrieves given field's value
1250
     * @param string $field Name of field
1251
     * @return mixed value
1252
     */
1253 81
    protected function _getFieldValue($field, $useDefault = true)
1254
    {
1255 81
        $fieldOptions = static::$_classFields[get_called_class()][$field];
1256
1257 81
        if (isset($this->_record[$fieldOptions['columnName']])) {
1258 81
            $value = $this->_record[$fieldOptions['columnName']];
1259
1260
            // apply type-dependent transformations
1261 81
            switch ($fieldOptions['type']) {
1262 81
                case 'password':
1263
                    {
1264 16
                        return $value;
1265
                    }
1266
1267 81
                case 'timestamp':
1268
                    {
1269 33
                        if (!isset($this->_convertedValues[$field])) {
1270 33
                            if ($value && is_string($value) && $value != '0000-00-00 00:00:00') {
1271 33
                                $this->_convertedValues[$field] = strtotime($value);
1272 4
                            } elseif (is_integer($value)) {
1273 4
                                $this->_convertedValues[$field] = $value;
1274
                            } else {
1275
                                unset($this->_convertedValues[$field]);
1276
                            }
1277
                        }
1278
1279 33
                        return $this->_convertedValues[$field];
1280
                    }
1281 81
                case 'serialized':
1282
                    {
1283 16
                        if (!isset($this->_convertedValues[$field])) {
1284 16
                            $this->_convertedValues[$field] = is_string($value) ? unserialize($value) : $value;
1285
                        }
1286
1287 16
                        return $this->_convertedValues[$field];
1288
                    }
1289 81
                case 'set':
1290 81
                case 'list':
1291
                    {
1292 16
                        if (!isset($this->_convertedValues[$field])) {
1293 8
                            $delim = empty($fieldOptions['delimiter']) ? ',' : $fieldOptions['delimiter'];
1294 8
                            $this->_convertedValues[$field] = array_filter(preg_split('/\s*'.$delim.'\s*/', $value));
1295
                        }
1296
1297 16
                        return $this->_convertedValues[$field];
1298
                    }
1299
1300 81
                case 'int':
1301 81
                case 'integer':
1302 66
                case 'uint':
1303 64
                    if (!isset($this->_convertedValues[$field])) {
1304 64
                        if (!$fieldOptions['notnull'] && is_null($value)) {
1305
                            $this->_convertedValues[$field] = $value;
1306
                        } else {
1307 64
                            $this->_convertedValues[$field] = intval($value);
1308
                        }
1309
                    }
1310 64
                    return $this->_convertedValues[$field];
1311
1312 66
                case 'boolean':
1313
                    {
1314 16
                        if (!isset($this->_convertedValues[$field])) {
1315 16
                            $this->_convertedValues[$field] = (bool)$value;
1316
                        }
1317
1318 16
                        return $this->_convertedValues[$field];
1319
                    }
1320
1321 66
                case 'decimal':
1322 19
                    if (!isset($this->_convertedValues[$field])) {
1323 19
                        if (!$fieldOptions['notnull'] && is_null($value)) {
1324
                            $this->_convertedValues[$field] = $value;
1325
                        } else {
1326 19
                            $this->_convertedValues[$field] = floatval($value);
1327
                        }
1328
                    }
1329 19
                    return $this->_convertedValues[$field];
1330
1331
                default:
1332
                    {
1333 66
                        return $value;
1334
                    }
1335
            }
1336 41
        } elseif ($useDefault && isset($fieldOptions['default'])) {
1337
            // return default
1338 16
            return $fieldOptions['default'];
1339
        } else {
1340 41
            switch ($fieldOptions['type']) {
1341 41
                case 'set':
1342 41
                case 'list':
1343
                    {
1344
                        return [];
1345
                    }
1346
                default:
1347
                    {
1348 41
                        return null;
1349
                    }
1350
            }
1351
        }
1352
    }
1353
1354
    /**
1355
     * Sets given field's value
1356
     * @param string $field Name of field
1357
     * @param mixed $value New value
1358
     * @return mixed value
1359
     */
1360 109
    protected function _setFieldValue($field, $value)
1361
    {
1362
        // ignore setting versioning fields
1363 109
        if (static::isVersioned()) {
1364 53
            if ($field === 'RevisionID') {
1365 1
                return false;
1366
            }
1367
        }
1368
1369 109
        if (!static::fieldExists($field)) {
1370
            return false;
1371
        }
1372 109
        $fieldOptions = static::$_classFields[get_called_class()][$field];
1373
1374
        // no overriding autoincrements
1375 109
        if ($fieldOptions['autoincrement']) {
1376 2
            return false;
1377
        }
1378
1379 109
        if (!isset($this->fieldSetMapper)) {
1380 109
            $this->fieldSetMapper = new DefaultSetMapper();
1381
        }
1382
1383
        // pre-process value
1384 109
        $forceDirty = false;
1385 109
        switch ($fieldOptions['type']) {
1386 109
            case 'clob':
1387 109
            case 'string':
1388
                {
1389 36
                    $value = $this->fieldSetMapper->setStringValue($value);
0 ignored issues
show
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

1389
                    /** @scrutinizer ignore-call */ 
1390
                    $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...
1390 36
                    if (!$fieldOptions['notnull'] && $fieldOptions['blankisnull'] && ($value === '' || $value === null)) {
1391 1
                        $value = null;
1392
                    }
1393 36
                    break;
1394
                }
1395
1396 109
            case 'boolean':
1397
                {
1398 12
                    $value = $this->fieldSetMapper->setBooleanValue($value);
1399 12
                    break;
1400
                }
1401
1402 109
            case 'decimal':
1403
                {
1404 15
                    $value = $this->fieldSetMapper->setDecimalValue($value);
1405 15
                    break;
1406
                }
1407
1408 109
            case 'int':
1409 109
            case 'uint':
1410 109
            case 'integer':
1411
                {
1412 15
                    $value = $this->fieldSetMapper->setIntegerValue($value);
1413 15
                    if (!$fieldOptions['notnull'] && ($value === '' || is_null($value))) {
1414 2
                        $value = null;
1415
                    }
1416 15
                    break;
1417
                }
1418
1419 109
            case 'timestamp':
1420
                {
1421 14
                    unset($this->_convertedValues[$field]);
1422 14
                    $value = $this->fieldSetMapper->setTimestampValue($value);
1423 14
                    break;
1424
                }
1425
1426 109
            case 'date':
1427
                {
1428 13
                    unset($this->_convertedValues[$field]);
1429 13
                    $value = $this->fieldSetMapper->setDateValue($value);
1430 13
                    break;
1431
                }
1432
1433
                // these types are converted to strings from another PHP type on save
1434 109
            case 'serialized':
1435
                {
1436
                    // if the value is a string we assume it's already serialized data
1437 12
                    if (!is_string($value)) {
1438 1
                        $value = $this->fieldSetMapper->setSerializedValue($value);
1439
                    }
1440 12
                    break;
1441
                }
1442 109
            case 'enum':
1443
                {
1444 109
                    $value = $this->fieldSetMapper->setEnumValue($fieldOptions['values'], $value);
1445 109
                    break;
1446
                }
1447 12
            case 'set':
1448 12
            case 'list':
1449
                {
1450 12
                    $value = $this->fieldSetMapper->setListValue($value, isset($fieldOptions['delimiter']) ? $fieldOptions['delimiter'] : null);
1451 12
                    $this->_convertedValues[$field] = $value;
1452 12
                    $forceDirty = true;
1453 12
                    break;
1454
                }
1455
        }
1456
1457 109
        if ($forceDirty || (empty($this->_record[$field]) && isset($value)) || ($this->_record[$field] !== $value)) {
1458 47
            $columnName = static::_cn($field);
1459 47
            if (isset($this->_record[$columnName])) {
1460 16
                $this->_originalValues[$field] = $this->_record[$columnName];
1461
            }
1462 47
            $this->_record[$columnName] = $value;
1463
            // only set value if this is an attribute mapped field
1464 47
            if (isset($this->_classFields[get_called_class()][$columnName]['attributeField'])) {
1465
                $this->$columnName = $value;
1466
            }
1467 47
            $this->_isDirty = true;
1468
1469
            // unset invalidated relationships
1470 47
            if (!empty($fieldOptions['relationships']) && static::isRelational()) {
1471
                foreach ($fieldOptions['relationships'] as $relationship => $isCached) {
1472
                    if ($isCached) {
1473
                        unset($this->_relatedObjects[$relationship]);
1474
                    }
1475
                }
1476
            }
1477 47
            return true;
1478
        } else {
1479 77
            return false;
1480
        }
1481
    }
1482
1483 23
    protected function _prepareRecordValues()
1484
    {
1485 23
        $record = [];
1486
1487 23
        foreach (static::$_classFields[get_called_class()] as $field => $options) {
1488 23
            $columnName = static::_cn($field);
1489
1490 23
            if (array_key_exists($columnName, $this->_record) || isset($this->$columnName)) {
1491 23
                $value = $this->_record[$columnName] ?? $this->$columnName;
1492
1493 23
                if (!$value && !empty($options['blankisnull'])) {
1494 23
                    $value = null;
1495
                }
1496 23
            } elseif (isset($options['default'])) {
1497 8
                $value = $options['default'];
1498
            } else {
1499 23
                continue;
1500
            }
1501
1502 23
            if (($options['type'] == 'date') && ($value == '0000-00-00') && !empty($options['blankisnull'])) {
1503
                $value = null;
1504
            }
1505 23
            if (($options['type'] == 'timestamp')) {
1506 23
                if (is_numeric($value)) {
1507 14
                    $value = date('Y-m-d H:i:s', $value);
1508 16
                } elseif ($value == null && !$options['notnull']) {
1509
                    $value = null;
1510
                }
1511
            }
1512
1513 23
            if (($options['type'] == 'serialized') && !is_string($value)) {
1514
                $value = serialize($value);
1515
            }
1516
1517 23
            if (($options['type'] == 'list') && is_array($value)) {
1518 11
                $delim = empty($options['delimiter']) ? ',' : $options['delimiter'];
1519 11
                $value = implode($delim, $value);
1520
            }
1521
1522 23
            $record[$field] = $value;
1523
        }
1524
1525 23
        return $record;
1526
    }
1527
1528 23
    protected static function _mapValuesToSet($recordValues)
1529
    {
1530 23
        $set = [];
1531
1532 23
        foreach ($recordValues as $field => $value) {
1533 23
            $fieldConfig = static::$_classFields[get_called_class()][$field];
1534
1535 23
            if ($value === null) {
1536 10
                $set[] = sprintf('`%s` = NULL', $fieldConfig['columnName']);
1537 23
            } elseif ($fieldConfig['type'] == 'timestamp' && $value == 'CURRENT_TIMESTAMP') {
1538
                $set[] = sprintf('`%s` = CURRENT_TIMESTAMP', $fieldConfig['columnName']);
1539 23
            } elseif ($fieldConfig['type'] == 'set' && is_array($value)) {
1540 11
                $set[] = sprintf('`%s` = "%s"', $fieldConfig['columnName'], DB::escape(join(',', $value)));
1541 23
            } elseif ($fieldConfig['type'] == 'boolean') {
1542 16
                $set[] = sprintf('`%s` = %u', $fieldConfig['columnName'], $value ? 1 : 0);
1543
            } else {
1544 23
                $set[] = sprintf('`%s` = "%s"', $fieldConfig['columnName'], DB::escape($value));
1545
            }
1546
        }
1547
1548 23
        return $set;
1549
    }
1550
1551 16
    protected static function _mapFieldOrder($order)
1552
    {
1553 16
        if (is_string($order)) {
1554 4
            return [$order];
1555 14
        } elseif (is_array($order)) {
1556 14
            $r = [];
1557
1558 14
            foreach ($order as $key => $value) {
1559 14
                if (is_string($key)) {
1560 13
                    $columnName = static::_cn($key);
1561 13
                    $direction = strtoupper($value)=='DESC' ? 'DESC' : 'ASC';
1562
                } else {
1563 2
                    $columnName = static::_cn($value);
1564 1
                    $direction = 'ASC';
1565
                }
1566
1567 13
                $r[] = sprintf('`%s` %s', $columnName, $direction);
1568
            }
1569
1570 13
            return $r;
1571
        }
1572
    }
1573
1574
    /**
1575
     * @param array<string,null|string|array{'operator': string, 'value': string}> $conditions
1576
     * @return array
1577
     */
1578 85
    protected static function _mapConditions($conditions)
1579
    {
1580 85
        foreach ($conditions as $field => &$condition) {
1581 85
            if (is_string($field)) {
1582 83
                if (isset(static::$_classFields[get_called_class()][$field])) {
1583 82
                    $fieldOptions = static::$_classFields[get_called_class()][$field];
1584
                }
1585
1586 83
                if ($condition === null || ($condition == '' && $fieldOptions['blankisnull'])) {
1587 1
                    $condition = sprintf('`%s` IS NULL', static::_cn($field));
1588 83
                } elseif (is_array($condition)) {
1589 1
                    $condition = sprintf('`%s` %s "%s"', static::_cn($field), $condition['operator'], DB::escape($condition['value']));
1590
                } else {
1591 83
                    $condition = sprintf('`%s` = "%s"', static::_cn($field), DB::escape($condition));
1592
                }
1593
            }
1594
        }
1595
1596 85
        return $conditions;
1597
    }
1598
1599 20
    protected function finishValidation()
1600
    {
1601 20
        $this->_isValid = $this->_isValid && !$this->_validator->hasErrors();
1602
1603 20
        if (!$this->_isValid) {
1604
            $this->_validationErrors = array_merge($this->_validationErrors, $this->_validator->getErrors());
1605
        }
1606
1607 20
        return $this->_isValid;
1608
    }
1609
}
1610