Passed
Branch release (695ec7)
by Henry
04:31
created

ActiveRecord::_prepareRecordValues()   D

Complexity

Conditions 18
Paths 146

Size

Total Lines 43
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 18
eloc 25
c 1
b 0
f 0
nc 146
nop 0
dl 0
loc 43
rs 4.4833

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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

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

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

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

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

697
            $this->/** @scrutinizer ignore-call */ 
698
                   beforeVersionedSave();
Loading history...
698
        }
699
700
        // set created
701
        if (static::fieldExists('Created') && (!$this->Created || ($this->Created == 'CURRENT_TIMESTAMP'))) {
702
            $this->Created = time();
703
        }
704
705
        // validate
706
        if (!$this->validate($deep)) {
707
            throw new Exception('Cannot save invalid record');
708
        }
709
710
        $this->clearCaches();
711
712
        if ($this->isDirty) {
713
            // prepare record values
714
            $recordValues = $this->_prepareRecordValues();
715
716
            // transform record to set array
717
            $set = static::_mapValuesToSet($recordValues);
718
            // create new or update existing
719
            if ($this->_isPhantom) {
720
                DB::nonQuery(
721
                    'INSERT INTO `%s` SET %s',
722
                    [
723
                        static::$tableName,
724
                        join(',', $set),
725
                    ],
726
                    [static::class,'handleError']
727
                );
728
                $this->_record[static::$primaryKey ? static::$primaryKey : 'ID'] = DB::insertID();
729
                $this->_isPhantom = false;
730
                $this->_isNew = true;
731
            } elseif (count($set)) {
732
                DB::nonQuery(
733
                    'UPDATE `%s` SET %s WHERE `%s` = %u',
734
                    [
735
                        static::$tableName,
736
                        join(',', $set),
737
                        static::_cn(static::$primaryKey ? static::$primaryKey : 'ID'),
738
                        $this->getPrimaryKeyValue(),
739
                    ],
740
                    [static::class,'handleError']
741
                );
742
743
                $this->_isUpdated = true;
744
            }
745
746
            // update state
747
            $this->_isDirty = false;
748
            if (static::isVersioned()) {
749
                $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

749
                $this->/** @scrutinizer ignore-call */ 
750
                       afterVersionedSave();
Loading history...
750
            }
751
        }
752
        $this->afterSave();
753
    }
754
755
756
    /**
757
     * Deletes this object.
758
     *
759
     * @return bool True if database returns number of affected rows above 0. False otherwise.
760
     */
761
    public function destroy()
762
    {
763
        if (static::isVersioned()) {
764
            if (static::$createRevisionOnDestroy) {
765
                // save a copy to history table
766
                if ($this->fieldExists('Created')) {
767
                    $this->Created = time();
768
                }
769
770
                $recordValues = $this->_prepareRecordValues();
771
                $set = static::_mapValuesToSet($recordValues);
772
773
                DB::nonQuery(
774
                    'INSERT INTO `%s` SET %s',
775
                    [
776
                                static::getHistoryTable(),
0 ignored issues
show
Bug introduced by
The method getHistoryTable() does not exist on Divergence\Models\ActiveRecord. It seems like you code against a sub-type of Divergence\Models\ActiveRecord such as Divergence\Tests\MockSite\Models\Forum\TagPost or Divergence\Tests\MockSite\Models\Canary or Divergence\Tests\MockSite\Models\Forum\Thread or Divergence\Tests\MockSite\Models\Forum\Category or Divergence\Tests\MockSite\Models\Forum\Post. ( Ignorable by Annotation )

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

776
                                static::/** @scrutinizer ignore-call */ 
777
                                        getHistoryTable(),
Loading history...
777
                                join(',', $set),
778
                        ]
779
                );
780
            }
781
        }
782
783
        return static::delete($this->getPrimaryKeyValue());
784
    }
785
786
    /**
787
     * Delete by ID
788
     *
789
     * @param int $id
790
     * @return bool True if database returns number of affected rows above 0. False otherwise.
791
     */
792
    public static function delete($id)
793
    {
794
        DB::nonQuery('DELETE FROM `%s` WHERE `%s` = %u', [
795
            static::$tableName,
796
            static::_cn(static::$primaryKey ? static::$primaryKey : 'ID'),
797
            $id,
798
        ], [static::class,'handleError']);
799
800
        return DB::affectedRows() > 0;
801
    }
