Completed
Push — master ( 210e4d...401f4c )
by Henry
02:14
created

ActiveRecord::save()   C

Complexity

Conditions 12
Paths 40

Size

Total Lines 65
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 12
eloc 33
nc 40
nop 1
dl 0
loc 65
rs 5.9833
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
/*
3
 * This file is part of the Divergence package.
4
 *
5
 * (c) Henry Paradiz <[email protected]>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
namespace Divergence\Models;
11
12
use Exception;
13
use 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();
1 ignored issue
show
Bug introduced by
The method beforeVersionedSave() does not exist on Divergence\Models\ActiveRecord. It seems like you code against a sub-type of said class. However, the method does not exist in Divergence\Models\Model or Divergence\Tests\MockSite\Models\Tag or Divergence\Tests\Models\Testables\relationalTag. Are you sure you never get one of those? ( Ignorable by Annotation )

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

422
            $this->/** @scrutinizer ignore-call */ 
423
                   beforeVersionedSave();
Loading history...
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();
1 ignored issue
show
Bug introduced by
The method afterVersionedSave() does not exist on Divergence\Models\ActiveRecord. It seems like you code against a sub-type of said class. However, the method does not exist in Divergence\Models\Model or Divergence\Tests\MockSite\Models\Tag or Divergence\Tests\Models\Testables\relationalTag. Are you sure you never get one of those? ( Ignorable by Annotation )

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

477
                $this->/** @scrutinizer ignore-call */ 
478
                       afterVersionedSave();
Loading history...
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,
1 ignored issue
show
Bug introduced by
The property rootClass does not exist on string.
Loading history...
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
        $where = $options['domainConstraints'];
0 ignored issues
show
Unused Code introduced by
The assignment to $where is dead and can be removed.
Loading history...
849
        
850
        $incarnation = 0;
851
        do {
852
            // TODO: check for repeat posting here?
853
            $incarnation++;
854
            
855
            if ($options['alwaysSuffix'] || $incarnation > 1) {
856
                $handle = sprintf($options['format'], $strippedText, $incarnation);
857
            }
858
        } while (static::getByWhere(array_merge($options['domainConstraints'], [$options['handleField']=>$handle])));
859
        
860
        return $handle;
861
    }
862
    
863
864
    // TODO: make the handleField
865
    public static function generateRandomHandle($length = 32)
866
    {
867
        do {
868
            $handle = substr(md5(mt_rand(0, mt_getrandmax())), 0, $length);
869
        } while (static::getByField(static::$handleField, $handle));
870
        
871
        return $handle;
872
    }
873
    
874
    public static function fieldExists($field)
875
    {
876
        static::init();
877
        return array_key_exists($field, static::$_classFields[get_called_class()]);
878
    }
879
    
880
    
881
    public static function getClassFields()
882
    {
883
        static::init();
884
        return static::$_classFields[get_called_class()];
885
    }
886
    
887
    public static function getFieldOptions($field, $optionKey = false)
888
    {
889
        if ($optionKey) {
890
            return static::$_classFields[get_called_class()][$field][$optionKey];
891
        } else {
892
            return static::$_classFields[get_called_class()][$field];
893
        }
894
    }
895
896
    /**
897
     * Returns columnName for given field
898
     * @param string $field name of field
899
     * @return string column name
900
     */
901
    public static function getColumnName($field)
902
    {
903
        static::init();
904
        if (!static::fieldExists($field)) {
905
            throw new Exception('getColumnName called on nonexisting column: ' . get_called_class().'->'.$field);
906
        }
907
        
908
        return static::$_classFields[get_called_class()][$field]['columnName'];
909
    }
910
    
911
    public static function mapFieldOrder($order)
912
    {
913
        return static::_mapFieldOrder($order);
914
    }
915
    
916
    public static function mapConditions($conditions)
917
    {
918
        return static::_mapConditions($conditions);
919
    }
920
    
921
    public function getRootClass()
922
    {
923
        return static::$rootClass;
924
    }
925
    
926
    public function addValidationErrors($array)
927
    {
928
        foreach ($array as $field => $errorMessage) {
929
            $this->addValidationError($field, $errorMessage);
930
        }
931
    }
932
933
    public function addValidationError($field, $errorMessage)
934
    {
935
        $this->_isValid = false;
936
        $this->_validationErrors[$field] = $errorMessage;
937
    }
938
    
939
    public function getValidationError($field)
