Passed
Push — master ( ed9dd1...f1a998 )
by Henry
02:28
created

ActiveRecord::jsonSerialize()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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

1326
            static::$_classFields[$className] = array_merge(static::$_classFields[$className], /** @scrutinizer ignore-type */ static::$versioningFields);
Loading history...
1327
        }
1328
    }
1329
1330
    
1331
    /**
1332
     * Called after _defineFields to initialize and apply defaults to the fields property
1333
     * Must be idempotent as it may be applied multiple times up the inheritence chain
1334
     */
1335
    protected static function _initFields()
1336
    {
1337
        $className = get_called_class();
1338
        $optionsMask = [
1339
            'type' => null,
1340
            'length' => null,
1341
            'primary' => null,
1342
            'unique' => null,
1343
            'autoincrement' => null,
1344
            'notnull' => null,
1345
            'unsigned' => null,
1346
            'default' => null,
1347
            'values' => null,
1348
        ];
1349
        
1350
        // apply default values to field definitions
1351
        if (!empty(static::$_classFields[$className])) {
1352
            $fields = [];
1353
            
1354
            foreach (static::$_classFields[$className] as $field => $options) {
1355
                if (is_string($field)) {
1356
                    if (is_array($options)) {
1357
                        $fields[$field] = array_merge($optionsMask, static::$fieldDefaults, ['columnName' => $field], $options);
1358
                    } elseif (is_string($options)) {
1359
                        $fields[$field] = array_merge($optionsMask, static::$fieldDefaults, ['columnName' => $field, 'type' => $options]);
1360
                    } elseif ($options == null) {
1361
                        continue;
1362
                    }
1363
                } elseif (is_string($options)) {
1364
                    $field = $options;
1365
                    $fields[$field] = array_merge($optionsMask, static::$fieldDefaults, ['columnName' => $field]);
1366
                }
1367
                
1368
                if ($field == 'Class') {
1369
                    // apply Class enum values
1370
                    $fields[$field]['values'] = static::$subClasses;
1371
                }
1372
                
1373
                if (!isset($fields[$field]['blankisnull']) && empty($fields[$field]['notnull'])) {
1374
                    $fields[$field]['blankisnull'] = true;
1375
                }
1376
                
1377
                if ($fields[$field]['autoincrement']) {
1378
                    $fields[$field]['primary'] = true;
1379
                }
1380
            }
1381
            
1382
            static::$_classFields[$className] = $fields;
1383
        }
1384
    }
1385
1386
1387
    /**
1388
     * Returns class name for instantiating given record
1389
     * @param array $record record
1390
     * @return string class name
1391
     */
1392
    protected static function _getRecordClass($record)
1393
    {
1394
        $static = get_called_class();
1395
        
1396
        if (!static::fieldExists('Class')) {
1397
            return $static;
1398
        }
1399
        
1400
        $columnName = static::_cn('Class');
1401
        
1402
        if (!empty($record[$columnName]) && is_subclass_of($record[$columnName], $static)) {
1403
            return $record[$columnName];
1404
        } else {
1405
            return $static;
1406
        }
1407
    }
1408
    
1409
    /**
1410
     * Shorthand alias for _getColumnName
1411
     * @param string $field name of field
1412
     * @return string column name
1413
     */
1414
    protected static function _cn($field)
1415
    {
1416
        return static::getColumnName($field);
1417
    }
1418
1419
    
1420
    /**
1421
     * Retrieves given field's value
1422
     * @param string $field Name of field
1423
     * @return mixed value
1424
     */
1425
    protected function _getFieldValue($field, $useDefault = true)
