DBObject::insert()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 22
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 2

Importance

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