940
    {
941
        // break apart path
942
        $crumbs = explode('.', $field);
943
944
        // resolve path recursively
945
        $cur = &$this->_validationErrors;
946
        while ($crumb = array_shift($crumbs)) {
947
            if (array_key_exists($crumb, $cur)) {
948
                $cur = &$cur[$crumb];
949
            } else {
950
                return null;
951
            }
952
        }
953
954
        // return current value
955
        return $cur;
956
    }
957
    
958
    
959
    public function validate($deep = true)
960
    {
961
        $this->_isValid = true;
962
        $this->_validationErrors = [];
963
964
        if (!isset($this->_validator)) {
965
            $this->_validator = new RecordValidator($this->_record);
966
        } else {
967
            $this->_validator->resetErrors();
968
        }
969
970
        foreach (static::$validators as $validator) {
971
            $this->_validator->validate($validator);
972
        }
973
        
974
        $this->finishValidation();
975
976
        if ($deep) {
977
            // validate relationship objects
978
            foreach (static::$_classRelationships[get_called_class()] as $relationship => $options) {
979
                if (empty($this->_relatedObjects[$relationship])) {
980
                    continue;
981
                }
982
                
983
                
984
                if ($options['type'] == 'one-one') {
985
                    if ($this->_relatedObjects[$relationship]->isDirty) {
986
                        $this->_relatedObjects[$relationship]->validate();
987
                        $this->_isValid = $this->_isValid && $this->_relatedObjects[$relationship]->isValid;
988
                        $this->_validationErrors[$relationship] = $this->_relatedObjects[$relationship]->validationErrors;
989
                    }
990
                } elseif ($options['type'] == 'one-many') {
991
                    foreach ($this->_relatedObjects[$relationship] as $i => $object) {
992
                        if ($object->isDirty) {
993
                            $object->validate();
994
                            $this->_isValid = $this->_isValid && $object->isValid;
995
                            $this->_validationErrors[$relationship][$i] = $object->validationErrors;
996
                        }
997
                    }
998
                }
999
            }
1000
        }
1001
        
1002
        return $this->_isValid;
1003
    }
1004
    
1005
    public static function handleError($query = null, $queryLog = null, $parameters = null)
1006
    {
1007
        $Connection = DB::getConnection();
1008
        
1009
        if ($Connection->errorCode() == '42S02' && static::$autoCreateTables) {
1010
            $CreateTable = SQL::getCreateTable(static::$rootClass);
1011
            
1012
            // history versions table
1013
            if (static::isVersioned()) {
1014
                $CreateTable .= SQL::getCreateTable(static::$rootClass, true);
1015
            }
1016
            
1017
            $Statement = $Connection->query($CreateTable);
1018
            
1019
            // check for errors
1020
            $ErrorInfo = $Statement->errorInfo();
1021
        
1022
            // handle query error
1023
            if ($ErrorInfo[0] != '00000') {
1024
                self::handleError($query, $queryLog);
1025
            }
1026
            
1027
            // clear buffer (required for the next query to work without running fetchAll first
1028
            $Statement->closeCursor();
1029
            
1030
            return $Connection->query($query); // now the query should finish with no error
1031
        } else {
1032
            return DB::handleError($query, $queryLog);
1033
        }
1034
    }
1035
    
1036
    protected static function _defineEvents()
1037
    {
1038
        // run before save
1039
        $className = get_called_class();
1040
        
1041
        // merge fields from first ancestor up
1042
        $classes = class_parents($className);
1043
        array_unshift($classes, $className);
1044
    
1045
        while ($class = array_pop($classes)) {
1046
            if (is_callable($class::$beforeSave)) {
1047
                if (!empty($class::$beforeSave)) {
1048
                    if (!in_array($class::$beforeSave, static::$_classBeforeSave)) {
1049
                        static::$_classBeforeSave[] = $class::$beforeSave;
1050
                    }
1051
                }
1052
            }
1053
            
1054
            if (is_callable($class::$afterSave)) {
1055
                if (!empty($class::$afterSave)) {
1056
                    if (!in_array($class::$afterSave, static::$_classAfterSave)) {
1057
                        static::$_classAfterSave[] = $class::$afterSave;
1058
                    }
1059
                }
1060
            }
1061
        }
1062
    }
1063
    
1064
    /**
1065
     * Called when a class is loaded to define fields before _initFields
1066
     */
1067
    protected static function _defineFields()
