Passed
Push — master ( fae027...8f7878 )
by Henry
01:46
created

ActiveRecord::buildHaving()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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

1172
            static::$_classFields[$className] = array_merge(static::$_classFields[$className], /** @scrutinizer ignore-type */ static::$versioningFields);
Loading history...
1173
        }
1174
    }
1175
1176
    
1177
    /**
1178
     * Called after _defineFields to initialize and apply defaults to the fields property
1179
     * Must be idempotent as it may be applied multiple times up the inheritence chain
1180
     */
1181
    protected static function _initFields()
1182
    {
1183
        $className = get_called_class();
1184
        $optionsMask = [
1185
            'type' => null,
1186
            'length' => null,
1187
            'primary' => null,
1188
            'unique' => null,
1189
            'autoincrement' => null,
1190
            'notnull' => null,
1191
            'unsigned' => null,
1192
            'default' => null,
1193
            'values' => null,
1194
        ];
1195
        
1196
        // apply default values to field definitions
1197
        if (!empty(static::$_classFields[$className])) {
1198
            $fields = [];
1199
            
1200
            foreach (static::$_classFields[$className] as $field => $options) {
1201
                if (is_string($field)) {
1202
                    if (is_array($options)) {
1203
                        $fields[$field] = array_merge($optionsMask, static::$fieldDefaults, ['columnName' => $field], $options);
1204
                    } elseif (is_string($options)) {
1205
                        $fields[$field] = array_merge($optionsMask, static::$fieldDefaults, ['columnName' => $field, 'type' => $options]);
1206
                    } elseif ($options == null) {
1207
                        continue;
1208
                    }
1209
                } elseif (is_string($options)) {
1210
                    $field = $options;
1211
                    $fields[$field] = array_merge($optionsMask, static::$fieldDefaults, ['columnName' => $field]);
1212
                }
1213
                
1214
                if ($field == 'Class') {
1215
                    // apply Class enum values
1216
                    $fields[$field]['values'] = static::$subClasses;
1217
                }
1218
                
1219
                if (!isset($fields[$field]['blankisnull']) && empty($fields[$field]['notnull'])) {
1220
                    $fields[$field]['blankisnull'] = true;
1221
                }
1222
                
1223
                if ($fields[$field]['autoincrement']) {
1224
                    $fields[$field]['primary'] = true;
1225
                }
1226
            }
1227
            
1228
            static::$_classFields[$className] = $fields;
1229
        }
1230
    }
1231
1232
1233
    /**
1234
     * Returns class name for instantiating given record
1235
     * @param array $record record
1236
     * @return string class name
1237
     */
1238
    protected static function _getRecordClass($record)
1239
    {
1240
        $static = get_called_class();
1241
        
1242
        if (!static::fieldExists('Class')) {
1243
            return $static;
1244
        }
1245
        
1246
        $columnName = static::_cn('Class');
1247
        
1248
        if (!empty($record[$columnName]) && is_subclass_of($record[$columnName], $static)) {
1249
            return $record[$columnName];
1250
        } else {
1251
            return $static;
1252
        }
1253
    }
1254
    
1255
    /**
1256
     * Shorthand alias for _getColumnName
1257
     * @param string $field name of field
1258
     * @return string column name
1259
     */
1260
    protected static function _cn($field)
1261
    {
1262
        return static::getColumnName($field);
1263
    }
1264
1265
    
1266
    /**
1267
     * Retrieves given field's value
1268
     * @param string $field Name of field
1269
     * @return mixed value
1270
     */
1271
    protected function _getFieldValue($field, $useDefault = true)
