Completed
Push — master ( e45ba3...adc5c9 )
by Mathieu
01:38
created

DBObject::__set()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 12
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 9
nc 4
nop 2
dl 0
loc 12
rs 9.2
c 1
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 $dbLink                       = false;
60
61
    protected $validatorMessages            = [];
62
    
63
64
    public function __construct()
65
    {
66
        $this->setRelations();
67
    }
68
    /**
69
     * Magic getter
70
     *
71
     * Try to get object property according this order :
72
     * <ul>
73
     *     <li>$dbVariable</li>
74
     *     <li>$protectedVariable (triggger call to accessToProtectedVariable()
75
     *         if not already loaded)</li>
76
     * </ul>
77
     *
78
     * @param  string $name     Property name
79
     * @return Mixed            Property value
80
     */
81
    public function __get($name)
82
    {
83
        if ($this->isDBVariable($name)) {
84
            return $this->getDBVariable($name);
85
        } elseif ($this->isProtectedVariable($name)) {
86
            return $this->getProtectedVariable($name);
87
        } elseif ($this->isRelation($name)) {
88
            return $this->getRelation($name);
89
        } elseif (!empty($this->$name)) {
90
            return $this->$name;
91
        } else {
92
            throw new \InvalidArgumentException('Undefined property ' . $name);
93
        }
94
    }
95
96
        /**
97
     * Magic setter
98
     *
99
     * Set a property to defined value
100
     * Assignment in this order :
101
     * - $dbVariable
102
     * - $protectedVariable
103
     *  </ul>
104
     * @param string $name  variable name
105
     * @param mixed $value variable value
106
     */
107
    public function __set($name, $value)
108
    {
109
        if ($this->isDBVariable($name)) {
110
            $this->dbValues[$name] = $value;
111
        } elseif ($this->isProtectedVariable($name)) {
112
            $this->protectedValues[$name] = $value;
113
        } elseif ($this->isRelation($name)) {
114
            $this->relationValues[$name] = $value;
115
        } else {
116
            $this->$name = $value;
117
        }
118
    }
119
120
    public function __isset($name)
121
    {
122
        if ($this->isDBVariable($name)) {
123
            return isset($this->dbValues[$name]);
124
        } elseif ($this->isProtectedVariable($name)) {
125
            // Load only one time protected variable automatically
126
            if (!$this->isProtectedVariableLoaded($name)) {
127
                $protectedAccessResult = $this->accessToProtectedVariable($name);
128
129
                if ($protectedAccessResult) {
130
                    $this->markProtectedVariableAsLoaded($name);
131
                }
132
            }
133
            return isset($this->protectedValues[$name]);
134
        } elseif ($this->isRelation($name)) {
135
            if (!$this->isRelationLoaded($name)) {
136
                $relationResult = $this->loadRelation($name);
137
138
                if ($relationResult) {
139
                    $this->markRelationAsLoaded($name);
140
                }
141
            }
142
            return isset($this->relationValues[$name]);
143
        } else {
144
            return false;
145
        }
146
    }
147
148
    /**
149
     * __sleep magic method, permits an inherited DBObject class to be serialized
150
     * @return Array of properties to serialize
151
     */
152
    public function __sleep()
153
    {
154
        $this->dbLink   = false;
155
        $this->relations= [];
156
        $reflection     = new \ReflectionClass($this);
157
        $props          = $reflection->getProperties();
158
        $result         = [];
159
        foreach ($props as $currentProperty) {
160
            $result[] = $currentProperty->name;
161
        }
162
163
        return $result;
164
    }
165
166
    public function __wakeup()
167
    {
168
        $this->setRelations();
169
    }
170
    
171
    private function getDBVariable($name)
172
    {
173
        if (isset($this->dbValues[$name])) {
174
            return $this->dbValues[$name];
175
        }
176
177
        return null;
178
    }
179
180
    /**
181
     * Check if variable is from DB
182
     * @param  string  $name variable name
183
     * @return boolean
184
     */
185
    public function isDBVariable($name)
186
    {
187
        return in_array($name, $this->dbVariables);
188
    }
189
190
    private function getProtectedVariable($name)
191
    {
192
        // Variable exists, and is already loaded
193
        if (isset($this->protectedValues[$name]) && $this->isProtectedVariableLoaded($name)) {
194
            return $this->protectedValues[$name];
195
        }
196
        // Variable has not been loaded
197
        if (!$this->isProtectedVariableLoaded($name)) {
198
            if ($this->accessToProtectedVariable($name)) {
199
                $this->markProtectedVariableAsLoaded($name);
200
            }
201
        }
202
203
        if (isset($this->protectedValues[$name])) {
204
            return $this->protectedValues[$name];
205
        }
206
207
        return null;
208
    }