802
803
    /**
804
     * Builds the extra columns you might want to add to a database select query after the initial list of model fields.
805
     *
806
     * @param array|string $columns An array of keys and values or a string which will be added to a list of fields after the query's SELECT clause.
807
     * @return string|null Extra columns to add after a SELECT clause in a query. Always starts with a comma.
808
     */
809
    public static function buildExtraColumns($columns)
810
    {
811
        if (!empty($columns)) {
812
            if (is_array($columns)) {
813
                foreach ($columns as $key => $value) {
814
                    return ', '.$value.' AS '.$key;
815
                }
816
            } else {
817
                return ', ' . $columns;
818
            }
819
        }
820
    }
821
822
    /**
823
     * Builds the HAVING clause of a MySQL database query.
824
     *
825
     * @param array|string $having Same as conditions. Can provide a string to use or an array of field/value pairs which will be joined by the AND operator.
826
     * @return string|null
827
     */
828
    public static function buildHaving($having)
829
    {
830
        if (!empty($having)) {
831
            return ' HAVING (' . (is_array($having) ? join(') AND (', static::_mapConditions($having)) : $having) . ')';
832
        }
833
    }
834
835
836
    // TODO: make the handleField
837
    public static function generateRandomHandle($length = 32)
838
    {
839
        do {
840
            $handle = substr(md5(mt_rand(0, mt_getrandmax())), 0, $length);
841
        } while (static::getByField(static::$handleField, $handle));
0 ignored issues
show
introduced by
The method getByField() does not exist on Divergence\Models\ActiveRecord. Maybe you want to declare this class abstract? ( Ignorable by Annotation )

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

841
        } while (static::/** @scrutinizer ignore-call */ getByField(static::$handleField, $handle));
Loading history...
842
843
        return $handle;
844
    }
845
846
    /**
847
     * Checks of a field exists for this model in the fields config.
848
     *
849
     * @param string $field Name of the field
850
     * @return bool True if the field exists. False otherwise.
851
     */
852
    public static function fieldExists($field)
853
    {
854
        static::init();
855
        return array_key_exists($field, static::$_classFields[get_called_class()]);
856
    }
857
858
    /**
859
     * Returns the current configuration of class fields for the called class.
860
     *
861
     * @return array Current configuration of class fields for the called class.
862
     */
863
    public static function getClassFields()
864
    {
865
        static::init();
866
        return static::$_classFields[get_called_class()];
867
    }
868
869
    /**
870
     * Returns either a field option or an array of all the field options.
871
     *
872
     * @param string $field Name of the field.
873
     * @param boolean $optionKey
874
     * @return void
875
     */
876
    public static function getFieldOptions($field, $optionKey = false)
877
    {
878
        if ($optionKey) {
879
            return static::$_classFields[get_called_class()][$field][$optionKey];
880
        } else {
881
            return static::$_classFields[get_called_class()][$field];
882
        }
883
    }
884
885
    /**
886
     * Returns columnName for given field
887
     * @param string $field name of field
888
     * @return string column name
889
     */
890
    public static function getColumnName($field)
891
    {
892
        static::init();
893
        if (!static::fieldExists($field)) {
894
            throw new Exception('getColumnName called on nonexisting column: ' . get_called_class().'->'.$field);
895
        }
896
897
        return static::$_classFields[get_called_class()][$field]['columnName'];
898
    }
899
900
    public static function mapFieldOrder($order)
901
    {
902
        return static::_mapFieldOrder($order);
903
    }
904
905
    public static function mapConditions($conditions)
906
    {
907
        return static::_mapConditions($conditions);
908
    }
909
910
    /**
911
     * Returns static::$rootClass for the called class.
912
     *
913
     * @return string static::$rootClass for the called class.
914
     */
915
    public function getRootClass()
916
    {
917
        return static::$rootClass;
918
    }
919
920
    /**
921
     * Sets an array of validation errors for this object.
922
     *
923
     * @param array $array Validation errors in the form Field Name => error message
924
     * @return void
925
     */
926
    public function addValidationErrors($array)
927
    {
928
        foreach ($array as $field => $errorMessage) {
929
            $this->addValidationError($field, $errorMessage);
930
        }
931
    }
932
933
    /**
934
     * Sets a validation error for this object. Sets $this->_isValid to false.
935
     *
936
     * @param string $field
937
     * @param string $errorMessage
938
     * @return void
939
     */
940
    public function addValidationError($field, $errorMessage)