1272
    {
1273
        $fieldOptions = static::$_classFields[get_called_class()][$field];
1274
    
1275
        if (isset($this->_record[$fieldOptions['columnName']])) {
1276
            $value = $this->_record[$fieldOptions['columnName']];
1277
            
1278
            // apply type-dependent transformations
1279
            switch ($fieldOptions['type']) {
1280
                case 'password':
1281
                {
1282
                    return $value;
1283
                }
1284
                
1285
                case 'timestamp':
1286
                {
1287
                    if (!isset($this->_convertedValues[$field])) {
1288
                        if ($value && $value != '0000-00-00 00:00:00') {
1289
                            $this->_convertedValues[$field] = strtotime($value);
1290
                        } else {
1291
                            $this->_convertedValues[$field] = null;
1292
                        }
1293
                    }
1294
                    
1295
                    return $this->_convertedValues[$field];
1296
                }
1297
                case 'serialized':
1298
                {
1299
                    if (!isset($this->_convertedValues[$field])) {
1300
                        $this->_convertedValues[$field] = is_string($value) ? unserialize($value) : $value;
1301
                    }
1302
                    
1303
                    return $this->_convertedValues[$field];
1304
                }
1305
                case 'set':
1306
                case 'list':
1307
                {
1308
                    if (!isset($this->_convertedValues[$field])) {
1309
                        $delim = empty($fieldOptions['delimiter']) ? ',' : $fieldOptions['delimiter'];
1310
                        $this->_convertedValues[$field] = array_filter(preg_split('/\s*'.$delim.'\s*/', $value));
1311
                    }
1312
                    
1313
                    return $this->_convertedValues[$field];
1314
                }
1315
                
1316
                case 'boolean':
1317
                {
1318
                    if (!isset($this->_convertedValues[$field])) {
1319
                        $this->_convertedValues[$field] = (boolean)$value;
1320
                    }
1321
                    
1322
                    return $this->_convertedValues[$field];
1323
                }
1324
                
1325
                default:
1326
                {
1327
                    return $value;
1328
                }
1329
            }
1330
        } elseif ($useDefault && isset($fieldOptions['default'])) {
1331
            // return default
1332
            return $fieldOptions['default'];
1333
        } else {
1334
            switch ($fieldOptions['type']) {
1335
                case 'set':
1336
                case 'list':
1337
                {
1338
                    return [];
1339
                }
1340
                default:
1341
                {
1342
                    return null;
1343
                }
1344
            }
1345
        }
1346
    }
1347
    
1348
    protected function _setStringValue($fieldOptions, $value)
1349
    {
1350
        if (!$fieldOptions['notnull'] && $fieldOptions['blankisnull'] && ($value === '' || $value === null)) {
1351
            return null;
1352
        }
1353
        return @mb_convert_encoding($value, DB::$encoding, 'auto'); // normalize encoding to ASCII
1354
    }
1355
1356
    protected function _setBooleanValue($value)
1357
    {
1358
        return (boolean)$value;
1359
    }
1360
1361
    protected function _setDecimalValue($value)
1362
    {
1363
        return preg_replace('/[^-\d.]/', '', $value);
1364
    }
1365
1366
    protected function _setIntegerValue($fieldOptions, $value)
1367
    {
1368
        $value = preg_replace('/[^-\d]/', '', $value);
1369
        if (!$fieldOptions['notnull'] && $value === '') {
1370
            return null;
1371
        }
1372
        return $value;
1373
    }
1374
1375
    protected function _setTimestampValue($value)
1376
    {
1377
        if (is_numeric($value)) {
1378
            return date('Y-m-d H:i:s', $value);
1379
        } elseif (is_string($value)) {
1380
            // trim any extra crap, or leave as-is if it doesn't fit the pattern
1381
            if (preg_match('/^(\d{4})\D?(\d{2})\D?(\d{2})T?(\d{2})\D?(\d{2})\D?(\d{2})/')) {
1382
                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);
1383
            } else {
1384
                return date('Y-m-d H:i:s', strtotime($value));
1385
            }
1386
        }
1387
        return null;
1388
    }
1389
1390
    protected function _setSerializedValue($value)
1391
    {
1392
        return serialize($value);
1393
    }
1394
1395
    protected function _setEnumValue($fieldOptions, $value)
1396
    {
1397
        return (in_array($value, $fieldOptions['values']) ? $value : null);
1398
    }
1399
1400
    protected function _setListValue($fieldOptions, $value)
1401
    {
1402
        if (!is_array($value)) {
1403
            $delim = empty($fieldOptions['delimiter']) ? ',' : $fieldOptions['delimiter'];
1404
            $value = array_filter(preg_split('/\s*'.$delim.'\s*/', $value));
1405
        }
1406
        return $value;
1407
    }
