Passed
Push — master ( 401f4c...1b4133 )
by Henry
01:41
created

ActiveRecord::setField()   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 2
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
 * (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
     * Field definitions
77
     * @var array
78
     */
79
    public static $fields = [];
80
    
81
    /**
82
     * Index definitions
83
     * @var array
84
     */
85
    public static $indexes = [];
86
    
87
    
88
    /**
89
    * Validation checks
90
    * @var array
91
    */
92
    public static $validators = [];
93
94
    /**
95
     * Relationship definitions
96
     * @var array
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
     * Default conditions for get* operations
109
     * @var array
110
     */
111
    public static $defaultConditions = [];
112
    
113
    public static $primaryKey = null;
114
    public static $handleField = 'Handle';
115
    
116
    // support subclassing
117
    public static $rootClass = null;
118
    public static $defaultClass = null;
119
    public static $subClasses = [];
120
    
121
    // versioning
122
    public static $historyTable;
123
    public static $createRevisionOnDestroy = true;
124
    public static $createRevisionOnSave = true;
125
    
126
    // callbacks
127
    public static $beforeSave;
128
    public static $afterSave;
129
130
    // protected members
131
    protected static $_classFields = [];
132
    protected static $_classRelationships = [];
133
    protected static $_classBeforeSave = [];
134
    protected static $_classAfterSave = [];
135
    
136
    // class members subclassing
137
    protected static $_fieldsDefined = [];
138
    protected static $_relationshipsDefined = [];
139
    protected static $_eventsDefined = [];
140
    
141
    protected $_record;
142
    protected $_convertedValues;
143
    protected $_relatedObjects;
144
    protected $_isDirty;
145
    protected $_isPhantom;
146
    protected $_wasPhantom;
147
    protected $_isValid;
148
    protected $_isNew;
149
    protected $_isUpdated;
150
    protected $_validator;
151
    protected $_validationErrors;
152
    protected $_originalValues;
153
154
    /*
155
     *  @return ActiveRecord    Instance of the value of $this->Class
156
     */
157
    public function __construct($record = [], $isDirty = false, $isPhantom = null)
158
    {
159
        $this->_record = $record;
160
        $this->_relatedObjects = [];
161
        $this->_isPhantom = isset($isPhantom) ? $isPhantom : empty($record);
162
        $this->_wasPhantom = $this->_isPhantom;
163
        $this->_isDirty = $this->_isPhantom || $isDirty;
164
        $this->_isNew = false;
165
        $this->_isUpdated = false;
166
        
167
        $this->_isValid = true;
168
        $this->_validationErrors = [];
169
        $this->_originalValues = [];
170
171
        static::init();
172
        
173
        // set Class
174
        if (static::fieldExists('Class') && !$this->Class) {
175
            $this->Class = get_class($this);
176
        }
177
    }
178
    
179
    public function __get($name)
180
    {
181
        return $this->getValue($name);
182
    }
183
    
184
    public function __set($name, $value)
185
    {
186
        return $this->setValue($name, $value);
187
    }
188
    
189
    public function __isset($name)
190
    {
191
        $value = $this->getValue($name);
192
        return isset($value);
193
    }
194
    
195
    public function getPrimaryKey()
196
    {
197
        return isset(static::$primaryKey) ? static::$primaryKey : 'ID';
198
    }
199
200
    public function getPrimaryKeyValue()
201
    {
202
        if (isset(static::$primaryKey)) {
203
            return $this->__get(static::$primaryKey);
204
        } else {
205
            return $this->ID;
206
        }
207
    }
208
    
209
    public static function init()
210
    {
211
        $className = get_called_class();
212
        if (!static::$_fieldsDefined[$className]) {
213
            static::_defineFields();
214
            static::_initFields();
215
            
216
            static::$_fieldsDefined[$className] = true;
217
        }
218
        if (!static::$_relationshipsDefined[$className] && static::isRelational()) {
219
            static::_defineRelationships();
220
            static::_initRelationships();
221
            
222
            static::$_relationshipsDefined[$className] = true;
223
        }
224
        
225
        if (!static::$_eventsDefined[$className]) {
226
            static::_defineEvents();
227
            
228
            static::$_eventsDefined[$className] = true;
229
        }
230
    }
231
    
232
    /*
233
234
     */
235
    public function getValue($name)
236
    {
237
        switch ($name) {
238
            case 'isDirty':
239
                return $this->_isDirty;
240
                
241
            case 'isPhantom':
242
                return $this->_isPhantom;
243
244
            case 'wasPhantom':
245
                return $this->_wasPhantom;
246
                
247
            case 'isValid':
248
                return $this->_isValid;
249
                
250
            case 'isNew':
251
                return $this->_isNew;
252
                
253
            case 'isUpdated':
254
                return $this->_isUpdated;
255
                
256
            case 'validationErrors':
257
                return array_filter($this->_validationErrors);
258
                
259
            case 'data':
260
                return $this->getData();
261
                
262
            case 'originalValues':
263
                return $this->_originalValues;
264
                
265
            default:
266
            {
267
                // handle field
268
                if (static::fieldExists($name)) {
269
                    return $this->_getFieldValue($name);
270
                }
271
                // handle relationship
272
                elseif (static::isRelational()) {
273
                    if (static::_relationshipExists($name)) {
274
                        return $this->_getRelationshipValue($name);
275
                    }
276
                }
277
                // default Handle to ID if not caught by fieldExists
278
                elseif ($name == static::$handleField) {
279
                    return $this->ID;
280
                }
281
            }
282
        }
283
        // undefined
284
        return null;
285
    }
286
    
287
    public function setValue($name, $value)
288
    {
289
        // handle field
290
        if (static::fieldExists($name)) {
291
            $this->_setFieldValue($name, $value);
292
        }
293
        // undefined
294
        else {
295
            return false;
296
        }
297
    }
298
    
299
    public static function isVersioned()
300
    {
301
        return in_array('Divergence\\Models\\Versioning', class_uses(get_called_class()));
302
    }
303
    
304
    public static function isRelational()
305
    {
306
        return in_array('Divergence\\Models\\Relations', class_uses(get_called_class()));
307
    }
308
    
309
    public static function create($values = [], $save = false)
310
    {
311
        $className = get_called_class();
312
        
313
        // create class
314
        $ActiveRecord = new $className();
315
        $ActiveRecord->setFields($values);
316
        
317
        if ($save) {
318
            $ActiveRecord->save();
319
        }
320
        
321
        return $ActiveRecord;
322
    }
323
    
324
    
325
    public function isA($class)
326
    {
327
        return is_a($this, $class);
328
    }
329
    
330
    public function changeClass($className = false, $fieldValues = false)
331
    {
332
        if (!$className) {
333
            return $this;
334
        }
335
336
        $this->_record[static::_cn('Class')] = $className;
337
        $ActiveRecord = new $className($this->_record, true, $this->isPhantom);
338
        
339
        if ($fieldValues) {
340
            $ActiveRecord->setFields($fieldValues);
341
        }
342
        
343
        if (!$this->isPhantom) {
344
            $ActiveRecord->save();
345
        }
346
        
347
        return $ActiveRecord;
348
    }
349
    
350
    public function setFields($values)
351
    {
352
        foreach ($values as $field => $value) {
353
            $this->_setFieldValue($field, $value);
354
        }
355
    }
356
    
357
    public function setField($field, $value)
358
    {
359
        $this->_setFieldValue($field, $value);
360
    }
361
    
362
    public function getData()
363
    {
364
        $data = [];
365
        
366
        foreach (static::$_classFields[get_called_class()] as $field => $options) {
367
            $data[$field] = $this->_getFieldValue($field);
368
        }
369
        
370
        if ($this->validationErrors) {
371
            $data['validationErrors'] = $this->validationErrors;
372
        }
373
        
374
        return $data;
375
    }
376
    
377
    public function isFieldDirty($field)
378
    {
379
        return $this->isPhantom || array_key_exists($field, $this->_originalValues);
380
    }
381
    
382
    public function getOriginalValue($field)
383
    {
384
        return $this->_originalValues[$field];
385
    }
386
387
    public function clearCaches()
388
    {
389
        foreach ($this->getClassFields() as $field => $options) {
390
            if (!empty($options['unique']) || !empty($options['primary'])) {
391
                $key = sprintf('%s/%s', static::$tableName, $field);
392
                DB::clearCachedRecord($key);
393
            }
394
        }
395
    }
396
    
397
    public function beforeSave()
398
    {
399
        foreach (static::$_classBeforeSave as $beforeSave) {
400
            if (is_callable($beforeSave)) {
401
                $beforeSave($this);
402
            }
403
        }
404
    }
405
406
407
    public function afterSave()
408
    {
409
        foreach (static::$_classAfterSave as $afterSave) {
410
            if (is_callable($afterSave)) {
411
                $afterSave($this);
412
            }
413
        }
414
    }
415
416
    public function save($deep = true)
417
    {
418
        // run before save
419
        $this->beforeSave();
420
        
421
        if (static::isVersioned()) {
422
            $this->beforeVersionedSave();
423
        }
424
        
425
        // set created
426
        if (static::fieldExists('Created') && (!$this->Created || ($this->Created == 'CURRENT_TIMESTAMP'))) {
427
            $this->Created = time();
428
        }
429
        
430
        // validate
431
        if (!$this->validate($deep)) {
432
            throw new Exception('Cannot save invalid record');
433
        }
434
        
435
        $this->clearCaches();
436
437
        if ($this->isDirty) {
438
            // prepare record values
439
            $recordValues = $this->_prepareRecordValues();
440
    
441
            // transform record to set array
442
            $set = static::_mapValuesToSet($recordValues);
443
            
444
            // create new or update existing
445
            if ($this->_isPhantom) {
446
                DB::nonQuery(
447
                    'INSERT INTO `%s` SET %s',
448
                    [
449
                        static::$tableName,
450
                        join(',', $set),
451
                    ],
452
                    [static::class,'handleError']
453
                );
454
                
455
                $this->_record[static::$primaryKey ? static::$primaryKey : 'ID'] = DB::insertID();
456
                $this->_isPhantom = false;
457
                $this->_isNew = true;
458
            } elseif (count($set)) {
459
                DB::nonQuery(
460
                    'UPDATE `%s` SET %s WHERE `%s` = %u',
461
                    [
462
                        static::$tableName,
463
                        join(',', $set),
464
                        static::_cn(static::$primaryKey ? static::$primaryKey : 'ID'),
465
                        $this->getPrimaryKeyValue(),
466
                    ],
467
                    [static::class,'handleError']
468
                );
469
                
470
                $this->_isUpdated = true;
471
            }
472
            
473
            // update state
474
            $this->_isDirty = false;
475
            
476
            if (static::isVersioned()) {
477
                $this->afterVersionedSave();
478
            }
479
        }
480
        $this->afterSave();
481
    }
482
    
483
    
484
    public function destroy()
485
    {
486
        if (static::isVersioned()) {
487
            if (static::$createRevisionOnDestroy) {
488
                // save a copy to history table
489
                if ($this->fieldExists('Created')) {
490
                    $this->Created = time();
491
                }
492
                
493
                $recordValues = $this->_prepareRecordValues();
494
                $set = static::_mapValuesToSet($recordValues);
495
            
496
                DB::nonQuery(
497
                        'INSERT INTO `%s` SET %s',
498
            
499
                    [
500
                                static::getHistoryTable(),
501
                                join(',', $set),
502
                        ]
503
                );
504
            }
505
        }
506
        
507
        return static::delete($this->getPrimaryKeyValue());
508
    }
509
    
510
    public static function delete($id)
511
    {
512
        DB::nonQuery('DELETE FROM `%s` WHERE `%s` = %u', [
513
            static::$tableName,
514
            static::_cn(static::$primaryKey ? static::$primaryKey : 'ID'),
515
            $id,
516
        ], [static::class,'handleError']);
517
        
518
        return DB::affectedRows() > 0;
519
    }
520
    
521
    public static function getByContextObject(ActiveRecord $Record, $options = [])
522
    {
523
        return static::getByContext($Record::$rootClass, $Record->getPrimaryKeyValue(), $options);
524
    }
525
    
526
    public static function getByContext($contextClass, $contextID, $options = [])
527
    {
528
        if (!static::fieldExists('ContextClass')) {
529
            throw new Exception('getByContext requires the field ContextClass to be defined');
530
        }
531
532
        $options = Util::prepareOptions($options, [
533
            'conditions' => [],
534
            'order' => false,
535
        ]);
536
        
537
        $options['conditions']['ContextClass'] = $contextClass;
538
        $options['conditions']['ContextID'] = $contextID;
539
    
540
        $record = static::getRecordByWhere($options['conditions'], $options);
541
542
        $className = static::_getRecordClass($record);
543
        
544
        return $record ? new $className($record) : null;
545
    }
546
    
547
    public static function getByHandle($handle)
548
    {
549
        if (static::fieldExists(static::$handleField)) {
550
            if ($Record = static::getByField(static::$handleField, $handle)) {
551
                return $Record;
552
            }
553
        }
554
        return static::getByID($handle);
555
    }
556
    
557
    public static function getByID($id)
558
    {
559
        $record = static::getRecordByField(static::$primaryKey ? static::$primaryKey : 'ID', $id, true);
560
        
561
        return static::instantiateRecord($record);
562
    }
563
        
564
    public static function getByField($field, $value, $cacheIndex = false)
565
    {
566
        $record = static::getRecordByField($field, $value, $cacheIndex);
567
        
568
        return static::instantiateRecord($record);
569
    }
570
    
571
    public static function getRecordByField($field, $value, $cacheIndex = false)
572
    {
573
        $query = 'SELECT * FROM `%s` WHERE `%s` = "%s" LIMIT 1';
574
        $params = [
575
            static::$tableName,
576
            static::_cn($field),
577
            DB::escape($value),
578
        ];
579
    
580
        if ($cacheIndex) {
581
            $key = sprintf('%s/%s:%s', static::$tableName, $field, $value);
582
            return DB::oneRecordCached($key, $query, $params, [static::class,'handleError']);
583
        } else {
584
            return DB::oneRecord($query, $params, [static::class,'handleError']);
585
        }
586
    }
587
    
588
    public static function getByWhere($conditions, $options = [])
589
    {
590
        $record = static::getRecordByWhere($conditions, $options);
591
        
592
        return static::instantiateRecord($record);
593
    }
594
    
595
    public static function getRecordByWhere($conditions, $options = [])
596
    {
597
        if (!is_array($conditions)) {
598
            $conditions = [$conditions];
599
        }
600
        
601
        $options = Util::prepareOptions($options, [
602
            'order' => false,
603
        ]);
604
605
        // initialize conditions and order
606
        $conditions = static::_mapConditions($conditions);
607
        $order = $options['order'] ? static::_mapFieldOrder($options['order']) : [];
608
        
609
        return DB::oneRecord(
610
            'SELECT * FROM `%s` WHERE (%s) %s LIMIT 1',
611
        
612
            [
613
                static::$tableName,
614
                join(') AND (', $conditions),
615
                $order ? 'ORDER BY '.join(',', $order) : '',
616
            ],
617
        
618
            [static::class,'handleError']
619
        );
620
    }
621
    
622
    public static function getByQuery($query, $params = [])
623
    {
624
        return static::instantiateRecord(DB::oneRecord($query, $params, [static::class,'handleError']));
625
    }
626
627
    public static function getAllByClass($className = false, $options = [])
628
    {
629
        return static::getAllByField('Class', $className ? $className : get_called_class(), $options);
630
    }
631
    
632
    public static function getAllByContextObject(ActiveRecord $Record, $options = [])
633
    {
634
        return static::getAllByContext($Record::$rootClass, $Record->getPrimaryKeyValue(), $options);
635
    }
636
637
    public static function getAllByContext($contextClass, $contextID, $options = [])
638
    {
639
        if (!static::fieldExists('ContextClass')) {
640
            throw new Exception('getByContext requires the field ContextClass to be defined');
641
        }
642
643
        $options = Util::prepareOptions($options, [
644
            'conditions' => [],
645
        ]);
646
        
647
        $options['conditions']['ContextClass'] = $contextClass;
648
        $options['conditions']['ContextID'] = $contextID;
649
    
650
        return static::instantiateRecords(static::getAllRecordsByWhere($options['conditions'], $options));
651
    }
652
    
653
    public static function getAllByField($field, $value, $options = [])
654
    {
655
        return static::getAllByWhere([$field => $value], $options);
656
    }
657
        
658
    public static function getAllByWhere($conditions = [], $options = [])
659
    {
660
        return static::instantiateRecords(static::getAllRecordsByWhere($conditions, $options));
661
    }
662
    
663
    public static function getAllRecordsByWhere($conditions = [], $options = [])
664
    {
665
        $className = get_called_class();
666
    
667
        $options = Util::prepareOptions($options, [
668
            'indexField' => false,
669
            'order' => false,
670
            'limit' => false,
671
            'offset' => 0,
672
            'calcFoundRows' => !empty($options['limit']),
673
            'joinRelated' => false,
674
            'extraColumns' => false,
675
            'having' => false,
676
        ]);
677
678
        $join = '';
679
        
680
        // handle joining related tables
681
        if (static::isRelational()) {
682
            $join = '';
683
            if ($options['joinRelated']) {
684
                if (is_string($options['joinRelated'])) {
685
                    $options['joinRelated'] = [$options['joinRelated']];
686
                }
687
                
688
                // prefix any conditions
689
                
690
                foreach ($options['joinRelated'] as $relationship) {
691
                    if (!$rel = static::$_classRelationships[get_called_class()][$relationship]) {
692
                        throw new Exception("joinRelated specifies a relationship that does not exist: $relationship");
693
                    }
694
                                    
695
                    switch ($rel['type']) {
696
                        case 'one-one':
697
                        {
698
                            $join .= sprintf(' JOIN `%1$s` AS `%2$s` ON(`%2$s`.`%3$s` = `%4$s`)', $rel['class']::$tableName, $relationship::$rootClass, $rel['foreign'], $rel['local']);
699
                            break;
700
                        }
701
                        default:
702
                        {
703
                            throw new Exception("getAllRecordsByWhere does not support relationship type $rel[type]");
704
                        }
705
                    }
706
                }
707
            }
708
        } // isRelational
709
        
710
        // initialize conditions
711
        if ($conditions) {
712
            if (is_string($conditions)) {
713
                $conditions = [$conditions];
714
            }
715
        
716
            $conditions = static::_mapConditions($conditions);
717
        }
718
        
719
        // build query
720
        $query  = 'SELECT %1$s `%3$s`.*';
721
        
722
        if (!empty($options['extraColumns'])) {
723
            if (is_array($options['extraColumns'])) {
724
                foreach ($options['extraColumns'] as $key => $value) {
725
                    $query .= ', '.$value.' AS '.$key;
726
                }
727
            } else {
728
                $query .= ', ' . $options['extraColumns'];
729
            }
730
        }
731
        $query .= ' FROM `%2$s` AS `%3$s` %4$s';
732
        $query .= ' WHERE (%5$s)';
733
        
734
        if (!empty($options['having'])) {
735
            $query .= ' HAVING (' . (is_array($options['having']) ? join(') AND (', static::_mapConditions($options['having'])) : $options['having']) . ')';
736
        }
737
        
738
        $params = [
739
            $options['calcFoundRows'] ? 'SQL_CALC_FOUND_ROWS' : '',
740
            static::$tableName,
741
            $className::$rootClass,
742
            $join,
743
            $conditions ? join(') AND (', $conditions) : '1',
744
        ];
745
        
746
        
747
748
        if ($options['order']) {
749
            $query .= ' ORDER BY ' . join(',', static::_mapFieldOrder($options['order']));
750
        }
751
        
752
        if ($options['limit']) {
753
            $query .= sprintf(' LIMIT %u,%u', $options['offset'], $options['limit']);
754
        }
755
        
756
        if ($options['indexField']) {
757
            return DB::table(static::_cn($options['indexField']), $query, $params, [static::class,'handleError']);
758
        } else {
759
            return DB::allRecords($query, $params, [static::class,'handleError']);
760
        }
761
    }
762
    
763
    public static function getAll($options = [])
764
    {
765
        return static::instantiateRecords(static::getAllRecords($options));
766
    }
767
    
768
    public static function getAllRecords($options = [])
769
    {
770
        $options = Util::prepareOptions($options, [
771
            'indexField' => false,
772
            'order' => false,
773
            'limit' => false,
774
            'calcFoundRows' => false,
775
            'offset' => 0,
776
        ]);
777
        
778
        $query = 'SELECT '.($options['calcFoundRows'] ? 'SQL_CALC_FOUND_ROWS' : '').'* FROM `%s`';
779
        $params = [
780
            static::$tableName,
781
        ];
782
        
783
        if ($options['order']) {
784
            $query .= ' ORDER BY ' . join(',', static::_mapFieldOrder($options['order']));
785
        }
786
        
787
        if ($options['limit']) {
788
            $query .= sprintf(' LIMIT %u,%u', $options['offset'], $options['limit']);
789
        }
790
791
        if ($options['indexField']) {
792
            return DB::table(static::_cn($options['indexField']), $query, $params, [static::class,'handleError']);
793
        } else {
794
            return DB::allRecords($query, $params, [static::class,'handleError']);
795
        }
796
    }
797
    
798
    public static function getAllByQuery($query, $params = [])
799
    {
800
        return static::instantiateRecords(DB::allRecords($query, $params, [static::class,'handleError']));
801
    }
802
803
    public static function getTableByQuery($keyField, $query, $params = [])
804
    {
805
        return static::instantiateRecords(DB::table($keyField, $query, $params, [static::class,'handleError']));
806
    }
807
808
    
809
    
810
    public static function instantiateRecord($record)
811
    {
812
        $className = static::_getRecordClass($record);
813
        return $record ? new $className($record) : null;
814
    }
815
    
816
    public static function instantiateRecords($records)
817
    {
818
        foreach ($records as &$record) {
819
            $className = static::_getRecordClass($record);
820
            $record = new $className($record);
821
        }
822
        
823
        return $records;
824
    }
825
    
826
    public static function getUniqueHandle($text, $options = [])
827
    {
828
        // apply default options
829
        $options = Util::prepareOptions($options, [
830
            'handleField' => static::$handleField,
831
            'domainConstraints' => [],
832
            'alwaysSuffix' => false,
833
            'format' => '%s:%u',
834
        ]);
835
        
836
        // transliterate accented characters
837
        $text = iconv('UTF-8', 'ASCII//TRANSLIT', $text);
838
    
839
        // strip bad characters
840
        $handle = $strippedText = preg_replace(
841
            ['/\s+/', '/_*[^a-zA-Z0-9\-_:]+_*/', '/:[-_]/', '/^[-_]+/', '/[-_]+$/'],
842
            ['_', '-', ':', '', ''],
843
            trim($text)
844
        );
845
        
846
        $handle = trim($handle, '-_');
847
        
848
        $incarnation = 0;
849
        do {
850
            // TODO: check for repeat posting here?
851
            $incarnation++;
852
            
853
            if ($options['alwaysSuffix'] || $incarnation > 1) {
854
                $handle = sprintf($options['format'], $strippedText, $incarnation);
855
            }
856
        } while (static::getByWhere(array_merge($options['domainConstraints'], [$options['handleField']=>$handle])));
857
        
858
        return $handle;
859
    }
860
    
861
862
    // TODO: make the handleField
863
    public static function generateRandomHandle($length = 32)
864
    {
865
        do {
866
            $handle = substr(md5(mt_rand(0, mt_getrandmax())), 0, $length);
867
        } while (static::getByField(static::$handleField, $handle));
868
        
869
        return $handle;
870
    }
871
    
872
    public static function fieldExists($field)
873
    {
874
        static::init();
875
        return array_key_exists($field, static::$_classFields[get_called_class()]);
876
    }
877
    
878
    
879
    public static function getClassFields()
880
    {
881
        static::init();
882
        return static::$_classFields[get_called_class()];
883
    }
884
    
885
    public static function getFieldOptions($field, $optionKey = false)
886
    {
887
        if ($optionKey) {
888
            return static::$_classFields[get_called_class()][$field][$optionKey];
889
        } else {
890
            return static::$_classFields[get_called_class()][$field];
891
        }
892
    }
893
894
    /**
895
     * Returns columnName for given field
896
     * @param string $field name of field
897
     * @return string column name
898
     */
899
    public static function getColumnName($field)
900
    {
901
        static::init();
902
        if (!static::fieldExists($field)) {
903
            throw new Exception('getColumnName called on nonexisting column: ' . get_called_class().'->'.$field);
904
        }
905
        
906
        return static::$_classFields[get_called_class()][$field]['columnName'];
907
    }
908
    
909
    public static function mapFieldOrder($order)
910
    {
911
        return static::_mapFieldOrder($order);
912
    }
913
    
914
    public static function mapConditions($conditions)
915
    {
916
        return static::_mapConditions($conditions);
917
    }
918
    
919
    public function getRootClass()
920
    {
921
        return static::$rootClass;
922
    }
923
    
924
    public function addValidationErrors($array)
925
    {
926
        foreach ($array as $field => $errorMessage) {
927
            $this->addValidationError($field, $errorMessage);
928
        }
929
    }
930
931
    public function addValidationError($field, $errorMessage)
932
    {
933
        $this->_isValid = false;
934
        $this->_validationErrors[$field] = $errorMessage;
935
    }
936
    
937
    public function getValidationError($field)
938
    {
939
        // break apart path
940
        $crumbs = explode('.', $field);
941
942
        // resolve path recursively
943
        $cur = &$this->_validationErrors;
944
        while ($crumb = array_shift($crumbs)) {
945
            if (array_key_exists($crumb, $cur)) {
946
                $cur = &$cur[$crumb];
947
            } else {
948
                return null;
949
            }
950
        }
951
952
        // return current value
953
        return $cur;
954
    }
955
    
956
    
957
    public function validate($deep = true)
958
    {
959
        $this->_isValid = true;
960
        $this->_validationErrors = [];
961
962
        if (!isset($this->_validator)) {
963
            $this->_validator = new RecordValidator($this->_record);
964
        } else {
965
            $this->_validator->resetErrors();
966
        }
967
968
        foreach (static::$validators as $validator) {
969
            $this->_validator->validate($validator);
970
        }
971
        
972
        $this->finishValidation();
973
974
        if ($deep) {
975
            // validate relationship objects
976
            foreach (static::$_classRelationships[get_called_class()] as $relationship => $options) {
977
                if (empty($this->_relatedObjects[$relationship])) {
978
                    continue;
979
                }
980
                
981
                
982
                if ($options['type'] == 'one-one') {
983
                    if ($this->_relatedObjects[$relationship]->isDirty) {
984
                        $this->_relatedObjects[$relationship]->validate();
985
                        $this->_isValid = $this->_isValid && $this->_relatedObjects[$relationship]->isValid;
986
                        $this->_validationErrors[$relationship] = $this->_relatedObjects[$relationship]->validationErrors;
987
                    }
988
                } elseif ($options['type'] == 'one-many') {
989
                    foreach ($this->_relatedObjects[$relationship] as $i => $object) {
990
                        if ($object->isDirty) {
991
                            $object->validate();
992
                            $this->_isValid = $this->_isValid && $object->isValid;
993
                            $this->_validationErrors[$relationship][$i] = $object->validationErrors;
994
                        }
995
                    }
996
                }
997
            }
998
        }
999
        
1000
        return $this->_isValid;
1001
    }
1002
    
1003
    public static function handleError($query = null, $queryLog = null, $parameters = null)
1004
    {
1005
        $Connection = DB::getConnection();
1006
        
1007
        if ($Connection->errorCode() == '42S02' && static::$autoCreateTables) {
1008
            $CreateTable = SQL::getCreateTable(static::$rootClass);
1009
            
1010
            // history versions table
1011
            if (static::isVersioned()) {
1012
                $CreateTable .= SQL::getCreateTable(static::$rootClass, true);
1013
            }
1014
            
1015
            $Statement = $Connection->query($CreateTable);
1016
            
1017
            // check for errors
1018
            $ErrorInfo = $Statement->errorInfo();
1019
        
1020
            // handle query error
1021
            if ($ErrorInfo[0] != '00000') {
1022
                self::handleError($query, $queryLog);
1023
            }
1024
            
1025
            // clear buffer (required for the next query to work without running fetchAll first
1026
            $Statement->closeCursor();
1027
            
1028
            return $Connection->query($query); // now the query should finish with no error
1029
        } else {
1030
            return DB::handleError($query, $queryLog);
1031
        }
1032
    }
1033
1034
    public function _setDateValue($value)
1035
    {
1036
        if (is_numeric($value)) {
1037
            $value = date('Y-m-d', $value);
1038
        } elseif (is_string($value)) {
1039
            // check if m/d/y format
1040
            if (preg_match('/^(\d{2})\D?(\d{2})\D?(\d{4}).*/', $value)) {
1041
                $value = preg_replace('/^(\d{2})\D?(\d{2})\D?(\d{4}).*/', '$3-$1-$2', $value);
1042
            }
1043
            
1044
            // trim time and any extra crap, or leave as-is if it doesn't fit the pattern
1045
            $value = preg_replace('/^(\d{4})\D?(\d{2})\D?(\d{2}).*/', '$1-$2-$3', $value);
1046
        } elseif (is_array($value) && count(array_filter($value))) {
1047
            // collapse array date to string
1048
            $value = sprintf(
1049
                '%04u-%02u-%02u',
1050
                is_numeric($value['yyyy']) ? $value['yyyy'] : 0,
1051
                is_numeric($value['mm']) ? $value['mm'] : 0,
1052
                is_numeric($value['dd']) ? $value['dd'] : 0
1053
            );
1054
        } else {
1055
            if ($value = strtotime($value)) {
1056
                $value = date('Y-m-d', $value) ?: null;
1057
            } else {
1058
                $value = null;
1059
            }
1060
        }
1061
        return $value;
1062
    }
1063
    
1064
    protected static function _defineEvents()
1065
    {
1066
        // run before save
1067
        $className = get_called_class();
1068
        
1069
        // merge fields from first ancestor up
1070
        $classes = class_parents($className);
1071
        array_unshift($classes, $className);
1072
    
1073
        while ($class = array_pop($classes)) {
1074
            if (is_callable($class::$beforeSave)) {
1075
                if (!empty($class::$beforeSave)) {
1076
                    if (!in_array($class::$beforeSave, static::$_classBeforeSave)) {
1077
                        static::$_classBeforeSave[] = $class::$beforeSave;
1078
                    }
1079
                }
1080
            }
1081
            
1082
            if (is_callable($class::$afterSave)) {
1083
                if (!empty($class::$afterSave)) {
1084
                    if (!in_array($class::$afterSave, static::$_classAfterSave)) {
1085
                        static::$_classAfterSave[] = $class::$afterSave;
1086
                    }
1087
                }
1088
            }
1089
        }
1090
    }
1091
    
1092
    /**
1093
     * Called when a class is loaded to define fields before _initFields
1094
     */
1095
    protected static function _defineFields()
1096
    {
1097
        $className = get_called_class();
1098
1099
        // skip if fields already defined
1100
        if (isset(static::$_classFields[$className])) {
1101
            return;
1102
        }
1103
        
1104
        // merge fields from first ancestor up
1105
        $classes = class_parents($className);
1106
        array_unshift($classes, $className);
1107
        
1108
        static::$_classFields[$className] = [];
1109
        while ($class = array_pop($classes)) {
1110
            if (!empty($class::$fields)) {
1111
                static::$_classFields[$className] = array_merge(static::$_classFields[$className], $class::$fields);
1112
            }
1113
        }
1114
        
1115
        // versioning
1116
        if (static::isVersioned()) {
1117
            static::$_classFields[$className] = array_merge(static::$_classFields[$className], static::$versioningFields);
1118
        }
1119
    }
1120
1121
    
1122
    /**
1123
     * Called after _defineFields to initialize and apply defaults to the fields property
1124
     * Must be idempotent as it may be applied multiple times up the inheritence chain
1125
     */
1126
    protected static function _initFields()
1127
    {
1128
        $className = get_called_class();
1129
        $optionsMask = [
1130
            'type' => null,
1131
            'length' => null,
1132
            'primary' => null,
1133
            'unique' => null,
1134
            'autoincrement' => null,
1135
            'notnull' => null,
1136
            'unsigned' => null,
1137
            'default' => null,
1138
            'values' => null,
1139
        ];
1140
        
1141
        // apply default values to field definitions
1142
        if (!empty(static::$_classFields[$className])) {
1143
            $fields = [];
1144
            
1145
            foreach (static::$_classFields[$className] as $field => $options) {
1146
                if (is_string($field)) {
1147
                    if (is_array($options)) {
1148
                        $fields[$field] = array_merge($optionsMask, static::$fieldDefaults, ['columnName' => $field], $options);
1149
                    } elseif (is_string($options)) {
1150
                        $fields[$field] = array_merge($optionsMask, static::$fieldDefaults, ['columnName' => $field, 'type' => $options]);
1151
                    } elseif ($options == null) {
1152
                        continue;
1153
                    }
1154
                } elseif (is_string($options)) {
1155
                    $field = $options;
1156
                    $fields[$field] = array_merge($optionsMask, static::$fieldDefaults, ['columnName' => $field]);
1157
                }
1158
                
1159
                if ($field == 'Class') {
1160
                    // apply Class enum values
1161
                    $fields[$field]['values'] = static::$subClasses;
1162
                }
1163
                
1164
                if (!isset($fields[$field]['blankisnull']) && empty($fields[$field]['notnull'])) {
1165
                    $fields[$field]['blankisnull'] = true;
1166
                }
1167
                
1168
                if ($fields[$field]['autoincrement']) {
1169
                    $fields[$field]['primary'] = true;
1170
                }
1171
            }
1172
            
1173
            static::$_classFields[$className] = $fields;
1174
        }
1175
    }
1176
1177
1178
    /**
1179
     * Returns class name for instantiating given record
1180
     * @param array $record record
1181
     * @return string class name
1182
     */
1183
    protected static function _getRecordClass($record)
1184
    {
1185
        $static = get_called_class();
1186
        
1187
        if (!static::fieldExists('Class')) {
1188
            return $static;
1189
        }
1190
        
1191
        $columnName = static::_cn('Class');
1192
        
1193
        if (!empty($record[$columnName]) && is_subclass_of($record[$columnName], $static)) {
1194
            return $record[$columnName];
1195
        } else {
1196
            return $static;
1197
        }
1198
    }
1199
    
1200
    /**
1201
     * Shorthand alias for _getColumnName
1202
     * @param string $field name of field
1203
     * @return string column name
1204
     */
1205
    protected static function _cn($field)
1206
    {
1207
        return static::getColumnName($field);
1208
    }
1209
1210
    
1211
    /**
1212
     * Retrieves given field's value
1213
     * @param string $field Name of field
1214
     * @return mixed value
1215
     */
1216
    protected function _getFieldValue($field, $useDefault = true)
1217
    {
1218
        $fieldOptions = static::$_classFields[get_called_class()][$field];
1219
    
1220
        if (isset($this->_record[$fieldOptions['columnName']])) {
1221
            $value = $this->_record[$fieldOptions['columnName']];
1222
            
1223
            // apply type-dependent transformations
1224
            switch ($fieldOptions['type']) {
1225
                case 'password':
1226
                {
1227
                    return $value;
1228
                }
1229
                
1230
                case 'timestamp':
1231
                {
1232
                    if (!isset($this->_convertedValues[$field])) {
1233
                        if ($value && $value != '0000-00-00 00:00:00') {
1234
                            $this->_convertedValues[$field] = strtotime($value);
1235
                        } else {
1236
                            $this->_convertedValues[$field] = null;
1237
                        }
1238
                    }
1239
                    
1240
                    return $this->_convertedValues[$field];
1241
                }
1242
                case 'serialized':
1243
                {
1244
                    if (!isset($this->_convertedValues[$field])) {
1245
                        $this->_convertedValues[$field] = is_string($value) ? unserialize($value) : $value;
1246
                    }
1247
                    
1248
                    return $this->_convertedValues[$field];
1249
                }
1250
                case 'set':
1251
                case 'list':
1252
                {
1253
                    if (!isset($this->_convertedValues[$field])) {
1254
                        $delim = empty($fieldOptions['delimiter']) ? ',' : $fieldOptions['delimiter'];
1255
                        $this->_convertedValues[$field] = array_filter(preg_split('/\s*'.$delim.'\s*/', $value));
1256
                    }
1257
                    
1258
                    return $this->_convertedValues[$field];
1259
                }
1260
                
1261
                case 'boolean':
1262
                {
1263
                    if (!isset($this->_convertedValues[$field])) {
1264
                        $this->_convertedValues[$field] = (boolean)$value;
1265
                    }
1266
                    
1267
                    return $this->_convertedValues[$field];
1268
                }
1269
                
1270
                default:
1271
                {
1272
                    return $value;
1273
                }
1274
            }
1275
        } elseif ($useDefault && isset($fieldOptions['default'])) {
1276
            // return default
1277
            return $fieldOptions['default'];
1278
        } else {
1279
            switch ($fieldOptions['type']) {
1280
                case 'set':
1281
                case 'list':
1282
                {
1283
                    return [];
1284
                }
1285
                default:
1286
                {
1287
                    return null;
1288
                }
1289
            }
1290
        }
1291
    }
1292
    
1293
    protected function _setStringValue($fieldOptions, $value)
1294
    {
1295
        if (!$fieldOptions['notnull'] && $fieldOptions['blankisnull'] && ($value === '' || $value === null)) {
1296
            return null;
1297
        }
1298
        return @mb_convert_encoding($value, DB::$encoding, 'auto'); // normalize encoding to ASCII
1299
    }
1300
1301
    protected function _setBooleanValue($value)
1302
    {
1303
        return (boolean)$value;
1304
    }
1305
1306
    protected function _setDecimalValue($value)
1307
    {
1308
        return preg_replace('/[^-\d.]/', '', $value);
1309
    }
1310
1311
    protected function _setIntegerValue($fieldOptions, $value)
1312
    {
1313
        $value = preg_replace('/[^-\d]/', '', $value);
1314
        if (!$fieldOptions['notnull'] && $value === '') {
1315
            return null;
1316
        }
1317
        return $value;
1318
    }
1319
1320
    protected function _setTimestampValue($value)
1321
    {
1322
        if (is_numeric($value)) {
1323
            return date('Y-m-d H:i:s', $value);
1324
        } elseif (is_string($value)) {
1325
            // trim any extra crap, or leave as-is if it doesn't fit the pattern
1326
            if (preg_match('/^(\d{4})\D?(\d{2})\D?(\d{2})T?(\d{2})\D?(\d{2})\D?(\d{2})/')) {
1327
                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);
1328
            } else {
1329
                return date('Y-m-d H:i:s', strtotime($value));
1330
            }
1331
        }
1332
        return null;
1333
    }
