Completed
Branch develop (6af1b6)
by Henry
08:27
created

ActiveRecord::generateRandomHandle()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 4
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 7
rs 10

1 Method

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

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

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

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

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

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

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

731
                    sprintf('`%s` = %u', static::_cn($this->getPrimaryKey()), /** @scrutinizer ignore-type */ $this->getPrimaryKeyValue())
Loading history...
732
                ), null, [static::class,'handleError']);
733
734
                $this->_isUpdated = true;
735
            }
736
737
            // update state
738
            $this->_isDirty = false;
739
            if (static::isVersioned()) {
740
                $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

740
                $this->/** @scrutinizer ignore-call */ 
741
                       afterVersionedSave();
Loading history...
741
            }
742
        }
743
        $this->afterSave();
744
    }
745
746
747
    /**
748
     * Deletes this object.
749
     *
750
     * @return bool True if database returns number of affected rows above 0. False otherwise.
751
     */
752
    public function destroy()
753
    {
754
        if (static::isVersioned()) {
755
            if (static::$createRevisionOnDestroy) {
756
                // save a copy to history table
757
                if ($this->fieldExists('Created')) {
758
                    $this->Created = time();
759
                }
760
761
                $recordValues = $this->_prepareRecordValues();
762
                $set = static::_mapValuesToSet($recordValues);
763
764
                DB::nonQuery((new Insert())->setTable(static::getHistoryTable())->set($set), null, [static::class,'handleError']);
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

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

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
980
981
            // history versions table
982
            if (static::isVersioned()) {
983
                $CreateTable .= SQL::getCreateTable(static::$rootClass, true);
0 ignored issues
show
Bug introduced by
Are you sure the usage of Divergence\IO\Database\S...tatic::rootClass, true) targeting Divergence\IO\Database\SQL::getCreateTable() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

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

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

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

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