Completed
Push — develop ( d844ac...f7d5f1 )
by Mathieu
01:40
created

DBObject::getTableIndex()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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

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