941
    {
942
        $this->_isValid = false;
943
        $this->_validationErrors[$field] = $errorMessage;
944
    }
945
946
    /**
947
     * Get a validation error for a given field.
948
     *
949
     * @param string $field Name of the field.
950
     * @return string|null A validation error for the field. Null is no validation error found.
951
     */
952
    public function getValidationError($field)
953
    {
954
        // break apart path
955
        $crumbs = explode('.', $field);
956
957
        // resolve path recursively
958
        $cur = &$this->_validationErrors;
959
        while ($crumb = array_shift($crumbs)) {
960
            if (array_key_exists($crumb, $cur)) {
961
                $cur = &$cur[$crumb];
962
            } else {
963
                return null;
964
            }
965
        }
966
967
        // return current value
968
        return $cur;
969
    }
970
971
    /**
972
     * Validates the model. Instantiates a new RecordValidator object and sets it to $this->_validator.
973
     * Then validates against the set validators in this model. Returns $this->_isValid
974
     *
975
     * @param boolean $deep If true will attempt to validate any already loaded relationship members.
976
     * @return bool $this->_isValid which could be set to true or false depending on what happens with the RecordValidator.
977
     */
978
    public function validate($deep = true)
979
    {
980
        $this->_isValid = true;
981
        $this->_validationErrors = [];
982
983
        if (!isset($this->_validator)) {
984
            $this->_validator = new RecordValidator($this->_record);
985
        } else {
986
            $this->_validator->resetErrors();
987
        }
988
989
        foreach (static::$validators as $validator) {
990
            $this->_validator->validate($validator);
991
        }
992
993
        $this->finishValidation();
994
995
        if ($deep) {
996
            // validate relationship objects
997
            if (!empty(static::$_classRelationships[get_called_class()])) {
998
                foreach (static::$_classRelationships[get_called_class()] as $relationship => $options) {
999
                    if (empty($this->_relatedObjects[$relationship])) {
1000
                        continue;
1001
                    }
1002
1003
1004
                    if ($options['type'] == 'one-one') {
1005
                        if ($this->_relatedObjects[$relationship]->isDirty) {
1006
                            $this->_relatedObjects[$relationship]->validate();
1007
                            $this->_isValid = $this->_isValid && $this->_relatedObjects[$relationship]->isValid;
1008
                            $this->_validationErrors[$relationship] = $this->_relatedObjects[$relationship]->validationErrors;
1009
                        }
1010
                    } elseif ($options['type'] == 'one-many') {
1011
                        foreach ($this->_relatedObjects[$relationship] as $i => $object) {
1012
                            if ($object->isDirty) {
1013
                                $object->validate();
1014
                                $this->_isValid = $this->_isValid && $object->isValid;
1015
                                $this->_validationErrors[$relationship][$i] = $object->validationErrors;
1016
                            }
1017
                        }
1018
                    }
1019
                } // foreach
1020
            } // if
1021
        } // if ($deep)
1022
1023
        return $this->_isValid;
1024
    }
1025
1026
    /**
1027
     * Handle any errors that come from the database client in the process of running a query.
1028
     * 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.
1029
     * Other errors will be routed through to DB::handleError
1030
     *
1031
     * @param string $query
1032
     * @param array $queryLog
1033
     * @param array|string $parameters
1034
     * @return mixed Retried query result or the return from DB::handleError
1035
     */
1036
    public static function handleError($query = null, $queryLog = null, $parameters = null)
1037
    {
1038
        $Connection = DB::getConnection();
1039
1040
        if ($Connection->errorCode() == '42S02' && static::$autoCreateTables) {
1041
            $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...
1042
1043
            // history versions table
1044
            if (static::isVersioned()) {
1045
                $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...
1046
            }
1047
1048
            $Statement = $Connection->query($CreateTable);
1049
1050
            // check for errors
1051
            $ErrorInfo = $Statement->errorInfo();
1052
1053
            // handle query error
1054
            if ($ErrorInfo[0] != '00000') {
1055
                self::handleError($query, $queryLog);
1056
            }
1057
1058
            // clear buffer (required for the next query to work without running fetchAll first
1059
            $Statement->closeCursor();
1060
1061
            return $Connection->query($query); // now the query should finish with no error
1062
        } else {
1063
            return DB::handleError($query, $queryLog);
1064
        }
1065
    }