1334
1335
    protected function _setSerializedValue($value)
1336
    {
1337
        return serialize($value);
1338
    }
1339
1340
    protected function _setEnumValue($fieldOptions, $value)
1341
    {
1342
        return (in_array($value, $fieldOptions['values']) ? $value : null);
1343
    }
1344
1345
    protected function _setListValue($fieldOptions, $value)
1346
    {
1347
        if (!is_array($value)) {
1348
            $delim = empty($fieldOptions['delimiter']) ? ',' : $fieldOptions['delimiter'];
1349
            $value = array_filter(preg_split('/\s*'.$delim.'\s*/', $value));
1350
        }
1351
        return $value;
1352
    }
1353
1354
    /**
1355
     * Sets given field's value
1356
     * @param string $field Name of field
1357
     * @param mixed $value New value
1358
     * @return mixed value
1359
     */
1360
    protected function _setFieldValue($field, $value)
1361
    {
1362
        // ignore setting versioning fields
1363
        if (static::isVersioned()) {
1364
            if (array_key_exists($field, static::$versioningFields)) {
1365
                return false;
1366
            }
1367
        }
1368
        
1369
        if (!static::fieldExists($field)) {
1370
            return false;
1371
        }
1372
        $fieldOptions = static::$_classFields[get_called_class()][$field];
1373
1374
        // no overriding autoincrements
1375
        if ($fieldOptions['autoincrement']) {
1376
            return false;
1377
        }
1378
1379
        // pre-process value
1380
        $forceDirty = false;
1381
        switch ($fieldOptions['type']) {
1382
            case 'clob':
1383
            case 'string':
1384
            {
1385
                $value = $this->_setStringValue($fieldOptions, $value);
1386
                break;
1387
            }
1388
            
1389
            case 'boolean':
1390
            {
1391
                $value = $this->_setBooleanValue($value);
1392
                break;
1393
            }
1394
            
1395
            case 'decimal':
1396
            {
1397
                $value = $this->_setDecimalValue($value);
1398
                break;
1399
            }
1400
            
1401
            case 'int':
1402
            case 'uint':
1403
            case 'integer':
1404
            {
1405
                $value = $this->_setIntegerValue($fieldOptions, $value);
1406
                break;
1407
            }
1408
            
1409
            case 'timestamp':
1410
            {
1411
                $value = $this->_setTimestampValue($value);
1412
                break;
1413
            }
1414
            
1415
            case 'date':
1416
            {
1417
                $value = $this->_setDateValue($value);
1418
                break;
1419
            }
1420
            
1421
            // these types are converted to strings from another PHP type on save
1422
            case 'serialized':
1423
            {
1424
                $this->_convertedValues[$field] = $value;
1425
                $value = $this->_setSerializedValue($value);
1426
                break;
1427
            }
1428
            case 'enum':
1429
            {
1430
                $value = $this->_setEnumValue($fieldOptions, $value);
1431
                break;
1432
            }
1433
            case 'set':
1434
            case 'list':
1435
            {
1436
                $value = $this->_setListValue($fieldOptions, $value);
1437
                $this->_convertedValues[$field] = $value;
1438
                $forceDirty = true;
1439
                break;
1440
            }
1441
        }
1442
        
1443
        if ($forceDirty || ($this->_record[$field] !== $value)) {
1444
            $columnName = static::_cn($field);
1445
            if (isset($this->_record[$columnName])) {
1446
                $this->_originalValues[$field] = $this->_record[$columnName];
1447
            }
1448
            $this->_record[$columnName] = $value;
1449
            $this->_isDirty = true;
1450
            
1451
            // unset invalidated relationships
1452
            if (!empty($fieldOptions['relationships']) && static::isRelational()) {
1453
                foreach ($fieldOptions['relationships'] as $relationship => $isCached) {
1454
                    if ($isCached) {
1455
                        unset($this->_relatedObjects[$relationship]);
1456
                    }
1457
                }
1458
            }
1459
            return true;
1460
        } else {
1461
            return false;
1462
        }
1463
    }
