Completed
Push — master ( a0843d...33473b )
by Mathieu
01:26
created

DBObject::connectDB()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
nc 3
nop 0
dl 0
loc 9
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
    /*
19
    * @const TABLE_NAME : Linked SQL Table
20
    */
21
    const TABLE_NAME    = '';
22
23
    /*
24
    * @const TABLE_INDEX : Unique Id of the SQL Table
25
    */
26
    const TABLE_INDEX   = '';
27
28
    /**
29
     * @const DB_CONFIG : Database configuration identifier
30
     */
31
    const DB_CONFIG     = '';
32
33
    /**
34
     * @const RELATION_ONE_ONE : Relation one to one
35
     */
36
    const RELATION_ONE_ONE      = 1;
37
    /**
38
     * @const RELATION_ONE_MANY : Relation one to many
39
     */
40
    const RELATION_ONE_MANY     = 2;
41
    /**
42
     * @const RELATION_MANY_MANY : Relation many to many
43
     */
44
    const RELATION_MANY_MANY    = 3;
45
46
    protected $dbVariables                  = [];
47
    protected $dbValues                     = [];
48
    
49
    protected $protectedVariables           = [];
50
    protected $protectedValues              = [];
51
    protected $loadedProtectedVariables     = [];
52
53
    protected $readOnlyVariables            = [];
54
55
    protected $relations                    = [];
56
    protected $relationValues               = [];
57
    protected $loadedRelations              = [];
58
59
    protected $exportedVariables            = [];
60
61
    protected $dbLink                       = false;
62
63
    protected $validatorMessages            = [];
64
    
65
66
    public function __construct()
67
    {
68
        $this->setRelations();
69
    }
70
    /**
71
     * Magic getter
72
     *
73
     * Try to get object property according this order :
74
     * <ul>
75
     *     <li>$dbVariable</li>
76
     *     <li>$protectedVariable (triggger call to accessToProtectedVariable()
77
     *         if not already loaded)</li>
78
     * </ul>
79
     *
80
     * @param  string $name     Property name
81
     * @return Mixed            Property value
82
     */
83
    public function __get($name)
84
    {
85
        if ($this->isDBVariable($name)) {
86
            return $this->getDBVariable($name);
87
        } elseif ($this->isProtectedVariable($name)) {
88
            return $this->getProtectedVariable($name);
89
        } elseif ($this->isRelation($name)) {
90
            return $this->getRelation($name);
91
        } elseif (!empty($this->$name)) {
92
            return $this->$name;
93
        }
94
95
        throw new \InvalidArgumentException('Undefined property ' . $name);
96
    }
97
98
        /**
99
     * Magic setter
100
     *
101
     * Set a property to defined value
102
     * Assignment in this order :
103
     * - $dbVariable
104
     * - $protectedVariable
105
     *  </ul>
106
     * @param string $name  variable name
107
     * @param mixed $value variable value
108
     */
109
    public function __set($name, $value)
110
    {
111
        if ($this->isDBVariable($name)) {
112
            $this->dbValues[$name] = $value;
113
        } elseif ($this->isProtectedVariable($name)) {
114
            $this->protectedValues[$name] = $value;
115
        } elseif ($this->isRelation($name)) {
116
            $this->relationValues[$name] = $value;
117
        } else {
118
            $this->$name = $value;
119
        }
120
    }
121
122
    public function __isset($name)
123
    {
124
        if ($this->isDBVariable($name)) {
125
            return isset($this->dbValues[$name]);
126
        } elseif ($this->isProtectedVariable($name)) {
127
            // Load only one time protected variable automatically
128
            if (!$this->isProtectedVariableLoaded($name)) {
129
                $protectedAccessResult = $this->accessToProtectedVariable($name);
130
131
                if ($protectedAccessResult) {
132
                    $this->markProtectedVariableAsLoaded($name);
133
                }
134
            }
135
            return isset($this->protectedValues[$name]);
136
        } elseif ($this->isRelation($name)) {
137
            if (!$this->isRelationLoaded($name)) {
138
                $relationResult = $this->loadRelation($name);
139
140
                if ($relationResult) {
141
                    $this->markRelationAsLoaded($name);
142
                }
143
            }
144
            return isset($this->relationValues[$name]);
145
        }
146
147
        return false;
148
    }
