Passed
Push — feature/insert-ignore ( 4b1dfb )
by Mathieu
04:10
created

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