Passed
Push — develop ( 5cea65...ca634b )
by Mathieu
02:04
created

DBObject   F

Complexity

Total Complexity 85

Size/Duplication

Total Lines 570
Duplicated Lines 0 %

Test Coverage

Coverage 75%

Importance

Changes 0
Metric Value
eloc 220
dl 0
loc 570
ccs 180
cts 240
cp 0.75
rs 2
c 0
b 0
f 0
wmc 85

32 Methods

Rating   Name   Duplication   Size   Complexity  
A getDBConfig() 0 3 1
B loadOrInstanciate() 0 36 6
A hydrate() 0 9 3
A load() 0 14 1
A getDBVariable() 0 7 2
A setExportedVariables() 0 13 3
A propertyExists() 0 6 4
A __wakeup() 0 4 1
A isDBVariable() 0 3 1
A getTableIndex() 0 3 1
A __sleep() 0 11 2
B __isset() 0 23 7
A loadOrCreate() 0 6 1
A delete() 0 12 2
A __get() 0 13 5
A toJson() 0 3 1
A isLoaded() 0 3 1
A __construct() 0 3 1
A loadFromSql() 0 16 3
A instanciate() 0 6 1
A getTableName() 0 3 1
A loadOrFail() 0 8 2
A resetLoadedVariables() 0 7 1
A getValidatorMessages() 0 3 1
B save() 0 30 9
A connectDB() 0 6 3
A validate() 0 3 1
A create() 0 6 1
A update() 0 22 3
A __set() 0 18 5
A insert() 0 22 2
B toArray() 0 37 9

How to fix   Complexity   

Complex Class

Complex classes like DBObject often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use DBObject, and based on these observations, apply Extract Interface, too.

1
<?php
2
namespace Suricate;
3
4
use Suricate\Traits\DBObjectRelations;
5
use Suricate\Traits\DBObjectProtected;
6
/**
7
 * DBObject, Pseudo ORM Class
8
 *
9
 * Two types of variables are available :
10
 * - $dbVariables, an array of fields contained in linked SQL table
11
 * - $protectedVariables, an array of variables not stored in SQL
12
 *     that can be triggered on access
13
 *
14
 * @package Suricate
15
 * @author  Mathieu LESNIAK <[email protected]>
16
 */
