Issues (94)

src/Suricate/DBObject.php (1 issue)

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