Passed
Push — develop ( ed14bd...e3f1ea )
by Mathieu
01:50
created

DBObject::isLoaded()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 0
dl 0
loc 3
rs 10
ccs 2
cts 2
cp 1
crap 1
1
<?php
2
namespace Suricate;
3
4
/**
5
 * DBObject, Pseudo ORM Class
6
 *
7
 * Two types of variables are available :
8
 * - $dbVariables, an array of fields contained in linked SQL table
9
 * - $protectedVariables, an array of variables not stored in SQL
10
 *     that can be triggered on access
11
 *
12
 * @package Suricate
13
 * @author  Mathieu LESNIAK <[email protected]>
14
 */
15
16
class DBObject implements Interfaces\IDBObject
17
{
18
    /** @var string Linked SQL Table */
19
    protected $tableName = '';
20
21
    /** @var string Unique ID of the SQL table */
22
    protected $tableIndex = '';
23
    
24
    /**
25
     * @const DB_CONFIG : Database configuration identifier
26
     */
27
    const DB_CONFIG     = '';
28
29
    /**
30
     * @const RELATION_ONE_ONE : Relation one to one
31
     */
32
    const RELATION_ONE_ONE      = 1;
33
    /**
34
     * @const RELATION_ONE_MANY : Relation one to many
35
     */
36
    const RELATION_ONE_MANY     = 2;
37
    /**
38
     * @const RELATION_MANY_MANY : Relation many to many
39
     */
40
    const RELATION_MANY_MANY    = 3;
41
42
    protected $dbVariables                  = [];
43
    protected $dbValues                     = [];
44
    
45
    protected $protectedVariables           = [];
46
    protected $protectedValues              = [];
47
    protected $loadedProtectedVariables     = [];
48
49
    protected $readOnlyVariables            = [];
50
51
    protected $relations                    = [];
52
    protected $relationValues               = [];
53
    protected $loadedRelations              = [];
54
55
    protected $exportedVariables            = [];
56
57
    protected $dbLink                       = false;
58
59
    protected $validatorMessages            = [];
60
    
61
62 7
    public function __construct()
63
    {
64 7
        $this->setRelations();
65 7
    }
66
    /**
67
     * Magic getter
68
     *
69
     * Try to get object property according this order :
70
     * <ul>
71
     *     <li>$dbVariable</li>
72
     *     <li>$protectedVariable (triggger call to accessToProtectedVariable()
73
     *         if not already loaded)</li>
74
     * </ul>
75
     *
76
     * @param  string $name     Property name
77
     * @return Mixed            Property value
78
     */
79 3
    public function __get($name)
80
    {
81 3
        if ($this->isDBVariable($name)) {
82 2
            return $this->getDBVariable($name);
83 1
        } elseif ($this->isProtectedVariable($name)) {
84
            return $this->getProtectedVariable($name);
85 1
        } elseif ($this->isRelation($name)) {
86
            return $this->getRelation($name);
87 1
        } elseif (!empty($this->$name)) {
88
            return $this->$name;
89
        }
90
91 1
        throw new \InvalidArgumentException('Undefined property ' . $name);
92
    }
93
94
        /**
95
     * Magic setter
96
     *
97
     * Set a property to defined value
98
     * Assignment in this order :
99
     * - $dbVariable
100
     * - $protectedVariable
101
     *  </ul>
102
     * @param string $name  variable name
103
     * @param mixed $value variable value
104
     */
105 1
    public function __set($name, $value)
106
    {
107 1
        if ($this->isDBVariable($name)) {
108
            $this->dbValues[$name] = $value;
109 1
        } elseif ($this->isProtectedVariable($name)) {
110
            $this->protectedValues[$name] = $value;
111 1
        } elseif ($this->isRelation($name)) {
112
            $this->relationValues[$name] = $value;
113
        } else {
114 1
            $this->$name = $value;
115
        }
116 1
    }
117
118 2
    public function __isset($name)
119
    {
120 2
        if ($this->isDBVariable($name)) {
121 1
            return isset($this->dbValues[$name]);
122 2
        } elseif ($this->isProtectedVariable($name)) {
123
            // Load only one time protected variable automatically
124
            if (!$this->isProtectedVariableLoaded($name)) {
125
                $protectedAccessResult = $this->accessToProtectedVariable($name);
126
127
                if ($protectedAccessResult) {
0 ignored issues
show
introduced by
The condition $protectedAccessResult is always false.
Loading history...
128
                    $this->markProtectedVariableAsLoaded($name);
129
                }
130
            }
131
            return isset($this->protectedValues[$name]);
132 2
        } elseif ($this->isRelation($name)) {
133
            if (!$this->isRelationLoaded($name)) {
134
                $relationResult = $this->loadRelation($name);
135
136
                if ($relationResult) {
137
                    $this->markRelationAsLoaded($name);
138
                }
139
            }
140
            return isset($this->relationValues[$name]);
141
        }
142
143 2
        return false;
144
    }
145
146 1
    public function getTableName()
147
    {
148 1
        return $this->tableName;
149
    }
150
151 2
    public function getTableIndex()
152
    {
153 2
        return $this->tableIndex;
154
    }
155
156
    /**
157
     * __sleep magic method, permits an inherited DBObject class to be serialized
158
     * @return Array of properties to serialize
159
     */
160
    public function __sleep()
161
    {
162
        $discardedProps = ['dbLink', 'relations'];
163
        $reflection     = new \ReflectionClass($this);
164
        $props          = $reflection->getProperties();
165
        $result         = [];
166
        foreach ($props as $currentProperty) {
167
            $result[] = $currentProperty->name;
168
        }
169
        
170
        return array_diff($result, $discardedProps);
171
    }
172
173
    public function __wakeup()
174
    {
175
        $this->dbLink = false;
176
        $this->setRelations();
177
    }
178
    
179
    /**
180
     * @param string $name
181
     */
182 2
    private function getDBVariable($name)
183
    {
184 2
        if (isset($this->dbValues[$name])) {
185 2
            return $this->dbValues[$name];
186
        }
187
188 2
        return null;
189
    }
190
191
    /**
192
     * Check if variable is from DB
193
     * @param  string  $name variable name
194
     * @return boolean
195
     */
196 4
    public function isDBVariable($name)
197
    {
198 4
        return in_array($name, $this->dbVariables);
199
    }
200
201
    /**
202
     * @param string $name
203
     */
204
    private function getProtectedVariable($name)
205
    {
206
        // Variable exists, and is already loaded
207
        if (isset($this->protectedValues[$name]) && $this->isProtectedVariableLoaded($name)) {
208
            return $this->protectedValues[$name];
209
        }
210
        // Variable has not been loaded
211
        if (!$this->isProtectedVariableLoaded($name)) {
212
            if ($this->accessToProtectedVariable($name)) {
213
                $this->markProtectedVariableAsLoaded($name);
214
            }
215
        }
216
217
        if (isset($this->protectedValues[$name])) {
218
            return $this->protectedValues[$name];
219
        }
220
221
        return null;
222
    }
223
224
    /**
225
     * @param string $name
226
     */
227
    private function getRelation($name)
228
    {
229
        if (isset($this->relationValues[$name]) && $this->isRelationLoaded($name)) {
230
            return $this->relationValues[$name];
231
        }
232
233
        if (!$this->isRelationLoaded($name)) {
234
            if ($this->loadRelation($name)) {
235
                $this->markRelationAsLoaded($name);
236
            }
237
        }
238
239
        if (isset($this->relationValues[$name])) {
240
            return $this->relationValues[$name];
241
        }
242
243
        return null;
244
    }
245
246
    /**
247
     * Check if variable is predefined relation
248
     * @param  string  $name variable name
249
     * @return boolean
250
     */
251 3
    protected function isRelation($name)
252
    {
253 3
        return isset($this->relations[$name]);
254
    }
255
    /**
256
     * Define object relations
257
     *
258
     * @return DBObject
259
     */
260 6
    protected function setRelations()
261
    {
262 6
        $this->relations = [];
263
264 6
        return $this;
265
    }
266
267
    /**
268
     * Define object exported variables
269
     *
270
     * @return DBObject
271
     */
272
    protected function setExportedVariables()
273
    {
274
        if (count($this->exportedVariables)) {
275
            return $this;
276
        }
277
278
        $dbMappingExport = [];
279
        foreach ($this->dbVariables as $field) {
280
            $dbMappingExport[$field] = $field;
281
        }
282
        $this->exportedVariables = $dbMappingExport;
283
284
        return $this;
285
    }
286
287
    /**
288
     * Export DBObject to array
289
     *
290
     * @return array
291
     */
292
    public function toArray()
293
    {
294
        $this->setExportedVariables();
295
        $result = [];
296
        foreach ($this->exportedVariables as $sourceName => $destinationName) {
297
            $omitEmpty  = false;
298
            $castType   = null;
299
            if (strpos($destinationName, ',') !== false) {
300
                $splitted   = explode(',', $destinationName);
301
                array_map(function ($item) use (&$castType, &$omitEmpty) {
302
                    if ($item === 'omitempty') {
303
                        $omitEmpty = true;
304
                        return;
305
                    }
306
                    if (substr($item, 0, 5) === 'type:') {
307
                        $castType = substr($item, 5);
308
                    }
309
                }, $splitted);
310
311
                $destinationName = $splitted[0];
312
            }
313
314
            if ($destinationName === '-') {
315
                continue;
316
            }
317
318
            if ($omitEmpty && empty($this->$sourceName)) {
319
                continue;
320
            }
321
            $value = $this->$sourceName;
322
            if ($castType !== null) {
323
                settype($value, $castType);
324
            }
325
            $result[$destinationName] = $value;
326
        }
327
328
        return $result;
329
    }
330
331
    /**
332
     * Export DBObject to JSON format
333
     *
334
     * @return string
335
     */
336
    public function toJson()
337
    {
338
        return implode('', array_map('json_encode', $this->toArray()));
339
    }
340
341
    /**
342
     * Mark a protected variable as loaded
343
     * @param  string $name varialbe name
344
     * @return void
345
     */
346
    public function markProtectedVariableAsLoaded($name)
347
    {
348
        if ($this->isProtectedVariable($name)) {
349
            $this->loadedProtectedVariables[$name] = true;
350
        }
351
    }
352
353
    /**
354
     * Mark a relation as loaded
355
     * @param  string $name varialbe name
356
     * @return void
357
     */
358
    protected function markRelationAsLoaded($name)
359
    {
360
        if ($this->isRelation($name)) {
361
            $this->loadedRelations[$name] = true;
362
        }
363
    }
364
     /**
365
     * Check if a relation already have been loaded
366
     * @param  string  $name Variable name
367
     * @return boolean
368
     */
369
    protected function isRelationLoaded($name)
370
    {
371
        return isset($this->loadedRelations[$name]);
372
    }
373
374
    protected function loadRelation($name)
375
    {
376
        if (isset($this->relations[$name])) {
377
            switch ($this->relations[$name]['type']) {
378
                case self::RELATION_ONE_ONE:
379
                    return $this->loadRelationOneOne($name);
380
                case self::RELATION_ONE_MANY:
381
                    return $this->loadRelationOneMany($name);
382
                case self::RELATION_MANY_MANY:
383
                    return $this->loadRelationManyMany($name);
384
            }
385
        }
386
387
        return false;
388
    }
389
390
    private function loadRelationOneOne($name)
391
    {
392
        $target = $this->relations[$name]['target'];
393
        $source = $this->relations[$name]['source'];
394
        $this->relationValues[$name] = new $target();
395
        $this->relationValues[$name]->load($this->$source);
396
        
397
        return true;
398
    }
399
400
    private function loadRelationOneMany($name)
401
    {
402
        $target         = $this->relations[$name]['target'];
403
        $parentId       = $this->{$this->relations[$name]['source']};
404
        $parentIdField  = isset($this->relations[$name]['target_field']) ? $this->relations[$name]['target_field'] : null;
405
        $validate       = dataGet($this->relations[$name], 'validate', null);
406
        
407
        $this->relationValues[$name] = $target::loadForParentId($parentId, $parentIdField, $validate);
408
409
        return true;
410
    }
411
412
    private function loadRelationManyMany($name)
413
    {
414
        $pivot      = $this->relations[$name]['pivot'];
415
        $sourceType = $this->relations[$name]['source_type'];
416
        $target     = dataGet($this->relations[$name], 'target');
417
        $validate   = dataGet($this->relations[$name], 'validate', null);
418
419
        $this->relationValues[$name] = $pivot::loadFor($sourceType, $this->{$this->relations[$name]['source']}, $target, $validate);
420
        
421
        return true;
422
    }
423
424
    private function resetLoadedVariables()
425
    {
426
        $this->loadedProtectedVariables = [];
427
        $this->loadedRelations          = [];
428
429
        return $this;
430
    }
431
432
    /**
433
     * Check if requested property exists
434
     *
435
     * Check in following order:
436
     * <ul>
437
     *     <li>$dbVariables</li>
438
     *     <li>$protectedVariables</li>
439
     *     <li>$relations</li>
440
     *     <li>legacy property</li>
441
     * </ul>
442
     * @param  string $property Property name
443
     * @return boolean           true if exists
444
     */
445 1
    public function propertyExists($property)
446
    {
447 1
        return $this->isDBVariable($property)
448 1
            || $this->isProtectedVariable($property)
449 1
            || $this->isRelation($property)
450 1
            || property_exists($this, $property);
451
    }
452
   
453
   /**
454
    * Check if variable is a protected variable
455
    * @param  string  $name variable name
456
    * @return boolean
457
    */
458 3
    public function isProtectedVariable($name)
459
    {
460 3
        return in_array($name, $this->protectedVariables);
461
    }
462
463
    
464
465
    /**
466
     * Check if a protected variable already have been loaded
467
     * @param  string  $name Variable name
468
     * @return boolean
469
     */
470
    protected function isProtectedVariableLoaded($name)
471
    {
472
        return isset($this->loadedProtectedVariables[$name]);
473
    }
474
475
    
476
    
477
    /**
478
     * Load ORM from Database
479
     * @param  mixed $id SQL Table Unique id
480
     * @return mixed     Loaded object or false on failure
481
     */
482
    public function load($id)
483
    {
484
        $this->connectDB();
485
        $this->resetLoadedVariables();
486
487
        if ($id != '') {
488
            $query  = "SELECT *";
489
            $query .= " FROM `" . $this->getTableName() ."`";
490
            $query .= " WHERE";
491
            $query .= "     `" . $this->getTableIndex() . "` =  :id";
492
            
493
            $params         = [];
494
            $params['id']   = $id;
495
496
            return $this->loadFromSql($query, $params);
497
        }
498
        
499
        return $this;
500
    }
501
502 1
    public function isLoaded()
503
    {
504 1
        return $this->{$this->getTableIndex()} !== null;
505
    }
506
507
    public function loadOrFail($id)
508
    {
509
        $this->load($id);
510
        if ($id == '' || $this->{$this->getTableIndex()} != $id) {
511
            throw (new Exception\ModelNotFoundException)->setModel(get_called_class());
512
        }
513
514
        return $this;
515
    }
516
517
    public static function loadOrCreate($arg)
518
    {
519
        $obj = static::loadOrInstanciate($arg);
520
        $obj->save();
521
522
        return $obj;
523
    }
524
525
    public static function loadOrInstanciate($arg)
526
    {
527
        $calledClass = get_called_class();
528
        $obj = new $calledClass;
529
        
530
        if (!is_array($arg)) {
531
            $arg = [$obj->getTableIndex() => $arg];
532
        }
533
        
534
535
        $sql = "SELECT *";
536
        $sql .= " FROM `" . $calledClass->getTableName() . "`";
537
        $sql .= " WHERE ";
538
539
        $sqlArray   = [];
540
        $params     = [];
541
        $i = 0;
542
        foreach ($arg as $key => $val) {
543
            if (is_null($val)) {
544
                $sqlArray[] = '`' . $key . '` IS :arg' . $i;
545
            } else {
546
                $sqlArray[] = '`' . $key . '`=:arg' . $i;
547
            }
548
            $params['arg' .$i] = $val;
549
            $i++;
550
        }
551
        $sql .= implode(' AND ', $sqlArray);
552
553
        if (!$obj->loadFromSql($sql, $params)) {
554
            foreach ($arg as $property => $value) {
555
                $obj->$property = $value;
556
            }
557
        }
558
559
        return $obj;
560
    }
561
    
562
    /**
563
     * @param string $sql
564
     */
565
    public function loadFromSql($sql, $sqlParams = [])
566
    {
567
        $this->connectDB();
568
        $this->resetLoadedVariables();
569
        
570
        $results = $this->dbLink->query($sql, $sqlParams)->fetch();
571
572
        if ($results !== false) {
573
            foreach ($results as $key => $value) {
574
                $this->$key = $value;
575
            }
576
577
            return $this;
578
        }
579
580
        return false;
581
    }
582
583
    /**
584
     * Construct an DBObject from an array
585
     * @param  array $data  associative array
586
     * @return DBObject       Built DBObject
587
     */
588
    public static function instanciate($data = [])
589
    {
590
        $calledClass    = get_called_class();
591
        $orm            = new $calledClass;
592
593
        return $orm->hydrate($data);
594
    }
595
596
    public function hydrate($data = [])
597
    {
598
        foreach ($data as $key => $val) {
599
            if ($this->propertyExists($key)) {
600
                $orm->$key = $val;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $orm seems to be never defined.
Loading history...
601
            }
602
        }
603
604
        return $this;
605
    }
606
607
    public static function create($data = [])
608
    {
609
        $obj = static::instanciate($data);
610
        $obj->save();
611
612
        return $obj;
613
    }
614
    
615
    /**
616
     * Delete record from SQL Table
617
     *
618
     * Delete record link to current object, according SQL Table unique id
619
     * @return null
620
     */
621
    public function delete()
622
    {
623
        $this->connectDB();
624
625
        if ($this->getTableIndex() !== '') {
626
            $query  = "DELETE FROM `" . $this->getTableName() . "`";
627
            $query .= " WHERE `" . $this->getTableIndex() . "` = :id";
628
629
            $queryParams = [];
630
            $queryParams['id'] = $this->{$this->getTableIndex()};
631
            
632
            $this->dbLink->query($query, $queryParams);
633
        }
634
    }
635
    
636
    /**
637
     * Save current object into db
638
     *
639
     * Call INSERT or UPDATE if unique index is set
640
     * @param  boolean $forceInsert true to force insert instead of update
641
     * @return null
642
     */
643
    public function save($forceInsert = false)
644
    {
645
        if (count($this->dbValues)) {
646
            $this->connectDB();
647
648
            if ($this->{$this->getTableIndex()} != '' && !$forceInsert) {
649
                $this->update();
650
                $insert = false;
651
            } else {
652
                $this->insert();
653
                $insert = true;
654
            }
655
656
            // Checking protected variables
657
            foreach ($this->protectedVariables as $variable) {
658
                // only if current protected_var is set
659
                if (isset($this->protectedValues[$variable]) && $this->isProtectedVariableLoaded($variable)) {
660
                    if ($this->protectedValues[$variable] instanceof Interfaces\ICollection) {
661
                        if ($insert) {
662
                            $this->protectedValues[$variable]->setParentIdForAll($this->{$this->getTableIndex()});
663
                        }
664
                        $this->protectedValues[$variable]->save();
665
                    }
666
                }
667
            }
668
        } else {
669
            throw new \RuntimeException("Object " . get_called_class() . " has no properties to save");
670
        }
671
    }
672
673
    /**
674
     * UPDATE current object into database
675
     * @return null
676
     */
677
    private function update()
678
    {
679
        $this->connectDB();
680
681
        $sqlParams = [];
682
683
        $sql  = 'UPDATE `' . $this->getTableName() . '`';
684
        $sql .= ' SET ';
685
        
686
687
        foreach ($this->dbValues as $key => $val) {
688
            if (!in_array($key, $this->readOnlyVariables)) {
689
                $sql .= ' `' . $key . '`=:' . $key .', ';
690
                $sqlParams[$key] = $val;
691
            }
692
        }
693
        $sql  = substr($sql, 0, -2);
694
        $sql .= " WHERE `" . $this->getTableIndex() . "` = :SuricateTableIndex";
695
696
        $sqlParams[':SuricateTableIndex'] = $this->{$this->getTableIndex()};
697
698
        $this->dbLink->query($sql, $sqlParams);
699
    }
700
701
    /**
702
     * INSERT current object into database
703
     * @access  private
704
     * @return null
705
     */
706
    private function insert()
707
    {
708
        $this->connectDB();
709
        
710
        $variables = array_diff($this->dbVariables, $this->readOnlyVariables);
711
712
        $sql  = 'INSERT INTO `' . $this->getTableName() . '`';
713
        $sql .= '(`';
714
        $sql .= implode('`, `', $variables);
715
        $sql .= '`)';
716
        $sql .= ' VALUES (:';
717
        $sql .= implode(', :', $variables);
718
        $sql .= ')';
719
720
        $sqlParams = [];
721
        foreach ($variables as $field) {
722
            $sqlParams[':' . $field] = $this->$field;
723
        }
724
        
725
        $this->dbLink->query($sql, $sqlParams);
726
727
        $this->{$this->getTableIndex()} = $this->dbLink->lastInsertId();
728
    }
729
    
730
    protected function connectDB()
731
    {
732
        if (!$this->dbLink) {
733
            $this->dbLink = Suricate::Database();
0 ignored issues
show
Bug introduced by
The method Database() does not exist on Suricate\Suricate. Since you implemented __callStatic, consider adding a @method annotation. ( Ignorable by Annotation )

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

733
            /** @scrutinizer ignore-call */ 
734
            $this->dbLink = Suricate::Database();
Loading history...
734
            if (static::DB_CONFIG != '') {
0 ignored issues
show
introduced by
The condition static::DB_CONFIG != '' is always false.
Loading history...
735
                $this->dbLink->setConfig(static::DB_CONFIG);
736
            }
737
        }
738
    }
739
    
740
    
741
    protected function accessToProtectedVariable($name)
0 ignored issues
show
Unused Code introduced by
The parameter $name is not used and could be removed. ( Ignorable by Annotation )

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

741
    protected function accessToProtectedVariable(/** @scrutinizer ignore-unused */ $name)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
742
    {
743
        return false;
744
    }
745
746
    public function validate()
747
    {
748
        return true;
749
    }
750
751
    public function getValidatorMessages()
752
    {
753
        return $this->validatorMessages;
754
    }
755
}
756