209
210
    private function getRelation($name)
211
    {
212
        if (isset($this->relationValues[$name]) && $this->isRelationLoaded($name)) {
213
            return $this->relationValues[$name];
214
        }
215
216
        if (!$this->isRelationLoaded($name)) {
217
            if ($this->loadRelation($name)) {
218
                $this->markRelationAsLoaded($name);
219
            }
220
        }
221
222
        if (isset($this->relationValues[$name])) {
223
            return $this->relationValues[$name];
224
        }
225
226
        return null;
227
    }
228
229
    /**
230
     * Check if variable is predefined relation
231
     * @param  string  $name variable name
232
     * @return boolean
233
     */
234
    protected function isRelation($name)
235
    {
236
        return isset($this->relations[$name]);
237
    }
238
    /**
239
     * Define object relations
240
     *
241
     * @return object
242
     */
243
    protected function setRelations()
244
    {
245
        $this->relations = [];
246
247
        return $this;
248
    }
249
250
    /**
251
     * Mark a protected variable as loaded
252
     * @param  string $name varialbe name
253
     * @return void
254
     */
255
    public function markProtectedVariableAsLoaded($name)
256
    {
257
        if ($this->isProtectedVariable($name)) {
258
            $this->loadedProtectedVariables[$name] = true;
259
        }
260
    }
261
262
    /**
263
     * Mark a relation as loaded
264
     * @param  string $name varialbe name
265
     * @return void
266
     */
267
    protected function markRelationAsLoaded($name)
268
    {
269
        if ($this->isRelation($name)) {
270
            $this->loadedRelations[$name] = true;
271
        }
272
    }
273
     /**
274
     * Check if a relation already have been loaded
275
     * @param  string  $name Variable name
276
     * @return boolean
277
     */
278
    protected function isRelationLoaded($name)
279
    {
280
        return isset($this->loadedRelations[$name]);
281
    }
282
283
    protected function loadRelation($name)
284
    {
285
        if (isset($this->relations[$name])) {
286
            switch ($this->relations[$name]['type']) {
287
                case self::RELATION_ONE_ONE:
288
                    return $this->loadRelationOneOne($name);
289
                case self::RELATION_ONE_MANY:
290
                    return $this->loadRelationOneMany($name);
291
                case self::RELATION_MANY_MANY:
292
                    return $this->loadRelationManyMany($name);
293
            }
294
        }
295
296
        return false;
297
    }
298
299
    private function loadRelationOneOne($name)
300
    {
301
        $target = $this->relations[$name]['target'];
302
        $source = $this->relations[$name]['source'];
303
        $this->relationValues[$name] = new $target();
304
        $this->relationValues[$name]->load($this->$source);
305
        
306
        return true;
307
    }
308
309 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...
310
    {
311
        $target         = $this->relations[$name]['target'];
312
        $parentId       = $this->{$this->relations[$name]['source']};
313
        $parentIdField  = isset($this->relations[$name]['target_field']) ? $this->relations[$name]['target_field'] : null;
314
        $validate       = dataGet($this->relations[$name], 'validate', null);
315
        
316
        $this->relationValues[$name] = $target::loadForParentId($parentId, $parentIdField, $validate);
317
318
        return true;
319
    }
320
321 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...
322
    {
323
        $pivot      = $this->relations[$name]['pivot'];
324
        $sourceType = $this->relations[$name]['source_type'];
325
        $target     = dataGet($this->relations[$name], 'target');
326
        $validate   = dataGet($this->relations[$name], 'validate', null);
327
328
        $this->relationValues[$name] = $pivot::loadFor($sourceType, $this->{$this->relations[$name]['source']}, $target, $validate);
329
        
330
        return true;
331
    }
332
333
    private function resetLoadedVariables()
334
    {
335
        $this->loadedProtectedVariables = [];
336
        $this->loadedRelations          = [];
337
338
        return $this;
339
    }
340
341
    /**
342
     * Check if requested property exists
343
     *
344
     * Check in following order:
345
     * <ul>
346
     *     <li>$dbVariables</li>
347
     *     <li>$protectedVariables</li>
348
     *     <li>$relations</li>
349
     *     <li>legacy property</li>
350
     * </ul>
351
     * @param  string $property Property name
352
     * @return boolean           true if exists
353
     */