1464
1465
    protected function _prepareRecordValues()
1466
    {
1467
        $record = [];
1468
1469
        foreach (static::$_classFields[get_called_class()] as $field => $options) {
1470
            $columnName = static::_cn($field);
1471
            
1472
            if (array_key_exists($columnName, $this->_record)) {
1473
                $value = $this->_record[$columnName];
1474
                
1475
                if (!$value && !empty($options['blankisnull'])) {
1476
                    $value = null;
1477
                }
1478
            } elseif (isset($options['default'])) {
1479
                $value = $options['default'];
1480
            } else {
1481
                continue;
1482
            }
1483
1484
            if (($options['type'] == 'date') && ($value == '0000-00-00') && !empty($options['blankisnull'])) {
1485
                $value = null;
1486
            }
1487
            if (($options['type'] == 'timestamp')) {
1488
                if (is_numeric($value)) {
1489
                    $value = date('Y-m-d H:i:s', $value);
1490
                } elseif ($value == null && !$options['notnull']) {
1491
                    $value = null;
1492
                }
1493
            }
1494
1495
            if (($options['type'] == 'serialized') && !is_string($value)) {
1496
                $value = serialize($value);
1497
            }
1498
            
1499
            if (($options['type'] == 'list') && is_array($value)) {
1500
                $delim = empty($options['delimiter']) ? ',' : $options['delimiter'];
1501
                $value = implode($delim, $value);
1502
            }
1503
            
1504
            $record[$field] = $value;
1505
        }
1506
1507
        return $record;
1508
    }
