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
     */    // FIXME: should be replaced by __serialize in PHP 8.5
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
    // FIXME: should be replaced by __unserialize in PHP 8.5
233
    public function __wakeup()
234 1
    {
235 1
        $this->dbLink = false;
236 1
        $this->setRelations();
237
    }
238
239
    /**
240
     * @param string $name
241 14
     */
242
    private function getDBVariable($name)
243 14
    {
244 14
        if (isset($this->dbValues[$name])) {
245
            return $this->dbValues[$name];
246
        }
247 5
248
        return null;
249
    }
250
251
    /**
252
     * Check if variable is from DB
253
     * @param  string  $name variable name
254
     * @return boolean
255 21
     */
256
    public function isDBVariable(string $name)
257 21
    {
258
        return in_array($name, $this->dbVariables);
259
    }
260 12
261
    private function resetLoadedVariables()
262 12
    {
263 12
        $this->loadedProtectedVariables = [];
264 12
        $this->loadedRelations = [];
265
        $this->loaded = false;
266 12
267
        return $this;
268
    }
269
270
    /**
271
     * Check if requested property exists
272
     *
273
     * Check in following order:
274
     * <ul>
275
     *     <li>$dbVariables</li>
276
     *     <li>$protectedVariables</li>
277
     *     <li>$relations</li>
278
     *     <li>legacy property</li>
279
     * </ul>
280
     * @param  string $property Property name
281
     * @return boolean           true if exists
282 8
     */
283
    public function propertyExists($property)
284 8
    {
285 2
        return $this->isDBVariable($property) ||
286 2
            $this->isProtectedVariable($property) ||
287 8
            $this->isRelation($property) ||
288
            property_exists($this, $property);
289
    }
290
291
    /**
292
     * Load ORM from Database
293
     * @param  mixed $id SQL Table Unique id
294
     * @return mixed     Loaded object or false on failure
295 11
     */
296
    public function load($id)
297 11
    {
298 11
        $this->connectDB();
299
        $this->resetLoadedVariables();
300 11
301 11
        $query = "SELECT *";
302 11
        $query .= " FROM `" . $this->getTableName() . "`";
303 11
        $query .= " WHERE";
304
        $query .= "     `" . $this->getTableIndex() . "` =  :id";
305 11
306 11
        $params = [];
307
        $params['id'] = $id;
308 11
309
        return $this->loadFromSql($query, $params);
310
    }
311
312
    /**
313
     * Load an object according to fieldName=fieldValue
314
     *
315
     * @param string $fieldName
316 11
     * @param mixed $fieldValue
317
     * @return static|bool
318 11
     */
319
    public function loadForField(string $fieldName, $fieldValue)
320
    {
321
        $this->connectDB();
322
        $this->resetLoadedVariables();
323
324
        $query = "SELECT *";
325
        $query .= " FROM `" . $this->getTableName() . "`";
326
        $query .= " WHERE";
327 4
        $query .= "     `" . $fieldName . "` =  :fieldValue";
328
329 4
        $params = [];
330
        $params['fieldValue'] = $fieldValue;
331 4
332
        return $this->loadFromSql($query, $params);
333
    }
334 2
335
    /**
336 2
     * Load an object according to fieldName=fieldValue array
337 2
     *
338 2
     * @param array $params ['fieldName' => fieldValue, 'fieldName2' => fieldValue2]
339 2
     * @param string $operand OR / AND
340
     * @return static|bool
341
     */
342
    public function loadForFields(array $params, string $operand = 'AND')
343 2
    {
344
        $this->connectDB();
345
        $this->resetLoadedVariables();
346
347
        $query = "SELECT *";
348
        $query .= " FROM `" . $this->getTableName() . "`";
349
        $query .= " WHERE";
350
        $subQuery = [];
351
        $subParams = [];
352
        $offset = 0;
353
        foreach ($params as $fieldName => $fieldValue) {
354
            $subQuery[] = "`" . $fieldName . "` =  :param_" . $offset;
355
            $subParams['param_' . $offset] = $fieldValue;
356
            $offset++;
357
        }
358
        $query .= implode(' ' . $operand . ' ', $subQuery);
359
360 1
        return $this->loadFromSql($query, $subParams);
361
    }
362 1
363 1
    /**
364
     * Check if object is linked to entry in database
365
     *
366 1
     * @return boolean
367 1
     */