1068
    {
1069
        $className = get_called_class();
1070
1071
        // skip if fields already defined
1072
        if (isset(static::$_classFields[$className])) {
1073
            return;
1074
        }
1075
        
1076
        // merge fields from first ancestor up
1077
        $classes = class_parents($className);
1078
        array_unshift($classes, $className);
1079
        
1080
        static::$_classFields[$className] = [];
1081
        while ($class = array_pop($classes)) {
1082
            if (!empty($class::$fields)) {
1083
                static::$_classFields[$className] = array_merge(static::$_classFields[$className], $class::$fields);
1084
            }
1085
        }
1086
        
1087
        // versioning
1088
        if (static::isVersioned()) {
1089
            static::$_classFields[$className] = array_merge(static::$_classFields[$className], static::$versioningFields);
1090
        }
1091
    }
1092
1093
    
1094
    /**
1095
     * Called after _defineFields to initialize and apply defaults to the fields property
1096
     * Must be idempotent as it may be applied multiple times up the inheritence chain
1097
     */
1098
    protected static function _initFields()
1099
    {
1100
        $className = get_called_class();
1101
        $optionsMask = [
1102
            'type' => null,
1103
            'length' => null,
1104
            'primary' => null,
1105
            'unique' => null,
1106
            'autoincrement' => null,
1107
            'notnull' => null,
1108
            'unsigned' => null,
1109
            'default' => null,
1110
            'values' => null,
1111
        ];
1112
        
1113
        // apply default values to field definitions
1114
        if (!empty(static::$_classFields[$className])) {
1115
            $fields = [];
1116
            
1117
            foreach (static::$_classFields[$className] as $field => $options) {
1118
                if (is_string($field)) {
1119
                    if (is_array($options)) {
1120
                        $fields[$field] = array_merge($optionsMask, static::$fieldDefaults, ['columnName' => $field], $options);
1121
                    } elseif (is_string($options)) {
1122
                        $fields[$field] = array_merge($optionsMask, static::$fieldDefaults, ['columnName' => $field, 'type' => $options]);
1123
                    } elseif ($options == null) {
1124
                        continue;
1125
                    }
1126
                } elseif (is_string($options)) {
1127
                    $field = $options;
1128
                    $fields[$field] = array_merge($optionsMask, static::$fieldDefaults, ['columnName' => $field]);
1129
                }
1130
                
1131
                if ($field == 'Class') {
1132
                    // apply Class enum values
1133
                    $fields[$field]['values'] = static::$subClasses;
1134
                }
1135
                
1136
                if (!isset($fields[$field]['blankisnull']) && empty($fields[$field]['notnull'])) {
1137
                    $fields[$field]['blankisnull'] = true;
1138
                }
1139
                
1140
                if ($fields[$field]['autoincrement']) {
1141
                    $fields[$field]['primary'] = true;
1142
                }
1143
            }
1144
            
1145
            static::$_classFields[$className] = $fields;
1146
        }
1147
    }
1148
1149
1150
    /**
1151
     * Returns class name for instantiating given record
1152
     * @param array $record record
1153
     * @return string class name
1154
     */
1155
    protected static function _getRecordClass($record)
1156
    {
1157
        $static = get_called_class();
1158
        
1159
        if (!static::fieldExists('Class')) {
1160
            return $static;
1161
        }
1162
        
1163
        $columnName = static::_cn('Class');
1164
        
1165
        if (!empty($record[$columnName]) && is_subclass_of($record[$columnName], $static)) {
1166
            return $record[$columnName];
1167
        } else {
1168
            return $static;
1169
        }
1170
    }
1171
    
1172
    /**
1173
     * Shorthand alias for _getColumnName
1174
     * @param string $field name of field
1175
     * @return string column name
1176
     */
1177
    protected static function _cn($field)
1178
    {
1179
        return static::getColumnName($field);
1180
    }
1181
1182
    
1183
    /**
1184
     * Retrieves given field's value
1185
     * @param string $field Name of field
1186
     * @return mixed value
1187
     */
1188
    protected function _getFieldValue($field, $useDefault = true)