149
150
    /**
151
     * __sleep magic method, permits an inherited DBObject class to be serialized
152
     * @return Array of properties to serialize
153
     */
154
    public function __sleep()
155
    {
156
        $discardedProps = ['dbLink', 'relations'];
157
        $reflection     = new \ReflectionClass($this);
158
        $props          = $reflection->getProperties();
159
        $result         = [];
160
        foreach ($props as $currentProperty) {
161
            $result[] = $currentProperty->name;
162
        }
163
        
164
        return array_diff($result, $discardedProps);
165
    }
166
167
    public function __wakeup()
168
    {
169
        $this->dbLink = false;
170
        $this->setRelations();
171
    }
172
    
173
    /**
174
     * @param string $name
175
     */
176
    private function getDBVariable($name)
177
    {
178
        if (isset($this->dbValues[$name])) {
179
            return $this->dbValues[$name];
180
        }
181
182
        return null;
183
    }
184
185
    /**
186
     * Check if variable is from DB
187
     * @param  string  $name variable name
188
     * @return boolean
189
     */
190
    public function isDBVariable($name)
191
    {
192
        return in_array($name, $this->dbVariables);
193
    }
194
195
    /**
196
     * @param string $name
197
     */
198
    private function getProtectedVariable($name)
199
    {
200
        // Variable exists, and is already loaded
201
        if (isset($this->protectedValues[$name]) && $this->isProtectedVariableLoaded($name)) {
202
            return $this->protectedValues[$name];
203
        }
204
        // Variable has not been loaded
205
        if (!$this->isProtectedVariableLoaded($name)) {
206
            if ($this->accessToProtectedVariable($name)) {
207
                $this->markProtectedVariableAsLoaded($name);
208
            }
209
        }
210
211
        if (isset($this->protectedValues[$name])) {
212
            return $this->protectedValues[$name];
213
        }
214
215
        return null;
216
    }
217
218
    /**
219
     * @param string $name
220
     */
221
    private function getRelation($name)
222
    {
223
        if (isset($this->relationValues[$name]) && $this->isRelationLoaded($name)) {
224
            return $this->relationValues[$name];
225
        }
226
227
        if (!$this->isRelationLoaded($name)) {
228
            if ($this->loadRelation($name)) {
229
                $this->markRelationAsLoaded($name);
230
            }
231
        }
232
233
        if (isset($this->relationValues[$name])) {
234
            return $this->relationValues[$name];
235
        }
236
237
        return null;
238
    }
239
240
    /**
241
     * Check if variable is predefined relation
242
     * @param  string  $name variable name
243
     * @return boolean
244
     */
245
    protected function isRelation($name)
246
    {
247
        return isset($this->relations[$name]);
248
    }
249
    /**
250
     * Define object relations
251
     *
252
     * @return DBObject
253
     */
254
    protected function setRelations()
255
    {
256
        $this->relations = [];
257
258
        return $this;
259
    }
260
261
    /**
262
     * Define object exported variables
263
     *
264
     * @return DBObject
265
     */
266
    protected function setExportedVariables()
267
    {
268
        if (count($this->exportedVariables)) {
269
            return $this;
270
        }
271
272
        $dbMappingExport = [];
273
        foreach ($this->dbVariables as $field) {
274
            $dbMappingExport[$field] = $field;
275
        }
276
        $this->exportedVariables = $dbMappingExport;
277
278
        return $this;
279
    }
280
281
    /**
282
     * Export DBObject to array
283
     *
284
     * @return array
285
     */
286
    public function toArray()
287
    {
288
        $result = [];
289
        foreach ($this->exportedVariables as $sourceName => $destinationName) {
290
            $omitEmpty  = false;
291
292
            if (strpos($destinationName, ',') !== false) {
293
                $splitted   = explode(',', $destinationName);
294
                $omitEmpty  = in_array('omitempty', $splitted);
295
                $destinationName = $splitted[0];
296
            }
297
298
            if ($destinationName === '-') {
299
                continue;
300
            }
301
302
            if ($omitEmpty && empty($this->$sourceName)) {
303
                continue;
304
            }
305
306
            $result[$destinationName] = $this->$sourceName;
307
        }
308
309
        return $result;
310
    }
311
312
    /**
313
     * Export DBObject to JSON format
314
     *
315
     * @return string
316
     */
317
    public function toJson()
