Completed
Push — develop ( 31199f...e56745 )
by Mathieu
01:56
created

DBObject::loadOrFail()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

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

562
            /** @scrutinizer ignore-call */ 
563
            $this->dbLink = Suricate::Database();
Loading history...
563
            if ($this->getDBConfig() !== '') {
564
                $this->dbLink->setConfig($this->getDBConfig());
565
            }
566
        }
567 6
    }
568
569
    public function validate()
570
    {
571
        return true;
572
    }
573
574
    public function getValidatorMessages()
575
    {
576
        return $this->validatorMessages;
577
    }
578
}
579