1189
    {
1190
        $fieldOptions = static::$_classFields[get_called_class()][$field];
1191
    
1192
        if (isset($this->_record[$fieldOptions['columnName']])) {
1193
            $value = $this->_record[$fieldOptions['columnName']];
1194
            
1195
            // apply type-dependent transformations
1196
            switch ($fieldOptions['type']) {
1197
                case 'password':
1198
                {
1199
                    return $value;
1200
                }
1201
                
1202
                case 'timestamp':
1203
                {
1204
                    if (!isset($this->_convertedValues[$field])) {
1205
                        if ($value && $value != '0000-00-00 00:00:00') {
1206
                            $this->_convertedValues[$field] = strtotime($value);
1207
                        } else {
1208
                            $this->_convertedValues[$field] = null;
1209
                        }
1210
                    }
1211
                    
1212
                    return $this->_convertedValues[$field];
1213
                }
1214
                case 'serialized':
1215
                {
1216
                    if (!isset($this->_convertedValues[$field])) {
1217
                        $this->_convertedValues[$field] = is_string($value) ? unserialize($value) : $value;
1218
                    }
1219
                    
1220
                    return $this->_convertedValues[$field];
1221
                }
1222
                case 'set':
1223
                case 'list':
1224
                {
1225
                    if (!isset($this->_convertedValues[$field])) {
1226
                        $delim = empty($fieldOptions['delimiter']) ? ',' : $fieldOptions['delimiter'];
1227
                        $this->_convertedValues[$field] = array_filter(preg_split('/\s*'.$delim.'\s*/', $value));
1 ignored issue
show
Bug introduced by
It seems like preg_split('/\s*' . $delim . '\s*/', $value) can also be of type false; however, parameter $input of array_filter() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

1227
                        $this->_convertedValues[$field] = array_filter(/** @scrutinizer ignore-type */ preg_split('/\s*'.$delim.'\s*/', $value));
Loading history...
1228
                    }
1229
                    
1230
                    return $this->_convertedValues[$field];
1231
                }
1232
                
1233
                case 'boolean':
1234
                {
1235
                    if (!isset($this->_convertedValues[$field])) {
1236
                        $this->_convertedValues[$field] = (boolean)$value;
1237
                    }
1238
                    
1239
                    return $this->_convertedValues[$field];
1240
                }
1241
                
1242
                default:
1243
                {
1244
                    return $value;
1245
                }
1246
            }
1247
        } elseif ($useDefault && isset($fieldOptions['default'])) {
1248
            // return default
1249
            return $fieldOptions['default'];
1250
        } else {
1251
            switch ($fieldOptions['type']) {
1252
                case 'set':
1253
                case 'list':
1254
                {
1255
                    return [];
1256
                }
1257
                default:
1258
                {
1259
                    return null;
1260
                }
1261
            }
1262
        }
1263
    }
1264
    
1265
    /**
1266
     * Sets given field's value
1267
     * @param string $field Name of field
1268
     * @param mixed $value New value
1269
     * @return mixed value
1270
     */
1271
    protected function _setFieldValue($field, $value)