1066
1067
    /**
1068
     * Iterates through all static::$beforeSave and static::$afterSave in this class and any of it's parent classes.
1069
     * Checks if they are callables and if they are adds them to static::$_classBeforeSave[] and static::$_classAfterSave[]
1070
     *
1071
     * @return void
1072
     *
1073
     * @uses static::$beforeSave
1074
     * @uses static::$afterSave
1075
     * @uses static::$_classBeforeSave
1076
     * @uses static::$_classAfterSave
1077
     */
1078
    protected static function _defineEvents()
1079
    {
1080
        // run before save
1081
        $className = get_called_class();
1082
1083
        // merge fields from first ancestor up
1084
        $classes = class_parents($className);
1085
        array_unshift($classes, $className);
1086
1087
        while ($class = array_pop($classes)) {
1088
            if (is_callable($class::$beforeSave)) {
1089
                if (!empty($class::$beforeSave)) {
1090
                    if (!in_array($class::$beforeSave, static::$_classBeforeSave)) {
1091
                        static::$_classBeforeSave[] = $class::$beforeSave;
1092
                    }
1093
                }
1094
            }
1095
1096
            if (is_callable($class::$afterSave)) {
1097
                if (!empty($class::$afterSave)) {
1098
                    if (!in_array($class::$afterSave, static::$_classAfterSave)) {
1099
                        static::$_classAfterSave[] = $class::$afterSave;
1100
                    }
1101
                }
1102
            }
1103
        }
1104
    }
1105
1106
    /**
1107
     * Merges all static::$_classFields in this class and any of it's parent classes.
1108
     * Sets the merged value to static::$_classFields[get_called_class()]
1109
     *
1110
     * @return void
1111
     *
1112
     * @uses static::$_classFields
1113
     * @uses static::$_classFields
1114
     */
1115
    protected static function _defineFields()
1116
    {
1117
        $className = get_called_class();
1118
1119
        // skip if fields already defined
1120
        if (isset(static::$_classFields[$className])) {
1121
            return;
1122
        }
1123
1124
        // merge fields from first ancestor up
1125
        $classes = class_parents($className);
1126
        array_unshift($classes, $className);
1127
1128
        static::$_classFields[$className] = [];
1129
        while ($class = array_pop($classes)) {
1130
            if (!empty($class::$fields)) {
1131
                static::$_classFields[$className] = array_merge(static::$_classFields[$className], $class::$fields);
1132
            }
1133
        }
1134
1135
        // versioning
1136
        if (static::isVersioned()) {
1137
            static::$_classFields[$className] = array_merge(static::$_classFields[$className], static::$versioningFields);
0 ignored issues
show
Bug introduced by
It seems like static::versioningFields can also be of type integer; however, parameter $array2 of array_merge() does only seem to accept array|null, 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

1137
            static::$_classFields[$className] = array_merge(static::$_classFields[$className], /** @scrutinizer ignore-type */ static::$versioningFields);
Loading history...
1138
        }
1139
    }
1140
1141
1142
    /**
1143
     * Called after _defineFields to initialize and apply defaults to the fields property
1144
     * Must be idempotent as it may be applied multiple times up the inheritance chain
1145
     * @return void
1146
     *
1147
     * @uses static::$_classFields
1148
     */
1149
    protected static function _initFields()
1150
    {
1151
        $className = get_called_class();
1152
        $optionsMask = [
1153
            'type' => null,
1154
            'length' => null,
1155
            'primary' => null,
1156
            'unique' => null,
1157
            'autoincrement' => null,
1158
            'notnull' => null,
1159
            'unsigned' => null,
1160
            'default' => null,
1161
            'values' => null,
1162
        ];
1163
1164
        // apply default values to field definitions
1165
        if (!empty(static::$_classFields[$className])) {
1166
            $fields = [];
1167
1168
            foreach (static::$_classFields[$className] as $field => $options) {
1169
                if (is_string($field)) {
1170
                    if (is_array($options)) {
1171
                        $fields[$field] = array_merge($optionsMask, static::$fieldDefaults, ['columnName' => $field], $options);
1172
                    } elseif (is_string($options)) {
1173
                        $fields[$field] = array_merge($optionsMask, static::$fieldDefaults, ['columnName' => $field, 'type' => $options]);
1174
                    } elseif ($options == null) {
1175
                        continue;
1176
                    }
1177
                } elseif (is_string($options)) {
1178
                    $field = $options;
1179
                    $fields[$field] = array_merge($optionsMask, static::$fieldDefaults, ['columnName' => $field]);
1180
                }
1181
1182
                if ($field == 'Class') {
1183
                    // apply Class enum values
1184
                    $fields[$field]['values'] = static::$subClasses;
1185
                }
1186
1187
                if (!isset($fields[$field]['blankisnull']) && empty($fields[$field]['notnull'])) {
1188
                    $fields[$field]['blankisnull'] = true;
1189
                }
1190
1191
                if ($fields[$field]['autoincrement']) {
1192
                    $fields[$field]['primary'] = true;
1193
                }
1194
            }
1195
1196
            static::$_classFields[$className] = $fields;
1197
        }
1198
    }