318
    {
319
        return array_map('json_encode', $this->toArray());
320
    }
321
322
    /**
323
     * Mark a protected variable as loaded
324
     * @param  string $name varialbe name
325
     * @return void
326
     */
327
    public function markProtectedVariableAsLoaded($name)
328
    {
329
        if ($this->isProtectedVariable($name)) {
330
            $this->loadedProtectedVariables[$name] = true;
331
        }
332
    }
333
334
    /**
335
     * Mark a relation as loaded
336
     * @param  string $name varialbe name
337
     * @return void
338
     */
339
    protected function markRelationAsLoaded($name)
340
    {
341
        if ($this->isRelation($name)) {
342
            $this->loadedRelations[$name] = true;
343
        }
344
    }
345
     /**
346
     * Check if a relation already have been loaded
347
     * @param  string  $name Variable name
348
     * @return boolean
349
     */
350
    protected function isRelationLoaded($name)
351
    {
352
        return isset($this->loadedRelations[$name]);
353
    }
354
355
    protected function loadRelation($name)
356
    {
357
        if (isset($this->relations[$name])) {
358
            switch ($this->relations[$name]['type']) {
359
                case self::RELATION_ONE_ONE:
360
                    return $this->loadRelationOneOne($name);
361
                case self::RELATION_ONE_MANY:
362
                    return $this->loadRelationOneMany($name);
363
                case self::RELATION_MANY_MANY:
364
                    return $this->loadRelationManyMany($name);
365
            }
366
        }
367
368
        return false;
369
    }
370
371
    private function loadRelationOneOne($name)
372
    {
373
        $target = $this->relations[$name]['target'];
374
        $source = $this->relations[$name]['source'];
375
        $this->relationValues[$name] = new $target();
376
        $this->relationValues[$name]->load($this->$source);
377
        
378
        return true;
379
    }
380
381 View Code Duplication
    private function loadRelationOneMany($name)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
382
    {
383
        $target         = $this->relations[$name]['target'];
384
        $parentId       = $this->{$this->relations[$name]['source']};
385
        $parentIdField  = isset($this->relations[$name]['target_field']) ? $this->relations[$name]['target_field'] : null;
386
        $validate       = dataGet($this->relations[$name], 'validate', null);
387
        
388
        $this->relationValues[$name] = $target::loadForParentId($parentId, $parentIdField, $validate);
389
390
        return true;
391
    }
392
393 View Code Duplication
    private function loadRelationManyMany($name)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
394
    {
395
        $pivot      = $this->relations[$name]['pivot'];
396
        $sourceType = $this->relations[$name]['source_type'];
397
        $target     = dataGet($this->relations[$name], 'target');
398
        $validate   = dataGet($this->relations[$name], 'validate', null);
399
400
        $this->relationValues[$name] = $pivot::loadFor($sourceType, $this->{$this->relations[$name]['source']}, $target, $validate);
401
        
402
        return true;
403
    }
404
405
    private function resetLoadedVariables()
406
    {
407
        $this->loadedProtectedVariables = [];
408
        $this->loadedRelations          = [];
409
410
        return $this;
411
    }
412
413
    /**
414
     * Check if requested property exists
415
     *
416
     * Check in following order:
417
     * <ul>
418
     *     <li>$dbVariables</li>
419
     *     <li>$protectedVariables</li>
420
     *     <li>$relations</li>
421
     *     <li>legacy property</li>
422
     * </ul>
423
     * @param  string $property Property name
424
     * @return boolean           true if exists
425
     */
426
    public function propertyExists($property)
427
    {
428
        return $this->isDBVariable($property)
429
            || $this->isProtectedVariable($property)
430
            || $this->isRelation($property)
431
            || property_exists($this, $property);
432
    }
433
   
434
   /**
435
    * Check if variable is a protected variable
436
    * @param  string  $name variable name
437
    * @return boolean
438
    */
439
    public function isProtectedVariable($name)
440
    {
441
        return in_array($name, $this->protectedVariables);
442
    }
443
444
    
445
446
    /**
447
     * Check if a protected variable already have been loaded
448
     * @param  string  $name Variable name
449
     * @return boolean
450
     */
451
    protected function isProtectedVariableLoaded($name)
452
    {
453
        return isset($this->loadedProtectedVariables[$name]);
454
    }
455
456
    
457
    