368
    public function isLoaded(): bool
369
    {
370 1
        return $this->loaded;
371 1
    }
372 1
373
    /**
374 1
     * Mark object as loaded
375 1
     * Useful when hydrated from collection, as individual object is not loaded
376 1
     * via the load() method
377 1
     * @return static
378 1
     */
379
    public function setLoaded()
380
    {
381 1
        $this->loaded = true;
382
383 1
        return $this;
384 1
    }
385
386 1
    public function loadOrFail($index)
387
    {
388 1
        $this->load($index);
389 1
        if ($this->{$this->getTableIndex()} != $index) {
390 1
            throw (new Exception\ModelNotFoundException())->setModel(
391
                get_called_class()
392
            );
393
        }
394 1
395
        return $this;
396
    }
397
398
    public static function loadOrCreate($arg)
399
    {
400
        $obj = static::loadOrInstanciate($arg);
401 12
        $obj->save();
402
403 12
        return $obj;
404 12
    }
405
406 12
    /**
407
     * Load existing object by passing properties or instanciate if
408 12
     *
409 12
     * @param mixed $arg
410 12
     * @return static
411
     */
412 12
    public static function loadOrInstanciate($arg)
413 12
    {
414
        $calledClass = get_called_class();
415
        $obj = new $calledClass();
416 5
417
        // got only one parameter ? consider as table index value (id)
418
        if (!is_array($arg)) {
419
            $arg = [$obj->getTableIndex() => $arg];
420
        }
421
422
        $sql = "SELECT *";
423
        $sql .= " FROM `" . $obj->getTableName() . "`";
424 6
        $sql .= " WHERE ";
425
426 6
        $sqlArray = [];
427 6
        $params = [];
428
        $offset = 0;
429 6
        foreach ($arg as $key => $val) {
430
            if (is_null($val)) {
431
                $sqlArray[] = '`' . $key . '` IS :arg' . $offset;
432
            } else {
433
                $sqlArray[] = '`' . $key . '`=:arg' . $offset;
434
            }
435
            $params['arg' . $offset] = $val;
436
            $offset++;
437
        }
438 7
        $sql .= implode(' AND ', $sqlArray);
439
440 7
        if (!$obj->loadFromSql($sql, $params)) {
441 7
            foreach ($arg as $property => $value) {
442 7
                $obj->$property = $value;
443
            }
444
        }
445
446 7
        return $obj;
447
    }
448
449
    /**
450
     * @param string $sql
451
     * @return static|bool
452
     */
453
    public function loadFromSql(string $sql, $sqlParams = [])
454
    {
455 1
        $this->connectDB();
456
        $this->resetLoadedVariables();
457 1
458 1
        $results = $this->dbLink->query($sql, $sqlParams)->fetch();
459
460 1
        if ($results !== false) {
461
            foreach ($results as $key => $value) {
462
                $this->$key = $value;
463
            }
464
            $this->loaded = true;
465
            return $this;
466
        }
467
468
        return false;
469 1
    }
470
471 1
    /**
472
     * Construct an DBObject from an array
473 1
     * @param  array $data  associative array
474 1
     * @return static       Built DBObject
475 1
     */
476
    public static function instanciate(array $data = [])
477 1
    {
478 1
        $calledClass = get_called_class();
479
        $orm = new $calledClass();
480 1
481
        return $orm->hydrate($data);
482 1
    }
483
484 1
    /**
485
     * Hydrate object (set dbValues)
486 1
     *
487
     * @param array $data
488 1
     * @return static
489
     */
490
    public function hydrate(array $data = [])
491
    {
492
        foreach ($data as $key => $val) {
493
            if ($this->propertyExists($key)) {
494
                $this->$key = $val;
495
            }
496
        }
497
498 4
        return $this;
499
    }
500 4
501 4
    /**
502
     * Create an object and save it to database
503 4
     *
504 1
     * @param array $data
505 1
     * @return static
506
     */
507
    public static function create(array $data = [])
508 4
    {
509 4
        $obj = static::instanciate($data);
510
        $obj->save();
511
512
        return $obj;
513
    }
514
515
    /**
516
     * Delete record from SQL Table
517
     *
518
     * Delete record link to current object, according SQL Table unique id
519
     * @return void
520
     */