1408
1409
    /**
1410
     * Sets given field's value
1411
     * @param string $field Name of field
1412
     * @param mixed $value New value
1413
     * @return mixed value
1414
     */
1415
    protected function _setFieldValue($field, $value)
1416
    {
1417
        // ignore setting versioning fields
1418
        if (static::isVersioned()) {
1419
            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

1419
            if (array_key_exists($field, /** @scrutinizer ignore-type */ static::$versioningFields)) {
Loading history...
1420
                return false;
1421
            }
1422
        }
1423
        
1424
        if (!static::fieldExists($field)) {
1425
            return false;
1426
        }
1427
        $fieldOptions = static::$_classFields[get_called_class()][$field];
1428
1429
        // no overriding autoincrements
1430
        if ($fieldOptions['autoincrement']) {
1431
            return false;
1432
        }
1433
1434
        // pre-process value
1435
        $forceDirty = false;
1436
        switch ($fieldOptions['type']) {
1437
            case 'clob':
1438
            case 'string':
1439
            {
1440
                $value = $this->_setStringValue($fieldOptions, $value);
1441
                break;
1442
            }
1443
            
1444
            case 'boolean':
1445
            {
1446
                $value = $this->_setBooleanValue($value);
1447
                break;
1448
            }
1449
            
1450
            case 'decimal':
1451
            {
1452
                $value = $this->_setDecimalValue($value);
1453
                break;
1454
            }
1455
            
1456
            case 'int':
1457
            case 'uint':
1458
            case 'integer':
1459
            {
1460
                $value = $this->_setIntegerValue($fieldOptions, $value);
1461
                break;
1462
            }
1463
            
1464
            case 'timestamp':
1465
            {
1466
                $value = $this->_setTimestampValue($value);
1467
                break;
1468
            }
1469
            
1470
            case 'date':
1471
            {
1472
                $value = $this->_setDateValue($value);
1473
                break;
1474
            }
1475
            
1476
            // these types are converted to strings from another PHP type on save
1477
            case 'serialized':
1478
            {
1479
                $this->_convertedValues[$field] = $value;
1480
                $value = $this->_setSerializedValue($value);
1481
                break;
1482
            }
1483
            case 'enum':
1484
            {
1485
                $value = $this->_setEnumValue($fieldOptions, $value);
1486
                break;
1487
            }
1488
            case 'set':
1489
            case 'list':
1490
            {
1491
                $value = $this->_setListValue($fieldOptions, $value);
1492
                $this->_convertedValues[$field] = $value;
1493
                $forceDirty = true;
1494
                break;
1495
            }
1496
        }
1497
        
1498
        if ($forceDirty || ($this->_record[$field] !== $value)) {
1499
            $columnName = static::_cn($field);
1500
            if (isset($this->_record[$columnName])) {
1501
                $this->_originalValues[$field] = $this->_record[$columnName];
1502
            }
1503
            $this->_record[$columnName] = $value;
1504
            $this->_isDirty = true;
1505
            
1506
            // unset invalidated relationships
1507
            if (!empty($fieldOptions['relationships']) && static::isRelational()) {
1508
                foreach ($fieldOptions['relationships'] as $relationship => $isCached) {
1509
                    if ($isCached) {
1510
                        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...
1511
                    }
1512
                }
1513
            }
1514
            return true;
1515
        } else {
1516
            return false;
1517
        }
1518
    }
1519
1520
    protected function _prepareRecordValues()