458
    /**
459
     * Load ORM from Database
460
     * @param  mixed $id SQL Table Unique id
461
     * @return mixed     Loaded object or false on failure
462
     */
463
    public function load($id)
464
    {
465
        $this->connectDB();
466
        $this->resetLoadedVariables();
467
468
        if ($id != '') {
469
            $query  = "SELECT *";
470
            $query .= " FROM `" . static::TABLE_NAME ."`";
471
            $query .= " WHERE";
472
            $query .= "     `" . static::TABLE_INDEX . "` =  :id";
473
            
474
            $params         = [];
475
            $params['id']   = $id;
476
477
            return $this->loadFromSql($query, $params);
478
        }
479
        
480
        return $this;
481
    }
482
483
    public function isLoaded()
484
    {
485
        return $this->{static::TABLE_INDEX} !== null;
486
    }
487
488
    public function loadOrFail($id)
489
    {
490
        $this->load($id);
491
        if ($id == '' || $this->{static::TABLE_INDEX} != $id) {
492
            throw (new Exception\ModelNotFoundException)->setModel(get_called_class());
493
        } else {
494
            return $this;
495
        }
496
    }
497
498
    public static function loadOrCreate($arg)
499
    {
500
        $obj = static::loadOrInstanciate($arg);
501
        $obj->save();
502
503
        return $obj;
504
    }
505
506
    public static function loadOrInstanciate($arg)
507
    {
508
        if (!is_array($arg)) {
509
            $arg = [static::TABLE_INDEX => $arg];
510
        }
511
512
        $sql = "SELECT *";
513
        $sql .= " FROM " . static::TABLE_NAME;
514
        $sql .= " WHERE ";
515
516
        $sqlArray   = [];
517
        $params     = [];
518
        $i = 0;
519
        foreach ($arg as $key => $val) {
520
            if (is_null($val)) {
521
                $sqlArray[] = '`' . $key . '` IS :arg' . $i;
522
            } else {
523
                $sqlArray[] = '`' . $key . '`=:arg' . $i;
524
            }
525
            $params['arg' .$i] = $val;
526
            $i++;
527
        }
528
        $sql .= implode(' AND ', $sqlArray);
529
530
531
532
        $calledClass = get_called_class();
533
        $obj = new $calledClass;
534
        if (!$obj->loadFromSql($sql, $params)) {
535
            foreach ($arg as $property => $value) {
536
                $obj->$property = $value;
537
            }
538
        }
539
540
        return $obj;
541
    }
542
    
543
    /**
544
     * @param string $sql
545
     */
546
    public function loadFromSql($sql, $sqlParams = [])
547
    {
548
        $this->connectDB();
549
        $this->resetLoadedVariables();
550
        
551
        $results = $this->dbLink->query($sql, $sqlParams)->fetch();
0 ignored issues
show
Bug introduced by
The method query cannot be called on $this->dbLink (of type boolean).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
552
553
        if ($results !== false) {
554
            foreach ($results as $key => $value) {
555
                $this->$key = $value;
556
            }
557
558
            return $this;
559
        }
560
561
        return false;
562
    }
563
564
    /**
565
     * Construct an DBObject from an array
566
     * @param  array $data  associative array
567
     * @return DBObject       Built DBObject
568
     */
569
    public static function instanciate($data = [])
570
    {
571
        $calledClass    = get_called_class();
572
        $orm            = new $calledClass;
573
574
        foreach ($data as $key => $val) {
575
            if ($orm->propertyExists($key)) {
576
                $orm->$key = $val;
577
            }
578
        }
579
        
580
        return $orm;
581
    }
582
583
    public static function create($data = [])
584
    {
585
        $obj = static::instanciate($data);
586
        $obj->save();
587
588
        return $obj;
589
    }
590
    
591
    /**
592
     * Delete record from SQL Table
593
     *
594
     * Delete record link to current object, according SQL Table unique id
595
     * @return null
596
     */
597
    public function delete()
598
    {
599
        $this->connectDB();
600
601
        if (static::TABLE_INDEX != '') {
602
            $query  = "DELETE FROM `" . static::TABLE_NAME . "`";
603
            $query .= " WHERE `" . static::TABLE_INDEX . "` = :id";
604
605
            $queryParams = [];
606
            $queryParams['id'] = $this->{static::TABLE_INDEX};
607
            
608
            $this->dbLink->query($query, $queryParams);
0 ignored issues
show
Bug introduced by
The method query cannot be called on $this->dbLink (of type boolean).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
609
        }
610
    }