1509
    
1510
    protected static function _mapValuesToSet($recordValues)
1511
    {
1512
        $set = [];
1513
1514
        foreach ($recordValues as $field => $value) {
1515
            $fieldConfig = static::$_classFields[get_called_class()][$field];
1516
            
1517
            if ($value === null) {
1518
                $set[] = sprintf('`%s` = NULL', $fieldConfig['columnName']);
1519
            } elseif ($fieldConfig['type'] == 'timestamp' && $value == 'CURRENT_TIMESTAMP') {
1520
                $set[] = sprintf('`%s` = CURRENT_TIMESTAMP', $fieldConfig['columnName']);
1521
            } elseif ($fieldConfig['type'] == 'set' && is_array($value)) {
1522
                $set[] = sprintf('`%s` = "%s"', $fieldConfig['columnName'], DB::escape(join(',', $value)));
1523
            } elseif ($fieldConfig['type'] == 'boolean') {
1524
                $set[] = sprintf('`%s` = %u', $fieldConfig['columnName'], $value ? 1 : 0);
1525
            } else {
1526
                $set[] = sprintf('`%s` = "%s"', $fieldConfig['columnName'], DB::escape($value));
1527
            }
1528
        }
1529
1530
        return $set;
1531
    }
1532
1533
    protected static function _mapFieldOrder($order)