1199
1200
1201
    /**
1202
     * Returns class name for instantiating given record
1203
     * @param array $record record
1204
     * @return string class name
1205
     */
1206
    protected static function _getRecordClass($record)
1207
    {
1208
        $static = get_called_class();
1209
1210
        if (!static::fieldExists('Class')) {
1211
            return $static;
1212
        }
1213
1214
        $columnName = static::_cn('Class');
1215
1216
        if (!empty($record[$columnName]) && is_subclass_of($record[$columnName], $static)) {
1217
            return $record[$columnName];
1218
        } else {
1219
            return $static;
1220
        }
1221
    }
1222
1223
    /**
1224
     * Shorthand alias for _getColumnName
1225
     * @param string $field name of field
1226
     * @return string column name
1227
     */
1228
    protected static function _cn($field)
1229
    {
1230
        return static::getColumnName($field);
1231
    }
1232
1233
1234
    /**
1235
     * Retrieves given field's value
1236
     * @param string $field Name of field
1237
     * @return mixed value
1238
     */
1239
    protected function _getFieldValue($field, $useDefault = true)
1240
    {
1241
        $fieldOptions = static::$_classFields[get_called_class()][$field];
1242
1243
        if (isset($this->_record[$fieldOptions['columnName']])) {
1244
            $value = $this->_record[$fieldOptions['columnName']];
1245
1246
            // apply type-dependent transformations
1247
            switch ($fieldOptions['type']) {
1248
                case 'password':
1249
                {
1250
                    return $value;
1251
                }
1252
1253
                case 'timestamp':
1254
                {
1255
                    if (!isset($this->_convertedValues[$field])) {
1256
                        if ($value && $value != '0000-00-00 00:00:00') {
1257
                            $this->_convertedValues[$field] = strtotime($value);
1258
                        } else {
1259
                            $this->_convertedValues[$field] = null;
1260
                        }
1261
                    }
1262
1263
                    return $this->_convertedValues[$field];
1264
                }
1265
                case 'serialized':
1266
                {
1267
                    if (!isset($this->_convertedValues[$field])) {
1268
                        $this->_convertedValues[$field] = is_string($value) ? unserialize($value) : $value;
1269
                    }
1270
1271
                    return $this->_convertedValues[$field];
1272
                }
1273
                case 'set':
1274
                case 'list':
1275
                {
1276
                    if (!isset($this->_convertedValues[$field])) {
1277
                        $delim = empty($fieldOptions['delimiter']) ? ',' : $fieldOptions['delimiter'];
1278
                        $this->_convertedValues[$field] = array_filter(preg_split('/\s*'.$delim.'\s*/', $value));
0 ignored issues
show
Bug introduced by
It seems like preg_split('/\s*' . $delim . '\s*/', $value) can also be of type false; however, parameter $input of array_filter() does only seem to accept array, 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

1278
                        $this->_convertedValues[$field] = array_filter(/** @scrutinizer ignore-type */ preg_split('/\s*'.$delim.'\s*/', $value));
Loading history...
1279
                    }
1280
1281
                    return $this->_convertedValues[$field];
1282
                }
1283
1284
                case 'int':
1285
                case 'integer':
1286
                case 'uint':
1287
                    if (!isset($this->_convertedValues[$field])) {
1288
                        if (!$fieldOptions['notnull'] && is_null($value)) {
1289
                            $this->_convertedValues[$field] = $value;
1290
                        } else {
1291
                            $this->_convertedValues[$field] = intval($value);
1292
                        }
1293
                    }
1294
                    return $this->_convertedValues[$field];
1295
                    
1296
                case 'boolean':
1297
                {
1298
                    if (!isset($this->_convertedValues[$field])) {
1299
                        $this->_convertedValues[$field] = (boolean)$value;
1300
                    }
1301
1302
                    return $this->_convertedValues[$field];
1303
                }
1304
1305
                default:
1306
                {
1307
                    return $value;
1308
                }
1309
            }
1310
        } elseif ($useDefault && isset($fieldOptions['default'])) {
1311
            // return default
1312
            return $fieldOptions['default'];
1313
        } else {
1314
            switch ($fieldOptions['type']) {
1315
                case 'set':
1316
                case 'list':
1317
                {
1318
                    return [];
1319
                }
1320
                default:
1321
                {
1322
                    return null;
1323
                }
1324
            }
1325
        }
1326
    }