611
    
612
    /**
613
     * Save current object into db
614
     *
615
     * Call INSERT or UPDATE if unique index is set
616
     * @param  boolean $forceInsert true to force insert instead of update
617
     * @return null
618
     */
619
    public function save($forceInsert = false)
620
    {
621
        if (count($this->dbValues)) {
622
            $this->connectDB();
623
624
            if ($this->{static::TABLE_INDEX} != '' && !$forceInsert) {
625
                $this->update();
626
                $insert = false;
627
            } else {
628
                $this->insert();
629
                $insert = true;
630
            }
631
632
            // Checking protected variables
633
            foreach ($this->protectedVariables as $variable) {
634
                // only if current protected_var is set
635
                if (isset($this->protectedValues[$variable]) && $this->isProtectedVariableLoaded($variable)) {
636
                    if ($this->protectedValues[$variable] instanceof Interfaces\ICollection) {
637
                        if ($insert) {
638
                            $this->protectedValues[$variable]->setParentIdForAll($this->id);
0 ignored issues
show
Documentation introduced by
The property id does not exist on object<Suricate\DBObject>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
639
                        }
640
                        $this->protectedValues[$variable]->save();
641
                    }
642
                }
643
            }
644
        } else {
645
            throw new \RuntimeException("Object " . get_called_class() . " has no properties to save");
646
        }
647
    }
648
649
    /**
650
     * UPDATE current object into database
651
     * @return null
652
     */
653
    private function update()
654
    {
655
        $this->connectDB();
656
657
        $sqlParams = [];
658
659
        $sql  = 'UPDATE `' . static::TABLE_NAME . '`';
660
        $sql .= ' SET ';
661
        
662
663
        foreach ($this->dbValues as $key => $val) {
664
            if (!in_array($key, $this->readOnlyVariables)) {
665
                $sql .= ' `' . $key . '`=:' . $key .', ';
666
                $sqlParams[$key] = $val;
667
            }
668
        }
669
        $sql  = substr($sql, 0, -2);
670
        $sql .= " WHERE `" . static::TABLE_INDEX . "` = :SuricateTableIndex";
671
672
        $sqlParams[':SuricateTableIndex'] = $this->{static::TABLE_INDEX};
673
674
        $this->dbLink->query($sql, $sqlParams);
0 ignored issues
show
Bug introduced by
The method query cannot be called on $this->dbLink (of type boolean).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
675
    }
676
677
    /**
678
     * INSERT current object into database
679
     * @access  private
680
     * @return null
681
     */
682
    private function insert()
683
    {
684
        $this->connectDB();
685
        
686
        $variables = array_diff($this->dbVariables, $this->readOnlyVariables);
687
688
        $sql  = 'INSERT INTO `' . static::TABLE_NAME . '`';
689
        $sql .= '(`';
690
        $sql .= implode('`, `', $variables);
691
        $sql .= '`)';
692
        $sql .= ' VALUES (:';
693
        $sql .= implode(', :', $variables);
694
        $sql .= ')';
695
696
        $sqlParams = [];
697
        foreach ($variables as $field) {
698
            $sqlParams[':' . $field] = $this->$field;
699
        }
700
        
701
        $this->dbLink->query($sql, $sqlParams);
0 ignored issues
show
Bug introduced by
The method query cannot be called on $this->dbLink (of type boolean).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
702
703
        $this->{static::TABLE_INDEX} = $this->dbLink->lastInsertId();
0 ignored issues
show
Bug introduced by
The method lastInsertId cannot be called on $this->dbLink (of type boolean).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
704
    }
705
    
706
    protected function connectDB()
707
    {
708
        if (!$this->dbLink) {
709
            $this->dbLink = Suricate::Database();
710
            if (static::DB_CONFIG != '') {
711
                $this->dbLink->setConfig(static::DB_CONFIG);
712
            }
713
        }
714
    }
715
    
716
    
717
    protected function accessToProtectedVariable($name)
718
    {
719
        return false;
720
    }
721
722
    public function validate()
723
    {
724
        return true;
725
    }
726
727
    public function getValidatorMessages()
728
    {
729
        return $this->validatorMessages;
730
    }
731
}
732