1521
    {
1522
        $record = [];
1523
1524
        foreach (static::$_classFields[get_called_class()] as $field => $options) {
1525
            $columnName = static::_cn($field);
1526
            
1527
            if (array_key_exists($columnName, $this->_record)) {
1528
                $value = $this->_record[$columnName];
1529
                
1530
                if (!$value && !empty($options['blankisnull'])) {
1531
                    $value = null;
1532
                }
1533
            } elseif (isset($options['default'])) {
1534
                $value = $options['default'];
1535
            } else {
1536
                continue;
1537
            }
1538
1539
            if (($options['type'] == 'date') && ($value == '0000-00-00') && !empty($options['blankisnull'])) {
1540
                $value = null;
1541
            }
1542
            if (($options['type'] == 'timestamp')) {
1543
                if (is_numeric($value)) {
1544
                    $value = date('Y-m-d H:i:s', $value);
1545
                } elseif ($value == null && !$options['notnull']) {
1546
                    $value = null;
1547
                }
1548
            }
1549
1550
            if (($options['type'] == 'serialized') && !is_string($value)) {
1551
                $value = serialize($value);
1552
            }
1553
            
1554
            if (($options['type'] == 'list') && is_array($value)) {
1555
                $delim = empty($options['delimiter']) ? ',' : $options['delimiter'];
1556
                $value = implode($delim, $value);
1557
            }
1558
            
1559
            $record[$field] = $value;
1560
        }
1561
1562
        return $record;
1563
    }
1564
    
1565
    protected static function _mapValuesToSet($recordValues)
1566
    {
1567
        $set = [];
1568
1569
        foreach ($recordValues as $field => $value) {
1570
            $fieldConfig = static::$_classFields[get_called_class()][$field];
1571
            
1572
            if ($value === null) {
1573
                $set[] = sprintf('`%s` = NULL', $fieldConfig['columnName']);
1574
            } elseif ($fieldConfig['type'] == 'timestamp' && $value == 'CURRENT_TIMESTAMP') {
1575
                $set[] = sprintf('`%s` = CURRENT_TIMESTAMP', $fieldConfig['columnName']);
1576
            } elseif ($fieldConfig['type'] == 'set' && is_array($value)) {
1577
                $set[] = sprintf('`%s` = "%s"', $fieldConfig['columnName'], DB::escape(join(',', $value)));
1578
            } elseif ($fieldConfig['type'] == 'boolean') {
1579
                $set[] = sprintf('`%s` = %u', $fieldConfig['columnName'], $value ? 1 : 0);
1580
            } else {
1581
                $set[] = sprintf('`%s` = "%s"', $fieldConfig['columnName'], DB::escape($value));
1582
            }
1583
        }
1584
1585
        return $set;
1586
    }
1587
1588
    protected static function _mapFieldOrder($order)
1589
    {
1590
        if (is_string($order)) {
1591
            return [$order];
1592
        } elseif (is_array($order)) {
1593
            $r = [];
1594
            
1595
            foreach ($order as $key => $value) {
1596
                if (is_string($key)) {
1597
                    $columnName = static::_cn($key);
1598
                    $direction = strtoupper($value)=='DESC' ? 'DESC' : 'ASC';
1599
                } else {
1600
                    $columnName = static::_cn($value);
1601
                    $direction = 'ASC';
1602
                }
1603
                
1604
                $r[] = sprintf('`%s` %s', $columnName, $direction);
1605
            }
1606
            
1607
            return $r;
1608
        }
1609
    }
1610
    
1611
    protected static function _mapConditions($conditions)
1612
    {
1613
        foreach ($conditions as $field => &$condition) {
1614
            if (is_string($field)) {
1615
                $fieldOptions = static::$_classFields[get_called_class()][$field];
1616
            
1617
                if ($condition === null || ($condition == '' && $fieldOptions['blankisnull'])) {
1618
                    $condition = sprintf('`%s` IS NULL', static::_cn($field));
1619
                } elseif (is_array($condition)) {
1620
                    $condition = sprintf('`%s` %s "%s"', static::_cn($field), $condition['operator'], DB::escape($condition['value']));
1621
                } else {
1622
                    $condition = sprintf('`%s` = "%s"', static::_cn($field), DB::escape($condition));
1623
                }
1624
            }
1625
        }
1626
        
1627
        return $conditions;
1628
    }
1629
    
1630
    protected function finishValidation()
1631
    {
1632
        $this->_isValid = $this->_isValid && !$this->_validator->hasErrors();
1633
        
1634
        if (!$this->_isValid) {
1635
            $this->_validationErrors = array_merge($this->_validationErrors, $this->_validator->getErrors());
1636
        }
1637
1638
        return $this->_isValid;
1639
    }
1640
}
1641