DBObject::loadForField()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 14
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 9
c 1
b 0
f 0
nc 1
nop 2
dl 0
loc 14
ccs 4
cts 4
cp 1
crap 1
rs 9.9666
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
introduced by
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
    {
320
        $this->connectDB();
321
        $this->resetLoadedVariables();
322
323
        $query = "SELECT *";
324
        $query .= " FROM `" . $this->getTableName() . "`";
325
        $query .= " WHERE";
326
        $query .= "     `" . $fieldName . "` =  :fieldValue";
327 4
328
        $params = [];
329 4
        $params['fieldValue'] = $fieldValue;
330
331 4
        return $this->loadFromSql($query, $params);
332
    }
333
334 2
    /**
335
     * Load an object according to fieldName=fieldValue array
336 2
     *
337 2
     * @param array $params ['fieldName' => fieldValue, 'fieldName2' => fieldValue2]
338 2
     * @param string $operand OR / AND
339 2
     * @return static|bool
340
     */
341
    public function loadForFields(array $params, string $operand = 'AND')
342
    {
343 2
        $this->connectDB();
344
        $this->resetLoadedVariables();
345
346
        $query = "SELECT *";
347
        $query .= " FROM `" . $this->getTableName() . "`";
348
        $query .= " WHERE";
349
        $subQuery = [];
350
        $subParams = [];
351
        $offset = 0;
352
        foreach ($params as $fieldName => $fieldValue) {
353
            $subQuery[] = "`" . $fieldName . "` =  :param_" . $offset;
354
            $subParams['param_' . $offset] = $fieldValue;
355
            $offset++;
356
        }
357
        $query .= implode(' ' . $operand . ' ', $subQuery);
358
359
        return $this->loadFromSql($query, $subParams);
360 1
    }
361
362 1
    /**
363 1
     * Check if object is linked to entry in database
364
     *
365
     * @return boolean
366 1
     */
367 1
    public function isLoaded(): bool
368
    {
369
        return $this->loaded;
370 1
    }
371 1
372 1
    /**
373
     * Mark object as loaded
374 1
     * Useful when hydrated from collection, as individual object is not loaded
375 1
     * via the load() method
376 1
     * @return static
377 1
     */
378 1
    public function setLoaded()
379
    {
380
        $this->loaded = true;
381 1
382
        return $this;
383 1
    }
384 1
385
    public function loadOrFail($index)
386 1
    {
387
        $this->load($index);
388 1
        if ($this->{$this->getTableIndex()} != $index) {
389 1
            throw (new Exception\ModelNotFoundException())->setModel(
390 1
                get_called_class()
391
            );
392
        }
393
394 1
        return $this;
395
    }
396
397
    public static function loadOrCreate($arg)
398
    {
399
        $obj = static::loadOrInstanciate($arg);
400
        $obj->save();
401 12
402
        return $obj;
403 12
    }
404 12
405
    /**
406 12
     * Load existing object by passing properties or instanciate if
407
     *
408 12
     * @param mixed $arg
409 12
     * @return static
410 12
     */
411
    public static function loadOrInstanciate($arg)
412 12
    {
413 12
        $calledClass = get_called_class();
414
        $obj = new $calledClass();
415
416 5
        // got only one parameter ? consider as table index value (id)
417
        if (!is_array($arg)) {
418
            $arg = [$obj->getTableIndex() => $arg];
419
        }
420
421
        $sql = "SELECT *";
422
        $sql .= " FROM `" . $obj->getTableName() . "`";
423
        $sql .= " WHERE ";
424 6
425
        $sqlArray = [];
426 6
        $params = [];
427 6
        $offset = 0;
428
        foreach ($arg as $key => $val) {
429 6
            if (is_null($val)) {
430
                $sqlArray[] = '`' . $key . '` IS :arg' . $offset;
431
            } else {
432
                $sqlArray[] = '`' . $key . '`=:arg' . $offset;
433
            }
434
            $params['arg' . $offset] = $val;
435
            $offset++;
436
        }
437
        $sql .= implode(' AND ', $sqlArray);
438 7
439
        if (!$obj->loadFromSql($sql, $params)) {
440 7
            foreach ($arg as $property => $value) {
441 7
                $obj->$property = $value;
442 7
            }
443
        }
444
445
        return $obj;
446 7
    }
447
448
    /**
449
     * @param string $sql
450
     * @return static|bool
451
     */
452
    public function loadFromSql(string $sql, $sqlParams = [])
453
    {
454
        $this->connectDB();
455 1
        $this->resetLoadedVariables();
456
457 1
        $results = $this->dbLink->query($sql, $sqlParams)->fetch();
458 1
459
        if ($results !== false) {
460 1
            foreach ($results as $key => $value) {
461
                $this->$key = $value;
462
            }
463
            $this->loaded = true;
464
            return $this;
465
        }
466
467
        return false;
468
    }
469 1
470
    /**
471 1
     * Construct an DBObject from an array
472
     * @param  array $data  associative array
473 1
     * @return static       Built DBObject
474 1
     */
475 1
    public static function instanciate(array $data = [])
476
    {
477 1
        $calledClass = get_called_class();
478 1
        $orm = new $calledClass();
479
480 1
        return $orm->hydrate($data);
481
    }