521 1
    public function delete()
522
    {
523 1
        $this->connectDB();
524
525 1
        if ($this->getTableIndex() !== '') {
526
            $query = "DELETE FROM `" . $this->getTableName() . "`";
527 1
            $query .= " WHERE `" . $this->getTableIndex() . "` = :id";
528 1
529
            $queryParams = [];
530 1
            $queryParams['id'] = $this->{$this->getTableIndex()};
531 1
532 1
            $this->dbLink->query($query, $queryParams);
533 1
        }
534
    }
535
536 1
    public function setInsertIgnore(bool $flag)
537 1
    {
538
        $this->insertIgnore = $flag;
539 1
540
        return $this;
541 1
    }
542 1
543
    /**
544
     * Save current object into db
545
     *
546
     * Call INSERT or UPDATE if unique index is set
547
     * @param  boolean $forceInsert true to force insert instead of update
548
     * @return null
549 4
     */
550
    public function save($forceInsert = false)
551 4
    {
552
        if (count($this->dbValues)) {
553 4
            $this->connectDB();
554 4
555
            if ($this->isLoaded() && !$forceInsert) {
556 4
                $this->update();
557 4
                return null;
558 4
            }
559 4
560 4
            $this->insert();
561 4
            return null;
562 4
        }
563
564 4
        throw new RuntimeException(
565 4
            "Object " . get_called_class() . " has no properties to save"
566 4
        );
567
    }
568
569 4
    /**
570 4
     * UPDATE current object into database
571 4
     * @return void
572 4
     */
573
    private function update()
574
    {
575
        $this->connectDB();
576
577
        $sqlParams = [];
578
579
        $sql = 'UPDATE `' . $this->getTableName() . '`';
580 12
        $sql .= ' SET ';
581
582 12
        foreach ($this->dbValues as $key => $val) {
583
            if (!in_array($key, $this->readOnlyVariables)) {
584
                $sql .= ' `' . $key . '`=:' . $key . ', ';
585
                $sqlParams[$key] = $val;
586
            }
587
        }
588 12
        $sql = substr($sql, 0, -2);
589
        $sql .= " WHERE `" . $this->getTableIndex() . "` = :SuricateTableIndex";
590 1
591
        $sqlParams[':SuricateTableIndex'] = $this->{$this->getTableIndex()};
592 1
593
        $this->dbLink->query($sql, $sqlParams);
594
    }
595
596
    /**
597
     * INSERT current object into database
598
     * @access  private
599
     * @return void
600
     */
601
    private function insert()
602
    {
603
        $this->connectDB();
604
605
        $variables = array_diff($this->dbVariables, $this->readOnlyVariables);
606
        $ignoreFlag = $this->insertIgnore ? 'IGNORE ' : '';
607
608
        $sql = 'INSERT ' . $ignoreFlag . 'INTO `' . $this->getTableName() . '`';
609
        $sql .= '(`';
610
        $sql .= implode('`, `', $variables);
611
        $sql .= '`)';
612
        $sql .= ' VALUES (:';
613
        $sql .= implode(', :', $variables);
614
        $sql .= ')';
615
616
        $sqlParams = [];
617
        foreach ($variables as $field) {
618
            $sqlParams[':' . $field] = $this->$field;
619
        }
620
621
        $this->dbLink->query($sql, $sqlParams);
622
        $this->loaded = true;
623
        $this->{$this->getTableIndex()} = $this->dbLink->lastInsertId();
624
    }
625
626
    /**
627
     * Connect to DB layer
628
     *
629
     * @return void
630
     * @SuppressWarnings("PHPMD.StaticAccess")
631
     */
632
    protected function connectDB()
633
    {
634
        // FIXME: potential reuse of connection. If using >= 2 differents DB Config
635
        // the missing `true` in Database() call keeps querying the previously connected DB
636
        // Check if performance issue of passing `true` everytime
637
        if (!$this->dbLink) {
638
            $this->dbLink = Suricate::Database();
639
            if ($this->getDBConfig() !== '') {
640
                $this->dbLink->setConfig($this->getDBConfig());
641
            }
642
        }
643
    }
644
645
    public function validate()
646
    {
647
        return true;
648
    }
649
650
    public function getValidatorMessages(): array
651
    {
652
        return $this->validatorMessages;
653
    }
654
}
655