354
    public function propertyExists($property)
355
    {
356
        return $this->isDBVariable($property)
357
            || $this->isProtectedVariable($property)
358
            || $this->isRelation($property)
359
            || property_exists($this, $property);
360
    }
361
   
362
   /**
363
    * Check if variable is a protected variable
364
    * @param  string  $name variable name
365
    * @return boolean
366
    */
367
    public function isProtectedVariable($name)
368
    {
369
        return in_array($name, $this->protectedVariables);
370
    }
371
372
    
373
374
    /**
375
     * Check if a protected variable already have been loaded
376
     * @param  string  $name Variable name
377
     * @return boolean
378
     */
379
    protected function isProtectedVariableLoaded($name)
380
    {
381
        return isset($this->loadedProtectedVariables[$name]);
382
    }
383
384
    
385
    
386
    /**
387
     * Load ORM from Database
388
     * @param  mixed $id SQL Table Unique id
389
     * @return mixed     Loaded object or false on failure
390
     */
391
    public function load($id)
392
    {
393
        $this->connectDB();
394
        $this->resetLoadedVariables();
395
396
        if ($id != '') {
397
            $query  = "SELECT *";
398
            $query .= " FROM `" . static::TABLE_NAME ."`";
399
            $query .= " WHERE";
400
            $query .= "     `" . static::TABLE_INDEX . "` =  :id";
401
            
402
            $params         = [];
403
            $params['id']   = $id;
404
405
            return $this->loadFromSql($query, $params);
406
        }
407
        
408
        return $this;
409
    }
410
411
    public function isLoaded()
412
    {
413
        return $this->{static::TABLE_INDEX} !== null;
414
    }
415
416
    public function loadOrFail($id)
417
    {
418
        $this->load($id);
419
        if ($id == '' || $this->{static::TABLE_INDEX} != $id) {
420
            throw (new Exception\ModelNotFoundException)->setModel(get_called_class());
421
        } else {
422
            return $this;
423
        }
424
    }
425
426
    public static function loadOrCreate($arg)
427
    {
428
        $obj = static::loadOrInstanciate($arg);
429
        $obj->save();
430
431
        return $obj;
432
    }
433
434
    public static function loadOrInstanciate($arg)
435
    {
436
        if (!is_array($arg)) {
437
            $arg = [static::TABLE_INDEX => $arg];
438
        }
439
440
        $sql = "SELECT *";
441
        $sql .= " FROM " . static::TABLE_NAME;
442
        $sql .= " WHERE ";
443
444
        $sqlArray   = [];
445
        $params     = [];
446
        $i = 0;
447
        foreach ($arg as $key => $val) {
448
            if (is_null($val)) {
449
                $sqlArray[] = '`' . $key . '` IS :arg' . $i;
450
            } else {
451
                $sqlArray[] = '`' . $key . '`=:arg' . $i;    
452
            }
453
            $params['arg' .$i] = $val;
454
            $i++;
455
        }
456
        $sql .= implode(' AND ', $sqlArray);
457
458
459
460
        $calledClass = get_called_class();
461
        $obj = new $calledClass;
462
        if (!$obj->loadFromSql($sql, $params)) {
463
            foreach($arg as $property => $value) {
464
                $obj->$property = $value;
465
            }
466
        }
467
468
        return $obj;
469
    }
470
    
471
    public function loadFromSql($sql, $sql_params = [])
0 ignored issues
show
Coding Style Naming introduced by
The parameter $sql_params is not named in camelCase.

This check marks parameter names that have not been written in camelCase.

In camelCase names are written without any punctuation, the start of each new word being marked by a capital letter. Thus the name database connection string becomes databaseConnectionString.

Loading history...
472
    {
473
        $this->connectDB();
474
        $this->resetLoadedVariables();
475
        
476
        $results = $this->dbLink->query($sql, $sql_params)->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...
477
478
        if ($results !== false) {
479
            foreach ($results as $key => $value) {
480
                $this->$key = $value;
481
            }
482
483
            return $this;
484
        }
485
486
        return false;
487
    }
488
489
    /**
490
     * Construct an DBObject from an array
491
     * @param  array $data  associative array
492
     * @return DBObject       Built DBObject
493
     */
494
    public static function instanciate($data = [])