1426
    {
1427
        $fieldOptions = static::$_classFields[get_called_class()][$field];
1428
    
1429
        if (isset($this->_record[$fieldOptions['columnName']])) {
1430
            $value = $this->_record[$fieldOptions['columnName']];
1431
            
1432
            // apply type-dependent transformations
1433
            switch ($fieldOptions['type']) {
1434
                case 'password':
1435
                {
1436
                    return $value;
1437
                }
1438
                
1439
                case 'timestamp':
1440
                {
1441
                    if (!isset($this->_convertedValues[$field])) {
1442
                        if ($value && $value != '0000-00-00 00:00:00') {
1443
                            $this->_convertedValues[$field] = strtotime($value);
1444
                        } else {
1445
                            $this->_convertedValues[$field] = null;
1446
                        }
1447
                    }
1448
                    
1449
                    return $this->_convertedValues[$field];
1450
                }
1451
                case 'serialized':
1452
                {
1453
                    if (!isset($this->_convertedValues[$field])) {
1454
                        $this->_convertedValues[$field] = is_string($value) ? unserialize($value) : $value;
1455
                    }
1456
                    
1457
                    return $this->_convertedValues[$field];
1458
                }
1459
                case 'set':
1460
                case 'list':
1461
                {
1462
                    if (!isset($this->_convertedValues[$field])) {
1463
                        $delim = empty($fieldOptions['delimiter']) ? ',' : $fieldOptions['delimiter'];
1464
                        $this->_convertedValues[$field] = array_filter(preg_split('/\s*'.$delim.'\s*/', $value));
1465
                    }
1466
                    
1467
                    return $this->_convertedValues[$field];
1468
                }
1469
                
1470
                case 'boolean':
1471
                {
1472
                    if (!isset($this->_convertedValues[$field])) {
1473
                        $this->_convertedValues[$field] = (boolean)$value;
1474
                    }
1475
                    
1476
                    return $this->_convertedValues[$field];
1477
                }
1478
                
1479
                default:
1480
                {
1481
                    return $value;
1482
                }
1483
            }
1484
        } elseif ($useDefault && isset($fieldOptions['default'])) {
1485
            // return default
1486
            return $fieldOptions['default'];
1487
        } else {
1488
            switch ($fieldOptions['type']) {
1489
                case 'set':
1490
                case 'list':
1491
                {
1492
                    return [];
1493
                }
1494
                default:
1495
                {
1496
                    return null;
1497
                }
1498
            }
1499
        }
1500
    }
1501
    
1502
    protected function _setStringValue($fieldOptions, $value)
1503
    {
1504
        if (!$fieldOptions['notnull'] && $fieldOptions['blankisnull'] && ($value === '' || $value === null)) {
1505
            return null;
1506
        }
1507
        return @mb_convert_encoding($value, DB::$encoding, 'auto'); // normalize encoding to ASCII
1508
    }
1509
1510
    protected function _setBooleanValue($value)
1511
    {
1512
        return (boolean)$value;
1513
    }
1514
1515
    protected function _setDecimalValue($value)
1516
    {
1517
        return preg_replace('/[^-\d.]/', '', $value);
1518
    }
1519
1520
    protected function _setIntegerValue($fieldOptions, $value)
1521
    {
1522
        $value = preg_replace('/[^-\d]/', '', $value);
1523
        if (!$fieldOptions['notnull'] && $value === '') {
1524
            return null;
1525
        }
1526
        return $value;
1527
    }
1528
1529
    protected function _setTimestampValue($value)
1530
    {
1531
        if (is_numeric($value)) {
1532
            return date('Y-m-d H:i:s', $value);
1533
        } elseif (is_string($value)) {
1534
            // trim any extra crap, or leave as-is if it doesn't fit the pattern
1535
            if (preg_match('/^(\d{4})\D?(\d{2})\D?(\d{2})T?(\d{2})\D?(\d{2})\D?(\d{2})/')) {
1536
                return preg_replace('/^(\d{4})\D?(\d{2})\D?(\d{2})T?(\d{2})\D?(\d{2})\D?(\d{2})/', '$1-$2-$3 $4:$5:$6', $value);
1537
            } else {
1538
                return date('Y-m-d H:i:s', strtotime($value));
1539
            }
1540
        }
1541
        return null;
1542
    }
1543
1544
    protected function _setSerializedValue($value)
1545
    {
1546
        return serialize($value);
1547
    }
1548
1549
    protected function _setEnumValue($fieldOptions, $value)
1550
    {
1551
        return (in_array($value, $fieldOptions['values']) ? $value : null);
1552
    }
1553
1554
    protected function _setListValue($fieldOptions, $value)
1555
    {
1556
        if (!is_array($value)) {
1557
            $delim = empty($fieldOptions['delimiter']) ? ',' : $fieldOptions['delimiter'];
1558
            $value = array_filter(preg_split('/\s*'.$delim.'\s*/', $value));
1559
        }
1560
        return $value;
1561
    }