1272
    {
1273
        // ignore setting versioning fields
1274
        if (static::isVersioned()) {
1275
            if (array_key_exists($field, static::$versioningFields)) {
1276
                return false;
1277
            }
1278
        }
1279
        
1280
        if (!static::fieldExists($field)) {
1281
            return false;
1282
        }
1283
        $fieldOptions = static::$_classFields[get_called_class()][$field];
1284
1285
        // no overriding autoincrements
1286
        if ($fieldOptions['autoincrement']) {
1287
            return false;
1288
        }
1289
1290
        // pre-process value
1291
        $forceDirty = false;
1292
        switch ($fieldOptions['type']) {
1293
            case 'clob':
1294
            case 'string':
1295
            {
1296
                if (!$fieldOptions['notnull'] && $fieldOptions['blankisnull'] && ($value === '' || $value === null)) {
1297
                    $value = null;
1298
                    break;
1299
                }
1300
            
1301
                // normalize encoding to ASCII
1302
                $value = @mb_convert_encoding($value, DB::$encoding, 'auto');
1303
                
1304
                break;
1305
            }
1306
            
1307
            case 'boolean':
1308
            {
1309
                $value = (boolean)$value;
1310
            }
1311
            
1312
            case 'decimal':
1313
            {
1314
                $value = preg_replace('/[^-\d.]/', '', $value);
1315
                break;
1316
            }
1317
            
1318
            case 'int':
1319
            case 'uint':
1320
            case 'integer':
1321
            {
1322
                $value = preg_replace('/[^-\d]/', '', $value);
1323
                
1324
                if (!$fieldOptions['notnull'] && $value === '') {
1325
                    $value = null;
1326
                }
1327
                
1328
                break;
1329
            }
1330
            
1331
            case 'timestamp':
1332
            {
1333
                if (is_numeric($value)) {
1334
                    $value = date('Y-m-d H:i:s', $value);
1335
                } elseif (is_string($value)) {
1336
                    // trim any extra crap, or leave as-is if it doesn't fit the pattern
1337
                    if (preg_match('/^(\d{4})\D?(\d{2})\D?(\d{2})T?(\d{2})\D?(\d{2})\D?(\d{2})/')) {
1338
                        $value = 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);
1339
                    } else {
1340
                        $value = date('Y-m-d H:i:s', strtotime($value));
1341
                    }
1342
                }
1343
                break;
1344
            }
1345
            
1346
            case 'date':
1347
            {
1348
                if (is_numeric($value)) {
1349
                    $value = date('Y-m-d', $value);
1350
                } elseif (is_string($value)) {
1351
                    // check if m/d/y format
1352
                    if (preg_match('/^(\d{2})\D?(\d{2})\D?(\d{4}).*/', $value)) {
1353
                        $value = preg_replace('/^(\d{2})\D?(\d{2})\D?(\d{4}).*/', '$3-$1-$2', $value);
1354
                    }
1355
                    
1356
                    // trim time and any extra crap, or leave as-is if it doesn't fit the pattern
1357
                    $value = preg_replace('/^(\d{4})\D?(\d{2})\D?(\d{2}).*/', '$1-$2-$3', $value);
1358
                } elseif (is_array($value) && count(array_filter($value))) {
1359
                    // collapse array date to string
1360
                    $value = sprintf(
1361
                        '%04u-%02u-%02u',
1362
                        is_numeric($value['yyyy']) ? $value['yyyy'] : 0,
1363
                        is_numeric($value['mm']) ? $value['mm'] : 0,
1364
                        is_numeric($value['dd']) ? $value['dd'] : 0
1365
                    );
1366
                } else {
1367
                    if ($value = strtotime($value)) {
1368
                        $value = date('Y-m-d', $value) ?: null;
1369
                    } else {
1370
                        $value = null;
1371
                    }
1372
                }
1373
                break;
1374
            }
1375
            
1376
            // these types are converted to strings from another PHP type on save
1377
            case 'serialized':
1378
            {
1379
                $this->_convertedValues[$field] = $value;
1380
                $value = serialize($value);
1381
                break;
1382
            }
1383
            case 'enum':
1384
            {
1385
                $value = in_array($value, $fieldOptions['values']) ? $value : null;
1386
                break;
1387
            }
1388
            case 'set':
1389
            case 'list':
1390
            {
1391
                if (!is_array($value)) {
1392
                    $delim = empty($fieldOptions['delimiter']) ? ',' : $fieldOptions['delimiter'];
1393
                    $value = array_filter(preg_split('/\s*'.$delim.'\s*/', $value));
1394
                }
1395
            
1396
                $this->_convertedValues[$field] = $value;
1397
                $forceDirty = true;
1398
                break;
1399
            }
1400
        }
1401
        
1402
        if ($forceDirty || ($this->_record[$field] !== $value)) {
1403
            $columnName = static::_cn($field);
1404
            if (isset($this->_record[$columnName])) {
1405
                $this->_originalValues[$field] = $this->_record[$columnName];
1406
            }
1407
            $this->_record[$columnName] = $value;
1408
            $this->_isDirty = true;
1409
            
1410
            // unset invalidated relationships
1411
            if (!empty($fieldOptions['relationships']) && static::isRelational()) {
1412
                foreach ($fieldOptions['relationships'] as $relationship => $isCached) {
1413
                    if ($isCached) {
1414
                        unset($this->_relatedObjects[$relationship]);
1415
                    }
1416
                }
1417
            }
1418
            
1419
            
1420
            return true;
1421
        } else {
1422
            return false;
1423
        }
1424
    }
1425
1426
    protected function _prepareRecordValues()