495
    {
496
        $calledClass    = get_called_class();
497
        $orm            = new $calledClass;
498
499
        foreach ($data as $key => $val) {
500
            if ($orm->propertyExists($key)) {
501
                $orm->$key = $val;
502
            }
503
        }
504
        
505
        return $orm;
506
    }
507
508
    public static function create($data = [])
509
    {
510
        $obj = static::instanciate($data);
511
        $obj->save();
512
513
        return $obj;
514
    }
515
    
516
    /**
517
     * Delete record from SQL Table
518
     *
519
     * Delete record link to current object, according SQL Table unique id
520
     * @return null
521
     */
522
    public function delete()
523
    {
524
        $this->connectDB();
525
526
        if (static::TABLE_INDEX != '') {
527
            $query  = "DELETE FROM `" . static::TABLE_NAME . "`";
528
            $query .= " WHERE `" . static::TABLE_INDEX . "` = :id";
529
530
            $queryParams = [];
531
            $queryParams['id'] = $this->{static::TABLE_INDEX};
532
            
533
            $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...
534
        }
535
    }
536
    
537
    /**
538
     * Save current object into db
539
     *
540
     * Call INSERT or UPDATE if unique index is set
541
     * @param  boolean $forceInsert true to force insert instead of update
542
     * @return null
543
     */
544
    public function save($forceInsert = false)
545
    {
546
        if (count($this->dbValues)) {
547
            $this->connectDB();
548
549
            if ($this->{static::TABLE_INDEX} != '' && !$forceInsert) {
550
                $this->update();
551
                $insert = false;
552
            } else {
553
                $this->insert();
554
                $insert = true;
555
            }
556
557
            // Checking protected variables
558
            foreach ($this->protectedVariables as $variable) {
559
                // only if current protected_var is set
560
                if (isset($this->protectedValues[$variable]) && $this->isProtectedVariableLoaded($variable)) {
561
                    if ($this->protectedValues[$variable] instanceof Interfaces\ICollection) {
562
                        if ($insert) {
563
                            $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...
564
                        }
565
                        $this->protectedValues[$variable]->save();
566
                    }
567
                }
568
            }
569
        } else {
570
            throw new \RuntimeException("Object " . get_called_class() . " has no properties to save");
571
        }
572
    }
573
574
    /**
575
     * UPDATE current object into database
576
     * @return null
577
     */
578
    private function update()
579
    {
580
        $this->connectDB();
581
582
        $sqlParams = [];
583
584
        $sql  = 'UPDATE `' . static::TABLE_NAME . '`';
585
        $sql .= ' SET ';
586
        
587
588
        foreach ($this->dbValues as $key => $val) {
589
            if (!in_array($key, $this->readOnlyVariables)) {
590
                $sql .= ' `' . $key . '`=:' . $key .', ';
591
                $sqlParams[$key] = $val;
592
            }
593
        }
594
        $sql  = substr($sql, 0, -2);
595
        $sql .= " WHERE `" . static::TABLE_INDEX . "` = :SuricateTableIndex";
596
597
        $sqlParams[':SuricateTableIndex'] = $this->{static::TABLE_INDEX};
598
599
        $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...
600
    }
601
602
    /**
603
     * INSERT current object into database
604
     * @access  private
605
     * @return null
606
     */
607
    private function insert()
608
    {
609
        $this->connectDB();
610
        
611
        $variables = array_diff($this->dbVariables, $this->readOnlyVariables);
612
613
        $sql  = 'INSERT INTO `' . static::TABLE_NAME . '`';
614
        $sql .= '(`';
615
        $sql .= implode('`, `', $variables);
616
        $sql .= '`)';
617
        $sql .= ' VALUES (:';
618
        $sql .= implode(', :', $variables);
619
        $sql .= ')';
620
621
        $sqlParams = [];
622
        foreach ($variables as $field) {
623
            $sqlParams[':' . $field] = $this->$field;
624
        }
625
        
626
        $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...
627
628
        $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...
629
    }
630
    
631
    protected function connectDB()
632
    {
633
        if (!$this->dbLink) {
634
            $this->dbLink = Suricate::Database();
635
            if (static::DB_CONFIG != '') {
636
                $this->dbLink->setConfig(static::DB_CONFIG);
637
            }
638
        }
639
    }
640
    
641
    
642
    protected function accessToProtectedVariable($name)
643
    {
644
        return false;
645
    }
646
647
    public function validate()
648
    {
649
        return true;
650
    }
651
652
    public function getValidatorMessages()
653
    {
654
        return $this->validatorMessages;
655
    }
656
}
657