17
18
class DBObject implements Interfaces\IDBObject
19
{
20
    use DBObjectRelations;
21
    use DBObjectProtected;
22
23
    /** @var string Linked SQL Table */
24
    protected $tableName = '';
25
26
    /** @var string Unique ID of the SQL table */
27
    protected $tableIndex = '';
28
    
29
    /** @var string Database config name */
30
    protected $DBConfig = '';
31
32
    /**
33
     * @const RELATION_ONE_ONE : Relation one to one
34
     */
35
    const RELATION_ONE_ONE      = 1;
36
    /**
37
     * @const RELATION_ONE_MANY : Relation one to many
38
     */
39
    const RELATION_ONE_MANY     = 2;
40
    /**
41
     * @const RELATION_MANY_MANY : Relation many to many
42
     */
43
    const RELATION_MANY_MANY    = 3;
44
45
    protected $loaded                       = false;
46
    protected $dbVariables                  = [];
47
    protected $dbValues                     = [];
48
49
    protected $readOnlyVariables            = [];
50
51
    protected $exportedVariables            = [];
52
53
    protected $dbLink                       = false;
54
55
    protected $validatorMessages            = [];
56
    
57
58 20
    public function __construct()
59
    {
60 20
        $this->setRelations();
61 20
    }
62
    /**
63
     * Magic getter
64
     *
65
     * Try to get object property according this order :
66
     * <ul>
67
     *     <li>$dbVariable</li>
68
     *     <li>$protectedVariable (triggger call to accessToProtectedVariable()
69
     *         if not already loaded)</li>
70
     * </ul>
71
     *
72
     * @param  string $name     Property name
73
     * @return Mixed            Property value
74
     */
75 9
    public function __get($name)
76
    {
77 9
        if ($this->isDBVariable($name)) {
78 8
            return $this->getDBVariable($name);
79 2
        } elseif ($this->isProtectedVariable($name)) {
80
            return $this->getProtectedVariable($name);
81 2
        } elseif ($this->isRelation($name)) {
82 1
            return $this->getRelation($name);
83 1
        } elseif (!empty($this->$name)) {
84
            return $this->$name;
85
        }
86
87 1
        throw new \InvalidArgumentException('Undefined property ' . $name);
88
    }
89
90
        /**
91
     * Magic setter
92
     *
93
     * Set a property to defined value
94
     * Assignment in this order :
95
     * - $dbVariable
96
     * - $protectedVariable
97
     *  </ul>
98
     * @param string $name  variable name
99
     * @param mixed $value variable value
100
     * 
101
     * @return void
102
     */
103 12
    public function __set($name, $value)
104
    {
105 12
        if ($this->isDBVariable($name)) {
106 11
            $this->dbValues[$name] = is_null($value) ? $value : (string) $value;
107 11
            return;
108
        }
109
110 2
        if ($this->isProtectedVariable($name)) {
111
            $this->protectedValues[$name] = $value;
112
            return;
113
        }
114
115 2
        if ($this->isRelation($name)) {
116
            $this->relationValues[$name] = $value;
117
            return;
118
        }
119
        
120 2
        $this->$name = $value;
121 2
    }
122
123 4
    public function __isset($name)
124
    {
125 4
        if ($this->isDBVariable($name)) {
126 2
            return isset($this->dbValues[$name]);
127 3
        } elseif ($this->isProtectedVariable($name)) {
128
            // Load only one time protected variable automatically
129
            if (!$this->isProtectedVariableLoaded($name)) {
130
                $protectedAccessResult = $this->accessToProtectedVariable($name);
131
132
                if ($protectedAccessResult) {
0 ignored issues
show
introduced by
The condition $protectedAccessResult is always false.
Loading history...
133
                    $this->markProtectedVariableAsLoaded($name);
134
                }
135
            }
136
            return isset($this->protectedValues[$name]);
137 3
        } elseif ($this->isRelation($name)) {
138 1
            if (!$this->isRelationLoaded($name)) {
139 1
                $this->loadRelation($name);
140 1
                $this->markRelationAsLoaded($name);
141
            }
142 1
            return isset($this->relationValues[$name]);
143
        }
144
145 2
        return false;
146
    }
147
148 9
    public function getTableName()
149
    {
150 9
        return $this->tableName;
151
    }
152
153 9
    public function getTableIndex()
154
    {
155 9
        return $this->tableIndex;
156
    }
157
158 1
    public function getDBConfig()
159
    {
160 1
        return $this->DBConfig;
161
    }
162
163
    /**
164
     * __sleep magic method, permits an inherited DBObject class to be serialized
165
     * @return Array of properties to serialize
166
     */
167
    public function __sleep()
168
    {
169
        $discardedProps = ['dbLink', 'relations'];
170
        $reflection     = new \ReflectionClass($this);
171
        $props          = $reflection->getProperties();
172
        $result         = [];
173
        foreach ($props as $currentProperty) {
174
            $result[] = $currentProperty->name;
175
        }
176
        
177
        return array_diff($result, $discardedProps);
178
    }
179
180 1
    public function __wakeup()
181
    {
182 1
        $this->dbLink = false;
183 1
        $this->setRelations();
184 1
    }
185
    
186
    /**
187
     * @param string $name
188
     */
189 8
    private function getDBVariable($name)
190
    {
191 8
        if (isset($this->dbValues[$name])) {
192 8
            return $this->dbValues[$name];
193
        }
194
195 3
        return null;
196
    }
197
198
    /**
199
     * Check if variable is from DB
200
     * @param  string  $name variable name
201
     * @return boolean
202
     */
203 14
    public function isDBVariable($name)
204
    {
205 14
        return in_array($name, $this->dbVariables);
206
    }
207
208
    /**
209
     * Define object exported variables
210
     *
211
     * @return DBObject
212
     */
213 2
    protected function setExportedVariables()
214
    {
215 2
        if (count($this->exportedVariables)) {
216 1
            return $this;
217
        }
218
219 2
        $dbMappingExport = [];
220 2
        foreach ($this->dbVariables as $field) {
221 2
            $dbMappingExport[$field] = $field;
222
        }
223 2
        $this->exportedVariables = $dbMappingExport;
224
225 2
        return $this;
226
    }
227
228
    /**
229
     * Export DBObject to array
230
     *
231
     * @return array
232
     */
233 2
    public function toArray()
234
    {
235 2
        $this->setExportedVariables();
236 2
        $result = [];
237 2
        foreach ($this->exportedVariables as $sourceName => $destinationName) {
238 2
            $omitEmpty  = false;
239 2
            $castType   = null;
240 2
            if (strpos($destinationName, ',') !== false) {
241 1
                $splitted   = explode(',', $destinationName);
242
                array_map(function ($item) use (&$castType, &$omitEmpty) {
243 1
                    if ($item === 'omitempty') {
244 1
                        $omitEmpty = true;
245 1
                        return;
246
                    }
247 1
                    if (substr($item, 0, 5) === 'type:') {
248 1
                        $castType = substr($item, 5);
249
                    }
250 1
                }, $splitted);
251
252 1
                $destinationName = $splitted[0];
253
            }
254
255 2
            if ($destinationName === '-') {
256 1
                continue;
257
            }
258
259 2
            if ($omitEmpty && empty($this->$sourceName)) {
260 1
                continue;
261
            }
262 2
            $value = $this->$sourceName;
263 2
            if ($castType !== null) {
264 1
                settype($value, $castType);
265
            }
266 2
            $result[$destinationName] = $value;
267
        }
268
269 2
        return $result;
270
    }
271
272
    /**
273
     * Export DBObject to JSON format
274
     *
275
     * @return string
276
     */
277 1
    public function toJson()
278
    {
279 1
        return json_encode($this->toArray());
280
    }
281
282 9
    private function resetLoadedVariables()
283
    {
284 9
        $this->loadedProtectedVariables = [];
285 9
        $this->loadedRelations          = [];
286 9
        $this->loaded                   = false;
287
288 9
        return $this;
289
    }
290
291
    /**
292
     * Check if requested property exists
293
     *
294
     * Check in following order:
295
     * <ul>
296
     *     <li>$dbVariables</li>
297
     *     <li>$protectedVariables</li>
298
     *     <li>$relations</li>
299
     *     <li>legacy property</li>
300
     * </ul>
301
     * @param  string $property Property name
302
     * @return boolean           true if exists
303
     */
304 4
    public function propertyExists($property)
305
    {
306 4
        return $this->isDBVariable($property)
307 2
            || $this->isProtectedVariable($property)
308 2
            || $this->isRelation($property)
309 4
            || property_exists($this, $property);
310
    }
311
312
    /**
313
     * Load ORM from Database
314
     * @param  mixed $id SQL Table Unique id
315
     * @return mixed     Loaded object or false on failure
316
     */
317 8
    public function load($id)
318
    {
319 8
        $this->connectDB();
320 8
        $this->resetLoadedVariables();
321
322 8
        $query  = "SELECT *";
323 8
        $query .= " FROM `" . $this->getTableName() ."`";
324 8
        $query .= " WHERE";
325 8
        $query .= "     `" . $this->getTableIndex() . "` =  :id";
326
        
327 8
        $params         = [];
328 8
        $params['id']   = $id;
329
330 8
        return $this->loadFromSql($query, $params);
331
    }
332
333 7
    public function isLoaded()
334
    {
335 7
        return $this->loaded;
336
    }
337
338 1
    public function loadOrFail($index)
339
    {
340 1
        $this->load($index);
341 1
        if ($this->{$this->getTableIndex()} != $index) {
342 1
            throw (new Exception\ModelNotFoundException)->setModel(get_called_class());
343
        }
344
345 1
        return $this;
346
    }
347
348
    public static function loadOrCreate($arg)
349
    {
350
        $obj = static::loadOrInstanciate($arg);
351
        $obj->save();
352
353
        return $obj;
354
    }
355
356
    /**
357
     * Load existing object by passing properties or instanciate if
358
     *
359
     * @param mixed $arg
360
     * @return DBObject
361
     */
362 1
    public static function loadOrInstanciate($arg)
363
    {
364 1
        $calledClass = get_called_class();
365 1
        $obj = new $calledClass;
366
        
367
        // got only one parameter ? consider as table index value (id)
368 1
        if (!is_array($arg)) {
369 1
            $arg = [$obj->getTableIndex() => $arg];
370
        }
371
        
372
373 1
        $sql = "SELECT *";
374 1
        $sql .= " FROM `" . $obj->getTableName() . "`";
375 1
        $sql .= " WHERE ";
376
377 1
        $sqlArray   = [];
378 1
        $params     = [];
379 1
        $offset = 0;
380 1
        foreach ($arg as $key => $val) {
381 1
            if (is_null($val)) {
382
                $sqlArray[] = '`' . $key . '` IS :arg' . $offset;
383
            } else {
384 1
                $sqlArray[] = '`' . $key . '`=:arg' . $offset;
385
            }
386 1
            $params['arg' .$offset] = $val;
387 1
            $offset++;
388
        }
389 1
        $sql .= implode(' AND ', $sqlArray);
390
391 1
        if (!$obj->loadFromSql($sql, $params)) {
392 1
            foreach ($arg as $property => $value) {
393 1
                $obj->$property = $value;
394
            }
395
        }
396
397 1
        return $obj;
398
    }
399
    
400
    /**
401
     * @param string $sql
402
     */
403 9
    public function loadFromSql(string $sql, $sqlParams = [])
404
    {
405 9
        $this->connectDB();
406 9
        $this->resetLoadedVariables();
407
        
408 9
        $results = $this->dbLink->query($sql, $sqlParams)->fetch();
409
410 9
        if ($results !== false) {
411 9
            foreach ($results as $key => $value) {
412 9
                $this->$key = $value;
413
            }
414 9
            $this->loaded = true;
415 9
            return $this;
416
        }
417
418 4
        return false;
419
    }
420
421
    /**
422
     * Construct an DBObject from an array
423
     * @param  array $data  associative array
424
     * @return DBObject       Built DBObject
425
     */
426 2
    public static function instanciate(array $data = [])
427
    {
428 2
        $calledClass    = get_called_class();
429 2
        $orm            = new $calledClass;
430
431 2
        return $orm->hydrate($data);
432
    }
433
434 3
    public function hydrate(array $data = [])
435
    {
436 3
        foreach ($data as $key => $val) {
437 3
            if ($this->propertyExists($key)) {
438 3
                $this->$key = $val;
439
            }
440
        }
441
442 3
        return $this;
443
    }
444
445 1
    public static function create(array $data = [])
446
    {
447 1
        $obj = static::instanciate($data);
448 1
        $obj->save();
449
450 1
        return $obj;
451
    }
452
    
453
    /**
454
     * Delete record from SQL Table
455
     *
456
     * Delete record link to current object, according SQL Table unique id
457
     * @return null
458
     */
459
    public function delete()
460
    {
461
        $this->connectDB();
462
463
        if ($this->getTableIndex() !== '') {
464
            $query  = "DELETE FROM `" . $this->getTableName() . "`";
465
            $query .= " WHERE `" . $this->getTableIndex() . "` = :id";
466
467
            $queryParams = [];
468
            $queryParams['id'] = $this->{$this->getTableIndex()};
469
            
470
            $this->dbLink->query($query, $queryParams);
471
        }
472
    }
473
    
474
    /**
475
     * Save current object into db
476
     *
477
     * Call INSERT or UPDATE if unique index is set
478
     * @param  boolean $forceInsert true to force insert instead of update
479
     * @return null
480
     */
481 1
    public function save($forceInsert = false)
482
    {
483 1
        if (count($this->dbValues)) {
484 1
            $this->connectDB();
485
486 1
            if ($this->isLoaded() && !$forceInsert) {
487
                $this->update();
488
                $insert = false;
489
            } else {
490 1
                $this->insert();
491 1
                $insert = true;
492
            }
493
494
            // Checking protected variables
495 1
            foreach ($this->protectedVariables as $variable) {
496
                // only if current protected_var is set
497
                if (isset($this->protectedValues[$variable]) && $this->isProtectedVariableLoaded($variable)) {
498
                    if ($this->protectedValues[$variable] instanceof Interfaces\ICollection) {
499
                        if ($insert) {
500
                            $this->protectedValues[$variable]->setParentIdForAll($this->{$this->getTableIndex()});
501
                        }
502
                        $this->protectedValues[$variable]->save();
503
                    }
504
                }
505
            }
506
507 1
            return;
508
        }
509
510
        throw new \RuntimeException("Object " . get_called_class() . " has no properties to save");
511
    }
512
513
    /**
514
     * UPDATE current object into database
515
     * @return null
516
     */
517
    private function update()
518
    {
519
        $this->connectDB();
520
521
        $sqlParams = [];
522
523
        $sql  = 'UPDATE `' . $this->getTableName() . '`';
524
        $sql .= ' SET ';
525
        
526
527
        foreach ($this->dbValues as $key => $val) {
528
            if (!in_array($key, $this->readOnlyVariables)) {
529
                $sql .= ' `' . $key . '`=:' . $key .', ';
530
                $sqlParams[$key] = $val;
531
            }
532
        }
533
        $sql  = substr($sql, 0, -2);
534
        $sql .= " WHERE `" . $this->getTableIndex() . "` = :SuricateTableIndex";
535
536
        $sqlParams[':SuricateTableIndex'] = $this->{$this->getTableIndex()};
537
538
        $this->dbLink->query($sql, $sqlParams);
539
    }
540
541
    /**
542
     * INSERT current object into database
543
     * @access  private
544
     * @return null
545
     */
546 1
    private function insert()
547
    {
548 1
        $this->connectDB();
549
        
550 1
        $variables = array_diff($this->dbVariables, $this->readOnlyVariables);
551
552 1
        $sql  = 'INSERT INTO `' . $this->getTableName() . '`';
553 1
        $sql .= '(`';
554 1
        $sql .= implode('`, `', $variables);
555 1
        $sql .= '`)';
556 1
        $sql .= ' VALUES (:';
557 1
        $sql .= implode(', :', $variables);
558 1
        $sql .= ')';
559
560 1
        $sqlParams = [];
561 1
        foreach ($variables as $field) {
562 1
            $sqlParams[':' . $field] = $this->$field;
563
        }
564
565 1
        $ret = $this->dbLink->query($sql, $sqlParams);
0 ignored issues
show
Unused Code introduced by
The assignment to $ret is dead and can be removed.
Loading history...
566 1
        $this->loaded = true;
567 1
        $this->{$this->getTableIndex()} = $this->dbLink->lastInsertId();
568 1
    }
569
    
570 9
    protected function connectDB()
571
    {
572 9
        if (!$this->dbLink) {
573
            $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

573
            /** @scrutinizer ignore-call */ 
574
            $this->dbLink = Suricate::Database();
Loading history...
574
            if ($this->getDBConfig() !== '') {
575
                $this->dbLink->setConfig($this->getDBConfig());
576
            }
577
        }
578 9
    }
579
580 1
    public function validate()
581
    {
582 1
        return true;
583
    }
584
585
    public function getValidatorMessages()
586
    {
587
        return $this->validatorMessages;
588
    }
589
}
590