1327
1328
    /**
1329
     * Sets given field's value
1330
     * @param string $field Name of field
1331
     * @param mixed $value New value
1332
     * @return mixed value
1333
     */
1334
    protected function _setFieldValue($field, $value)
1335
    {
1336
        // ignore setting versioning fields
1337
        if (static::isVersioned()) {
1338
            if (array_key_exists($field, static::$versioningFields)) {
0 ignored issues
show
Bug introduced by
It seems like static::versioningFields can also be of type integer and null; however, parameter $search of array_key_exists() does only seem to accept array, 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

1338
            if (array_key_exists($field, /** @scrutinizer ignore-type */ static::$versioningFields)) {
Loading history...
1339
                return false;
1340
            }
1341
        }
1342
1343
        if (!static::fieldExists($field)) {
1344
            return false;
1345
        }
1346
        $fieldOptions = static::$_classFields[get_called_class()][$field];
1347
1348
        // no overriding autoincrements
1349
        if ($fieldOptions['autoincrement']) {
1350
            return false;
1351
        }
1352
1353
        if (!isset($this->fieldSetMapper)) {
1354
            $this->fieldSetMapper = new DefaultSetMapper();
1355
        }
1356
1357
        // pre-process value
1358
        $forceDirty = false;
1359
        switch ($fieldOptions['type']) {
1360
            case 'clob':
1361
            case 'string':
1362
            {
1363
                $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

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

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

1510
                $set[] = sprintf('`%s` = "%s"', $fieldConfig['columnName'], /** @scrutinizer ignore-type */ DB::escape($value));
Loading history...
1511
            }
1512
        }
1513
1514
        return $set;
1515
    }
1516
1517
    protected static function _mapFieldOrder($order)
1518
    {
1519
        if (is_string($order)) {
1520
            return [$order];
1521
        } elseif (is_array($order)) {
1522
            $r = [];
1523
1524
            foreach ($order as $key => $value) {
1525
                if (is_string($key)) {
1526
                    $columnName = static::_cn($key);
1527
                    $direction = strtoupper($value)=='DESC' ? 'DESC' : 'ASC';
1528
                } else {
1529
                    $columnName = static::_cn($value);
1530
                    $direction = 'ASC';
1531
                }
1532
1533
                $r[] = sprintf('`%s` %s', $columnName, $direction);
1534
            }
1535
1536
            return $r;
1537
        }
1538
    }
1539
1540
    protected static function _mapConditions($conditions)
1541
    {
1542
        foreach ($conditions as $field => &$condition) {
1543
            if (is_string($field)) {
1544
                if (isset(static::$_classFields[get_called_class()][$field])) {
1545
                    $fieldOptions = static::$_classFields[get_called_class()][$field];
1546
                }
1547
1548
                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...
1549
                    $condition = sprintf('`%s` IS NULL', static::_cn($field));
1550
                } elseif (is_array($condition)) {
1551
                    $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 $args of sprintf() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

1551
                    $condition = sprintf('`%s` %s "%s"', static::_cn($field), $condition['operator'], /** @scrutinizer ignore-type */ DB::escape($condition['value']));
Loading history...
1552
                } else {
1553
                    $condition = sprintf('`%s` = "%s"', static::_cn($field), DB::escape($condition));
1554
                }
1555
            }
1556
        }
1557
1558
        return $conditions;
1559
    }
1560
1561
    protected function finishValidation()
1562
    {
1563
        $this->_isValid = $this->_isValid && !$this->_validator->hasErrors();
1564
1565
        if (!$this->_isValid) {
1566
            $this->_validationErrors = array_merge($this->_validationErrors, $this->_validator->getErrors());
1567
        }
1568
1569
        return $this->_isValid;
1570
    }
1571
}
1572