Completed
Push — master ( db7db7...b603ad )
by Mathieu
07:07 queued 05:18
created

DBObject::setLoaded()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

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