1427
    {
1428
        $record = [];
1429
1430
        foreach (static::$_classFields[get_called_class()] as $field => $options) {
1431
            $columnName = static::_cn($field);
1432
            
1433
            if (array_key_exists($columnName, $this->_record)) {
1434
                $value = $this->_record[$columnName];
1435
                
1436
                if (!$value && !empty($options['blankisnull'])) {
1437
                    $value = null;
1438
                }
1439
            } elseif (isset($options['default'])) {
1440
                $value = $options['default'];
1441
            } else {
1442
                continue;
1443
            }
1444
1445
            if (($options['type'] == 'date') && ($value == '0000-00-00') && !empty($options['blankisnull'])) {
1446
                $value = null;
1447
            }
1448
            if (($options['type'] == 'timestamp')) {
1449
                if (is_numeric($value)) {
1450
                    $value = date('Y-m-d H:i:s', $value);
1451
                } elseif ($value == null && !$options['notnull']) {
1452
                    $value = null;
1453
                }
1454
            }
1455
1456
            if (($options['type'] == 'serialized') && !is_string($value)) {
1457
                $value = serialize($value);
1458
            }
1459
            
1460
            if (($options['type'] == 'list') && is_array($value)) {
1461
                $delim = empty($options['delimiter']) ? ',' : $options['delimiter'];
1462
                $value = implode($delim, $value);
1463
            }
1464
            
1465
            $record[$field] = $value;
1466
        }
1467
1468
        return $record;
1469
    }
1470
    
1471
    protected static function _mapValuesToSet($recordValues)
1472
    {
1473
        $set = [];
1474
1475
        foreach ($recordValues as $field => $value) {
1476
            $fieldConfig = static::$_classFields[get_called_class()][$field];
1477
            
1478
            if ($value === null) {
1479
                $set[] = sprintf('`%s` = NULL', $fieldConfig['columnName']);
1480
            } elseif ($fieldConfig['type'] == 'timestamp' && $value == 'CURRENT_TIMESTAMP') {
1481
                $set[] = sprintf('`%s` = CURRENT_TIMESTAMP', $fieldConfig['columnName']);
1482
            } elseif ($fieldConfig['type'] == 'set' && is_array($value)) {
1483
                $set[] = sprintf('`%s` = "%s"', $fieldConfig['columnName'], DB::escape(join(',', $value)));
1484
            } elseif ($fieldConfig['type'] == 'boolean') {
1485
                $set[] = sprintf('`%s` = %u', $fieldConfig['columnName'], $value ? 1 : 0);
1486
            } else {
1487
                $set[] = sprintf('`%s` = "%s"', $fieldConfig['columnName'], DB::escape($value));
1488
            }
1489
        }
1490
1491
        return $set;
1492
    }
1493
1494
    protected static function _mapFieldOrder($order)
1495
    {
1496
        if (is_string($order)) {
1497
            return [$order];
1498
        } elseif (is_array($order)) {
1499
            $r = [];
1500
            
1501
            foreach ($order as $key => $value) {
1502
                if (is_string($key)) {
1503
                    $columnName = static::_cn($key);
1504
                    $direction = strtoupper($value)=='DESC' ? 'DESC' : 'ASC';
1505
                } else {
1506
                    $columnName = static::_cn($value);
1507
                    $direction = 'ASC';
1508
                }
1509
                
1510
                $r[] = sprintf('`%s` %s', $columnName, $direction);
1511
            }
1512
            
1513
            return $r;
1514
        }
1515
    }
1516
    
1517
    protected static function _mapConditions($conditions)
1518
    {
1519
        foreach ($conditions as $field => &$condition) {
1520
            if (is_string($field)) {
1521
                $fieldOptions = static::$_classFields[get_called_class()][$field];
1522
            
1523
                if ($condition === null || ($condition == '' && $fieldOptions['blankisnull'])) {
1524
                    $condition = sprintf('`%s` IS NULL', static::_cn($field));
1525
                } elseif (is_array($condition)) {
1526
                    $condition = sprintf('`%s` %s "%s"', static::_cn($field), $condition['operator'], DB::escape($condition['value']));
1 ignored issue
show
Bug introduced by
It seems like Divergence\IO\Database\M...pe($condition['value']) can also be of type array and array; however, parameter $args of sprintf() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

1526
                    $condition = sprintf('`%s` %s "%s"', static::_cn($field), $condition['operator'], /** @scrutinizer ignore-type */ DB::escape($condition['value']));
Loading history...
1527
                } else {
1528
                    $condition = sprintf('`%s` = "%s"', static::_cn($field), DB::escape($condition));
1529
                }
1530
            }
1531
        }
1532
        
1533
        return $conditions;
1534
    }
1535
    
1536
    protected function finishValidation()
1537
    {
1538
        $this->_isValid = $this->_isValid && !$this->_validator->hasErrors();
1539
        
1540
        if (!$this->_isValid) {
1541
            $this->_validationErrors = array_merge($this->_validationErrors, $this->_validator->getErrors());
1542
        }
1543
1544
        return $this->_isValid;
1545
    }
1546
}
1547