Completed
Push — develop ( 56f424...dccc18 )
by Mathieu
01:46
created

DBObject::load()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 14
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 1

Importance

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

735
            /** @scrutinizer ignore-call */ 
736
            $this->dbLink = Suricate::Database();
Loading history...
736
            if ($this->getDBConfig() !== '') {
737
                $this->dbLink->setConfig($this->getDBConfig());
738
            }
739
        }
740 4
    }
741
    
742
    
743
    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

743
    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...
744
    {
745
        return false;
746
    }
747
748
    public function validate()
749
    {
750
        return true;
751
    }
752
753
    public function getValidatorMessages()
754
    {
755
        return $this->validatorMessages;
756
    }
757
}
758