1562
1563
    /**
1564
     * Sets given field's value
1565
     * @param string $field Name of field
1566
     * @param mixed $value New value
1567
     * @return mixed value
1568
     */
1569
    protected function _setFieldValue($field, $value)
1570
    {
1571
        // ignore setting versioning fields
1572
        if (static::isVersioned()) {
1573
            if (array_key_exists($field, static::$versioningFields)) {
0 ignored issues
show
Bug introduced by
It seems like static::versioningFields can also be of type null and integer; 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

1573
            if (array_key_exists($field, /** @scrutinizer ignore-type */ static::$versioningFields)) {
Loading history...
1574
                return false;
1575
            }
1576
        }
1577
        
1578
        if (!static::fieldExists($field)) {
1579
            return false;
1580
        }
1581
        $fieldOptions = static::$_classFields[get_called_class()][$field];
1582
1583
        // no overriding autoincrements
1584
        if ($fieldOptions['autoincrement']) {
1585
            return false;
1586
        }
1587
1588
        // pre-process value
1589
        $forceDirty = false;
1590
        switch ($fieldOptions['type']) {
1591
            case 'clob':
1592
            case 'string':
1593
            {
1594
                $value = $this->_setStringValue($fieldOptions, $value);
1595
                break;
1596
            }
1597
            
1598
            case 'boolean':
1599
            {
1600
                $value = $this->_setBooleanValue($value);
1601
                break;
1602
            }
1603
            
1604
            case 'decimal':
1605
            {
1606
                $value = $this->_setDecimalValue($value);
1607
                break;
1608
            }
1609
            
1610
            case 'int':
1611
            case 'uint':
1612
            case 'integer':
1613
            {
1614
                $value = $this->_setIntegerValue($fieldOptions, $value);
1615
                break;
1616
            }
1617
            
1618
            case 'timestamp':
1619
            {
1620
                $value = $this->_setTimestampValue($value);
1621
                break;
1622
            }
1623
            
1624
            case 'date':
1625
            {
1626
                $value = $this->_setDateValue($value);
1627
                break;
1628
            }
1629
            
1630
            // these types are converted to strings from another PHP type on save
1631
            case 'serialized':
1632
            {
1633
                $this->_convertedValues[$field] = $value;
1634
                $value = $this->_setSerializedValue($value);
1635
                break;
1636
            }
1637
            case 'enum':
1638
            {
1639
                $value = $this->_setEnumValue($fieldOptions, $value);
1640
                break;
1641
            }
1642
            case 'set':
1643
            case 'list':
1644
            {
1645
                $value = $this->_setListValue($fieldOptions, $value);
1646
                $this->_convertedValues[$field] = $value;
1647
                $forceDirty = true;
1648
                break;
1649
            }
1650
        }
1651
        
1652
        if ($forceDirty || ($this->_record[$field] !== $value)) {
1653
            $columnName = static::_cn($field);
1654
            if (isset($this->_record[$columnName])) {
1655
                $this->_originalValues[$field] = $this->_record[$columnName];
1656
            }
1657
            $this->_record[$columnName] = $value;
1658
            $this->_isDirty = true;
1659
            
1660
            // unset invalidated relationships
1661
            if (!empty($fieldOptions['relationships']) && static::isRelational()) {
1662
                foreach ($fieldOptions['relationships'] as $relationship => $isCached) {
1663
                    if ($isCached) {
1664
                        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...
1665
                    }
1666
                }
1667
            }
1668
            return true;
1669
        } else {
1670
            return false;
1671
        }
1672
    }
1673
1674
    protected function _prepareRecordValues()
1675
    {
1676
        $record = [];
1677
1678
        foreach (static::$_classFields[get_called_class()] as $field => $options) {
1679
            $columnName = static::_cn($field);
1680
            
1681
            if (array_key_exists($columnName, $this->_record)) {
1682
                $value = $this->_record[$columnName];
1683
                
1684
                if (!$value && !empty($options['blankisnull'])) {
1685
                    $value = null;
1686
                }
1687
            } elseif (isset($options['default'])) {
1688
                $value = $options['default'];
1689
            } else {
1690
                continue;
1691
            }
1692
1693
            if (($options['type'] == 'date') && ($value == '0000-00-00') && !empty($options['blankisnull'])) {
1694
                $value = null;
1695
            }
1696
            if (($options['type'] == 'timestamp')) {
1697
                if (is_numeric($value)) {
1698
                    $value = date('Y-m-d H:i:s', $value);
1699
                } elseif ($value == null && !$options['notnull']) {
1700
                    $value = null;
1701
                }
1702
            }
1703
1704
            if (($options['type'] == 'serialized') && !is_string($value)) {
1705
                $value = serialize($value);
1706
            }
1707
            
1708
            if (($options['type'] == 'list') && is_array($value)) {
1709
                $delim = empty($options['delimiter']) ? ',' : $options['delimiter'];
1710
                $value = implode($delim, $value);
1711
            }
1712
            
1713
            $record[$field] = $value;
1714
        }
1715
1716
        return $record;
1717
    }
1718
    
1719
    protected static function _mapValuesToSet($recordValues)
1720
    {
1721
        $set = [];
1722
1723
        foreach ($recordValues as $field => $value) {
1724
            $fieldConfig = static::$_classFields[get_called_class()][$field];
1725
            
1726
            if ($value === null) {
1727
                $set[] = sprintf('`%s` = NULL', $fieldConfig['columnName']);
1728
            } elseif ($fieldConfig['type'] == 'timestamp' && $value == 'CURRENT_TIMESTAMP') {
1729
                $set[] = sprintf('`%s` = CURRENT_TIMESTAMP', $fieldConfig['columnName']);
1730
            } elseif ($fieldConfig['type'] == 'set' && is_array($value)) {
1731
                $set[] = sprintf('`%s` = "%s"', $fieldConfig['columnName'], DB::escape(join(',', $value)));
1732
            } elseif ($fieldConfig['type'] == 'boolean') {
1733
                $set[] = sprintf('`%s` = %u', $fieldConfig['columnName'], $value ? 1 : 0);
1734
            } else {
1735
                $set[] = sprintf('`%s` = "%s"', $fieldConfig['columnName'], DB::escape($value));
1736
            }
1737
        }
1738
1739
        return $set;
1740
    }
1741
1742
    protected static function _mapFieldOrder($order)
1743
    {
1744
        if (is_string($order)) {
1745
            return [$order];
1746
        } elseif (is_array($order)) {
1747
            $r = [];
1748
            
1749
            foreach ($order as $key => $value) {
1750
                if (is_string($key)) {
1751
                    $columnName = static::_cn($key);
1752
                    $direction = strtoupper($value)=='DESC' ? 'DESC' : 'ASC';
1753
                } else {
1754
                    $columnName = static::_cn($value);
1755
                    $direction = 'ASC';
1756
                }
1757
                
1758
                $r[] = sprintf('`%s` %s', $columnName, $direction);
1759
            }
1760
            
1761
            return $r;
1762
        }
1763
    }
1764
    
1765
    protected static function _mapConditions($conditions)
1766
    {
1767
        foreach ($conditions as $field => &$condition) {
1768
            if (is_string($field)) {
1769
                $fieldOptions = static::$_classFields[get_called_class()][$field];
1770
            
1771
                if ($condition === null || ($condition == '' && $fieldOptions['blankisnull'])) {
1772
                    $condition = sprintf('`%s` IS NULL', static::_cn($field));
1773
                } elseif (is_array($condition)) {
1774
                    $condition = sprintf('`%s` %s "%s"', static::_cn($field), $condition['operator'], DB::escape($condition['value']));
1775
                } else {
1776
                    $condition = sprintf('`%s` = "%s"', static::_cn($field), DB::escape($condition));
1777
                }
1778
            }
1779
        }
1780
        
1781
        return $conditions;
1782
    }
1783
    
1784
    protected function finishValidation()
1785
    {
1786
        $this->_isValid = $this->_isValid && !$this->_validator->hasErrors();
1787
        
1788
        if (!$this->_isValid) {
1789
            $this->_validationErrors = array_merge($this->_validationErrors, $this->_validator->getErrors());
1790
        }
1791
1792
        return $this->_isValid;
1793
    }
1794
}
1795