1534
    {
1535
        if (is_string($order)) {
1536
            return [$order];
1537
        } elseif (is_array($order)) {
1538
            $r = [];
1539
            
1540
            foreach ($order as $key => $value) {
1541
                if (is_string($key)) {
1542
                    $columnName = static::_cn($key);
1543
                    $direction = strtoupper($value)=='DESC' ? 'DESC' : 'ASC';
1544
                } else {
1545
                    $columnName = static::_cn($value);
1546
                    $direction = 'ASC';
1547
                }
1548
                
1549
                $r[] = sprintf('`%s` %s', $columnName, $direction);
1550
            }
1551
            
1552
            return $r;
1553
        }
1554
    }
1555
    
1556
    protected static function _mapConditions($conditions)
1557
    {
1558
        foreach ($conditions as $field => &$condition) {
1559
            if (is_string($field)) {
1560
                $fieldOptions = static::$_classFields[get_called_class()][$field];
1561
            
1562
                if ($condition === null || ($condition == '' && $fieldOptions['blankisnull'])) {
1563
                    $condition = sprintf('`%s` IS NULL', static::_cn($field));
1564
                } elseif (is_array($condition)) {
1565
                    $condition = sprintf('`%s` %s "%s"', static::_cn($field), $condition['operator'], DB::escape($condition['value']));
1566
                } else {
1567
                    $condition = sprintf('`%s` = "%s"', static::_cn($field), DB::escape($condition));
1568
                }
1569
            }
1570
        }
1571
        
1572
        return $conditions;
1573
    }
1574
    
1575
    protected function finishValidation()
1576
    {
1577
        $this->_isValid = $this->_isValid && !$this->_validator->hasErrors();
1578
        
1579
        if (!$this->_isValid) {
1580
            $this->_validationErrors = array_merge($this->_validationErrors, $this->_validator->getErrors());
1581
        }
1582
1583
        return $this->_isValid;
1584
    }
1585
}
1586