Completed
Push — develop ( 07b44e...eeaea7 )
by Mathieu
02:25
created

DBObject::loadRelationOneMany()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 2
nop 1
dl 0
loc 8
ccs 0
cts 6
cp 0
crap 6
rs 10
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 15
    public function __construct()
61
    {
62 15
        $this->setRelations();
63 15
    }
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 7
    public function __set($name, $value)
104
    {
105 7
        if ($this->isDBVariable($name)) {
106 6
            $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 7
    }
115
116 3
    public function __isset($name)
117
    {
118 3
        if ($this->isDBVariable($name)) {
119 1
            return isset($this->dbValues[$name]);
120 3
        } 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 3
        } elseif ($this->isRelation($name)) {
131 1
            if (!$this->isRelationLoaded($name)) {
132 1
                $this->loadRelation($name);
133 1
                $this->markRelationAsLoaded($name);
134
            }
135 1
            return isset($this->relationValues[$name]);
136
        }
137
138 2
        return false;
139
    }
140
141 5
    public function getTableName()
142
    {
143 5
        return $this->tableName;
144
    }
145
146 6
    public function getTableIndex()
147
    {
148 6
        return $this->tableIndex;
149
    }
150
151 1
    public function getDBConfig()
152
    {
153 1
        return $this->DBConfig;
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 1
    public function __wakeup()
174
    {
175 1
        $this->dbLink = false;
176 1
        $this->setRelations();
177 1
    }
178
    
179
    /**
180
     * @param string $name
181
     */
182 6
    private function getDBVariable($name)
183
    {
184 6
        if (isset($this->dbValues[$name])) {
185 6
            return $this->dbValues[$name];
186
        }
187
188 3
        return null;
189
    }
190
191
    /**
192
     * Check if variable is from DB
193
     * @param  string  $name variable name
194
     * @return boolean
195
     */
196 10
    public function isDBVariable($name)
197
    {
198 10
        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 1
    protected function getRelation($name)
228
    {
229 1
        if (isset($this->relationValues[$name]) && $this->isRelationLoaded($name)) {
230 1
            return $this->relationValues[$name];
231
        }
232
233 1
        if (!$this->isRelationLoaded($name)) {
234 1
            $this->loadRelation($name);
235 1
            $this->markRelationAsLoaded($name);
236
        }
237
238 1
        if (isset($this->relationValues[$name])) {
239 1
            return $this->relationValues[$name];
240
        }
241
242
        return null;
243
    }
244
245
    /**
246
     * Check if variable is predefined relation
247
     * @param  string  $name variable name
248
     * @return boolean
249
     */
250 5
    protected function isRelation($name)
251
    {
252 5
        return isset($this->relations[$name]);
253
    }
254
    /**
255
     * Define object relations
256
     *
257
     * @return DBObject
258
     */
259 13
    protected function setRelations()
260
    {
261 13
        $this->relations = [];
262
263 13
        return $this;
264
    }
265
266
    /**
267
     * Define object exported variables
268
     *
269
     * @return DBObject
270
     */
271 2
    protected function setExportedVariables()
272
    {
273 2
        if (count($this->exportedVariables)) {
274 1
            return $this;
275
        }
276
277 2
        $dbMappingExport = [];
278 2
        foreach ($this->dbVariables as $field) {
279 2
            $dbMappingExport[$field] = $field;
280
        }
281 2
        $this->exportedVariables = $dbMappingExport;
282
283 2
        return $this;
284
    }
285
286
    /**
287
     * Export DBObject to array
288
     *
289
     * @return array
290
     */
291 2
    public function toArray()
292
    {
293 2
        $this->setExportedVariables();
294 2
        $result = [];
295 2
        foreach ($this->exportedVariables as $sourceName => $destinationName) {
296 2
            $omitEmpty  = false;
297 2
            $castType   = null;
298 2
            if (strpos($destinationName, ',') !== false) {
299 1
                $splitted   = explode(',', $destinationName);
300 1
                array_map(function ($item) use (&$castType, &$omitEmpty) {
301 1
                    if ($item === 'omitempty') {
302
                        $omitEmpty = true;
303
                        return;
304
                    }
305 1
                    if (substr($item, 0, 5) === 'type:') {
306 1
                        $castType = substr($item, 5);
307
                    }
308 1
                }, $splitted);
309
310 1
                $destinationName = $splitted[0];
311
            }
312
313 2
            if ($destinationName === '-') {
314 1
                continue;
315
            }
316
317 2
            if ($omitEmpty && empty($this->$sourceName)) {
318
                continue;
319
            }
320 2
            $value = $this->$sourceName;
321 2
            if ($castType !== null) {
322 1
                settype($value, $castType);
323
            }
324 2
            $result[$destinationName] = $value;
325
        }
326
327 2
        return $result;
328
    }
329
330
    /**
331
     * Export DBObject to JSON format
332
     *
333
     * @return string
334
     */
335 1
    public function toJson()
336
    {
337 1
        return json_encode($this->toArray());
338
    }
339
340
    /**
341
     * Mark a protected variable as loaded
342
     * @param  string $name varialbe name
343
     *
344
     * @return DBObject
345
     */
346
    public function markProtectedVariableAsLoaded($name)
347
    {
348
        if ($this->isProtectedVariable($name)) {
349
            $this->loadedProtectedVariables[$name] = true;
350
        }
351
352
        return $this;
353
    }
354
355
    /**
356
     * Mark a relation as loaded
357
     * @param  string $name varialbe name
358
     * @return void
359
     */
360 1
    protected function markRelationAsLoaded($name)
361
    {
362 1
        if ($this->isRelation($name)) {
363 1
            $this->loadedRelations[$name] = true;
364
        }
365 1
    }
366
     /**
367
     * Check if a relation already have been loaded
368
     * @param  string  $name Variable name
369
     * @return boolean
370
     */
371 1
    protected function isRelationLoaded($name)
372
    {
373 1
        return isset($this->loadedRelations[$name]);
374
    }
375
376
    /**
377
     * Load realation according to relation type
378
     *
379
     * @param string $name
380
     * @return void
381
     */
382 1
    protected function loadRelation($name)
383
    {
384 1
        if ($this->isRelation($name)) {
385 1
            switch ($this->relations[$name]['type']) {
386 1
                case self::RELATION_ONE_ONE:
387 1
                    $this->loadRelationOneOne($name);
388 1
                    return;
389
                case self::RELATION_ONE_MANY:
390
                    $this->loadRelationOneMany($name);
391
                    return;
392
                case self::RELATION_MANY_MANY:
393
                    $this->loadRelationManyMany($name);
394
                    return;
395
            }
396
        }
397
    }
398
399
    /**
400
     * Load one to one relationship
401
     *
402
     * @param string $name
403
     * @return void
404
     */
405 1
    private function loadRelationOneOne($name)
406
    {
407 1
        $target = $this->relations[$name]['target'];
408 1
        $source = $this->relations[$name]['source'];
409 1
        $this->relationValues[$name] = new $target();
410 1
        $this->relationValues[$name]->load($this->$source);
411 1
    }
412
413
    /**
414
     * Load one to many relationship
415
     *
416
     * @param string $name
417
     * @return void
418
     */
419
    private function loadRelationOneMany($name)
420
    {
421
        $target         = $this->relations[$name]['target'];
422
        $parentId       = $this->{$this->relations[$name]['source']};
423
        $parentIdField  = isset($this->relations[$name]['target_field']) ? $this->relations[$name]['target_field'] : null;
424
        $validate       = dataGet($this->relations[$name], 'validate', null);
425
        
426
        $this->relationValues[$name] = $target::loadForParentId($parentId, $parentIdField, $validate);
427
    }
428
429
    /**
430
     * Load many to many relationship
431
     *
432
     * @param string $name
433
     * @return void
434
     */
435
    private function loadRelationManyMany($name)
436
    {
437
        $pivot      = $this->relations[$name]['pivot'];
438
        $sourceType = $this->relations[$name]['source_type'];
439
        $target     = dataGet($this->relations[$name], 'target');
440
        $validate   = dataGet($this->relations[$name], 'validate', null);
441
442
        $this->relationValues[$name] = $pivot::loadFor($sourceType, $this->{$this->relations[$name]['source']}, $target, $validate);
443
    }
444
445 4
    private function resetLoadedVariables()
446
    {
447 4
        $this->loadedProtectedVariables = [];
448 4
        $this->loadedRelations          = [];
449
450 4
        return $this;
451
    }
452
453
    /**
454
     * Check if requested property exists
455
     *
456
     * Check in following order:
457
     * <ul>
458
     *     <li>$dbVariables</li>
459
     *     <li>$protectedVariables</li>
460
     *     <li>$relations</li>
461
     *     <li>legacy property</li>
462
     * </ul>
463
     * @param  string $property Property name
464
     * @return boolean           true if exists
465
     */
466 3
    public function propertyExists($property)
467
    {
468 3
        return $this->isDBVariable($property)
469 2
            || $this->isProtectedVariable($property)
470 2
            || $this->isRelation($property)
471 3
            || property_exists($this, $property);
472
    }
473
   
474
   /**
475
    * Check if variable is a protected variable
476
    * @param  string  $name variable name
477
    * @return boolean
478
    */
479 5
    public function isProtectedVariable($name)
480
    {
481 5
        return in_array($name, $this->protectedVariables);
482
    }
483
484
    
485
486
    /**
487
     * Check if a protected variable already have been loaded
488
     * @param  string  $name Variable name
489
     * @return boolean
490
     */
491
    protected function isProtectedVariableLoaded($name)
492
    {
493
        return isset($this->loadedProtectedVariables[$name]);
494
    }
495
496
    
497
    
498
    /**
499
     * Load ORM from Database
500
     * @param  mixed $id SQL Table Unique id
501
     * @return mixed     Loaded object or false on failure
502
     */
503 4
    public function load($id)
504
    {
505 4
        $this->connectDB();
506 4
        $this->resetLoadedVariables();
507
508 4
        $query  = "SELECT *";
509 4
        $query .= " FROM `" . $this->getTableName() ."`";
510 4
        $query .= " WHERE";
511 4
        $query .= "     `" . $this->getTableIndex() . "` =  :id";
512
        
513 4
        $params         = [];
514 4
        $params['id']   = $id;
515
516 4
        return $this->loadFromSql($query, $params);
517
    }
518
519 2
    public function isLoaded()
520
    {
521 2
        return $this->{$this->getTableIndex()} !== null;
522
    }
523
524
    public function loadOrFail($id)
525
    {
526
        $this->load($id);
527
        if ($id == '' || $this->{$this->getTableIndex()} != $id) {
528
            throw (new Exception\ModelNotFoundException)->setModel(get_called_class());
529
        }
530
531
        return $this;
532
    }
533
534
    public static function loadOrCreate($arg)
535
    {
536
        $obj = static::loadOrInstanciate($arg);
537
        $obj->save();
538
539
        return $obj;
540
    }
541
542
    public static function loadOrInstanciate($arg)
543
    {
544
        $calledClass = get_called_class();
545
        $obj = new $calledClass;
546
        
547
        if (!is_array($arg)) {
548
            $arg = [$obj->getTableIndex() => $arg];
549
        }
550
        
551
552
        $sql = "SELECT *";
553
        $sql .= " FROM `" . $obj->getTableName() . "`";
554
        $sql .= " WHERE ";
555
556
        $sqlArray   = [];
557
        $params     = [];
558
        $i = 0;
559
        foreach ($arg as $key => $val) {
560
            if (is_null($val)) {
561
                $sqlArray[] = '`' . $key . '` IS :arg' . $i;
562
            } else {
563
                $sqlArray[] = '`' . $key . '`=:arg' . $i;
564
            }
565
            $params['arg' .$i] = $val;
566
            $i++;
567
        }
568
        $sql .= implode(' AND ', $sqlArray);
569
570
        if (!$obj->loadFromSql($sql, $params)) {
571
            foreach ($arg as $property => $value) {
572
                $obj->$property = $value;
573
            }
574
        }
575
576
        return $obj;
577
    }
578
    
579
    /**
580
     * @param string $sql
581
     */
582 4
    public function loadFromSql($sql, $sqlParams = [])
583
    {
584 4
        $this->connectDB();
585 4
        $this->resetLoadedVariables();
586
        
587 4
        $results = $this->dbLink->query($sql, $sqlParams)->fetch();
588
589 4
        if ($results !== false) {
590 4
            foreach ($results as $key => $value) {
591 4
                $this->$key = $value;
592
            }
593
594 4
            return $this;
595
        }
596
597
        return false;
598
    }
599
600
    /**
601
     * Construct an DBObject from an array
602
     * @param  array $data  associative array
603
     * @return DBObject       Built DBObject
604
     */
605 1
    public static function instanciate($data = [])
606
    {
607 1
        $calledClass    = get_called_class();
608 1
        $orm            = new $calledClass;
609
610 1
        return $orm->hydrate($data);
611
    }
612
613 2
    public function hydrate($data = [])
614
    {
615 2
        foreach ($data as $key => $val) {
616 2
            if ($this->propertyExists($key)) {
617 2
                $this->$key = $val;
618
            }
619
        }
620
621 2
        return $this;
622
    }
623
624
    public static function create($data = [])
625
    {
626
        $obj = static::instanciate($data);
627
        $obj->save();
628
629
        return $obj;
630
    }
631
    
632
    /**
633
     * Delete record from SQL Table
634
     *
635
     * Delete record link to current object, according SQL Table unique id
636
     * @return null
637
     */
638
    public function delete()
639
    {
640
        $this->connectDB();
641
642
        if ($this->getTableIndex() !== '') {
643
            $query  = "DELETE FROM `" . $this->getTableName() . "`";
644
            $query .= " WHERE `" . $this->getTableIndex() . "` = :id";
645
646
            $queryParams = [];
647
            $queryParams['id'] = $this->{$this->getTableIndex()};
648
            
649
            $this->dbLink->query($query, $queryParams);
650
        }
651
    }
652
    
653
    /**
654
     * Save current object into db
655
     *
656
     * Call INSERT or UPDATE if unique index is set
657
     * @param  boolean $forceInsert true to force insert instead of update
658
     * @return null
659
     */
660
    public function save($forceInsert = false)
661
    {
662
        if (count($this->dbValues)) {
663
            $this->connectDB();
664
665
            if ($this->{$this->getTableIndex()} != '' && !$forceInsert) {
666
                $this->update();
667
                $insert = false;
668
            } else {
669
                $this->insert();
670
                $insert = true;
671
            }
672
673
            // Checking protected variables
674
            foreach ($this->protectedVariables as $variable) {
675
                // only if current protected_var is set
676
                if (isset($this->protectedValues[$variable]) && $this->isProtectedVariableLoaded($variable)) {
677
                    if ($this->protectedValues[$variable] instanceof Interfaces\ICollection) {
678
                        if ($insert) {
679
                            $this->protectedValues[$variable]->setParentIdForAll($this->{$this->getTableIndex()});
680
                        }
681
                        $this->protectedValues[$variable]->save();
682
                    }
683
                }
684
            }
685
        } else {
686
            throw new \RuntimeException("Object " . get_called_class() . " has no properties to save");
687
        }
688
    }
689
690
    /**
691
     * UPDATE current object into database
692
     * @return null
693
     */
694
    private function update()
695
    {
696
        $this->connectDB();
697
698
        $sqlParams = [];
699
700
        $sql  = 'UPDATE `' . $this->getTableName() . '`';
701
        $sql .= ' SET ';
702
        
703
704
        foreach ($this->dbValues as $key => $val) {
705
            if (!in_array($key, $this->readOnlyVariables)) {
706
                $sql .= ' `' . $key . '`=:' . $key .', ';
707
                $sqlParams[$key] = $val;
708
            }
709
        }
710
        $sql  = substr($sql, 0, -2);
711
        $sql .= " WHERE `" . $this->getTableIndex() . "` = :SuricateTableIndex";
712
713
        $sqlParams[':SuricateTableIndex'] = $this->{$this->getTableIndex()};
714
715
        $this->dbLink->query($sql, $sqlParams);
716
    }
717
718
    /**
719
     * INSERT current object into database
720
     * @access  private
721
     * @return null
722
     */
723
    private function insert()
724
    {
725
        $this->connectDB();
726
        
727
        $variables = array_diff($this->dbVariables, $this->readOnlyVariables);
728
729
        $sql  = 'INSERT INTO `' . $this->getTableName() . '`';
730
        $sql .= '(`';
731
        $sql .= implode('`, `', $variables);
732
        $sql .= '`)';
733
        $sql .= ' VALUES (:';
734
        $sql .= implode(', :', $variables);
735
        $sql .= ')';
736
737
        $sqlParams = [];
738
        foreach ($variables as $field) {
739
            $sqlParams[':' . $field] = $this->$field;
740
        }
741
        
742
        $this->dbLink->query($sql, $sqlParams);
743
744
        $this->{$this->getTableIndex()} = $this->dbLink->lastInsertId();
745
    }
746
    
747 4
    protected function connectDB()
748
    {
749 4
        if (!$this->dbLink) {
750
            $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

750
            /** @scrutinizer ignore-call */ 
751
            $this->dbLink = Suricate::Database();
Loading history...
751
            if ($this->getDBConfig() !== '') {
752
                $this->dbLink->setConfig($this->getDBConfig());
753
            }
754
        }
755 4
    }
756
    
757
    
758
    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

758
    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...
759
    {
760
        return false;
761
    }
762
763
    public function validate()
764
    {
765
        return true;
766
    }
767
768
    public function getValidatorMessages()
769
    {
770
        return $this->validatorMessages;
771
    }
772
}
773