482 1
483
    /**
484 1
     * Hydrate object (set dbValues)
485
     *
486 1
     * @param array $data
487
     * @return static
488 1
     */
489
    public function hydrate(array $data = [])
490
    {
491
        foreach ($data as $key => $val) {
492
            if ($this->propertyExists($key)) {
493
                $this->$key = $val;
494
            }
495
        }
496
497
        return $this;
498 4
    }
499
500 4
    /**
501 4
     * Create an object and save it to database
502
     *
503 4
     * @param array $data
504 1
     * @return static
505 1
     */
506
    public static function create(array $data = [])
507
    {
508 4
        $obj = static::instanciate($data);
509 4
        $obj->save();
510
511
        return $obj;
512
    }
513
514
    /**
515
     * Delete record from SQL Table
516
     *
517
     * Delete record link to current object, according SQL Table unique id
518
     * @return void
519
     */
520
    public function delete()
521 1
    {
522
        $this->connectDB();
523 1
524
        if ($this->getTableIndex() !== '') {
525 1
            $query = "DELETE FROM `" . $this->getTableName() . "`";
526
            $query .= " WHERE `" . $this->getTableIndex() . "` = :id";
527 1
528 1
            $queryParams = [];
529
            $queryParams['id'] = $this->{$this->getTableIndex()};
530 1
531 1
            $this->dbLink->query($query, $queryParams);
532 1
        }
533 1
    }
534
535
    public function setInsertIgnore(bool $flag)
536 1
    {
537 1
        $this->insertIgnore = $flag;
538
539 1
        return $this;
540
    }
541 1
542 1
    /**
543
     * Save current object into db
544
     *
545
     * Call INSERT or UPDATE if unique index is set
546
     * @param  boolean $forceInsert true to force insert instead of update
547
     * @return null
548
     */
549 4
    public function save($forceInsert = false)
550
    {
551 4
        if (count($this->dbValues)) {
552
            $this->connectDB();
553 4
554 4
            if ($this->isLoaded() && !$forceInsert) {
555
                $this->update();
556 4
                return null;
557 4
            }
558 4
559 4
            $this->insert();
560 4
            return null;
561 4
        }
562 4
563
        throw new RuntimeException(
564 4
            "Object " . get_called_class() . " has no properties to save"
565 4
        );
566 4
    }
567
568
    /**
569 4
     * UPDATE current object into database
570 4
     * @return void
571 4
     */
572 4
    private function update()
573
    {
574
        $this->connectDB();
575
576
        $sqlParams = [];
577
578
        $sql = 'UPDATE `' . $this->getTableName() . '`';
579
        $sql .= ' SET ';
580 12
581
        foreach ($this->dbValues as $key => $val) {
582 12
            if (!in_array($key, $this->readOnlyVariables)) {
583
                $sql .= ' `' . $key . '`=:' . $key . ', ';
584
                $sqlParams[$key] = $val;
585
            }
586
        }
587
        $sql = substr($sql, 0, -2);
588 12
        $sql .= " WHERE `" . $this->getTableIndex() . "` = :SuricateTableIndex";
589
590 1
        $sqlParams[':SuricateTableIndex'] = $this->{$this->getTableIndex()};
591
592 1
        $this->dbLink->query($sql, $sqlParams);
593
    }
594
595
    /**
596
     * INSERT current object into database
597
     * @access  private
598
     * @return void
599
     */
600
    private function insert()
601
    {
602
        $this->connectDB();
603
604
        $variables = array_diff($this->dbVariables, $this->readOnlyVariables);
605
        $ignoreFlag = $this->insertIgnore ? 'IGNORE ' : '';
606
607
        $sql = 'INSERT ' . $ignoreFlag . 'INTO `' . $this->getTableName() . '`';
608
        $sql .= '(`';
609
        $sql .= implode('`, `', $variables);
610
        $sql .= '`)';
611
        $sql .= ' VALUES (:';
612
        $sql .= implode(', :', $variables);
613
        $sql .= ')';
614
615
        $sqlParams = [];
616
        foreach ($variables as $field) {
617
            $sqlParams[':' . $field] = $this->$field;
618
        }
619
620
        $this->dbLink->query($sql, $sqlParams);
621
        $this->loaded = true;
622
        $this->{$this->getTableIndex()} = $this->dbLink->lastInsertId();
623
    }
624
625
    /**
626
     * Connect to DB layer
627
     *
628
     * @return void
629
     * @SuppressWarnings(PHPMD.StaticAccess)
630
     */
631
    protected function connectDB()
632
    {
633
        // FIXME: potential reuse of connection. If using >= 2 differents DB Config
634
        // the missing `true` in Database() call keeps querying the previously connected DB
635
        // Check if performance issue of passing `true` everytime
636
        if (!$this->dbLink) {
637
            $this->dbLink = Suricate::Database();
638
            if ($this->getDBConfig() !== '') {
639
                $this->dbLink->setConfig($this->getDBConfig());
640
            }
641
        }
642
    }
643
644
    public function validate()
645
    {
646
        return true;
647
    }
648
649
    public function getValidatorMessages(): array
650
    {
651
        return $this->validatorMessages;
652
    }
653
}
654