Completed
Push — master ( 3e9a7d...862f35 )
by Anton
13s
created

Row::afterRead()   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
nc 1
nop 0
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * Bluz Framework Component
4
 *
5
 * @copyright Bluz PHP Team
6
 * @link      https://github.com/bluzphp/framework
7
 */
8
9
declare(strict_types=1);
10
11
namespace Bluz\Db;
12
13
use Bluz\Common\Container;
14
use Bluz\Db\Exception\InvalidPrimaryKeyException;
15
use Bluz\Db\Exception\RelationNotFoundException;
16
use Bluz\Db\Exception\TableNotFoundException;
17
18
/**
19
 * Db Table Row
20
 *
21
 * Example of Users\Row
22
 * <code>
23
 *     namespace Application\Users;
24
 *     class Row extends \Bluz\Db\Row
25
 *     {
26
 *        public function beforeInsert()
27
 *        {
28
 *            $this->created = gmdate('Y-m-d H:i:s');
29
 *        }
30
 *
31
 *        public function beforeUpdate()
32
 *        {
33
 *            $this->updated = gmdate('Y-m-d H:i:s');
34
 *        }
35
 *     }
36
 *
37
 *     $userRow = new \Application\Users\Row();
38
 *     $userRow -> login = 'username';
39
 *     $userRow -> save();
40
 * </code>
41
 *
42
 * @package  Bluz\Db
43
 * @author   Anton Shevchuk
44
 * @link     https://github.com/bluzphp/framework/wiki/Db-Row
45
 */
46
class Row implements \JsonSerializable, \ArrayAccess
47
{
48
    use Container\Container;
49
    use Container\ArrayAccess;
50
    use Container\JsonSerialize;
51
    use Container\MagicAccess;
52
53
    /**
54
     * @var Table instance of Table class
55
     */
56
    protected $table;
57
58
    /**
59
     * @var string name of Table class
60
     */
61
    protected $tableClass;
62
63
    /**
64
     * This is set to a copy of $data when the data is fetched from
65
     * a database, specified as a new tuple in the constructor, or
66
     * when dirty data is posted to the database with save().
67
     *
68
     * @var array
69
     */
70
    protected $clean = [];
71
72
    /**
73
     * @var array relations rows
74
     */
75
    protected $relations = [];
76
77
    /**
78
     * Create Row instance
79
     *
80
     * @param array $data
81
     */
82 16
    public function __construct($data = [])
83
    {
84
        // original cleaner data
85 16
        $this->clean = $this->toArray();
86
87
        // not clean data, but not modified
88 16
        if (count($data)) {
89
            $this->setFromArray($data);
90
        }
91 16
        $this->afterRead();
92 16
    }
93
94
    /**
95
     * List of required for serialization properties
96
     *
97
     * @return string[]
98
     */
99
    public function __sleep()
100
    {
101
        return ['container', 'clean'];
102
    }
103
104
    /**
105
     * Cast to string as class name
106
     *
107
     * @return string
108
     */
109
    public function __toString()
110
    {
111
        return static::class;
112
    }
113
114
    /**
115
     * Magic method for var_dump()
116
     *
117
     * @return array
118
     * @see var_dump()
119
     */
120
    public function __debugInfo()
121
    {
122
        return [
123
            'TABLE' => $this->getTable()->getName(),
124
            'DATA::CLEAN' => $this->clean,
125
            'DATA::RAW' => $this->container,
126
            'RELATIONS' => $this->relations
127
        ];
128
    }
129
130
    /**
131
     * Validate input data
132
     *
133
     * @param  array|object $data
134
     *
135
     * @return bool
136
     */
137
    public function validate($data)
138
    {
139
        return true;
140
    }
141
142
    /**
143
     * Assert input data
144
     *
145
     * @param  array|object $data
146
     *
147
     * @return void
148
     */
149
    public function assert($data)
150
    {
151
    }
152
153
    /**
154
     * Saves the properties to the database.
155
     *
156
     * This performs an intelligent insert/update, and reloads the
157
     * properties with fresh data from the table on success.
158
     *
159
     * @return mixed The primary key value(s), as an associative array if the
160
     *               key is compound, or a scalar if the key is single-column
161
     */
162 2
    public function save()
163
    {
164 2
        $this->beforeSave();
165
        /**
166
         * If the primary key is empty, this is an INSERT of a new row.
167
         * Otherwise check primary key updated or not, if it changed - INSERT
168
         * otherwise UPDATE
169
         */
170 2
        if (!count(array_filter($this->getPrimaryKey()))) {
171 1
            $result = $this->doInsert();
172 1
        } elseif (count(array_diff_assoc($this->getPrimaryKey(), $this->clean))) {
173
            $result = $this->doInsert();
174
        } else {
175 1
            $result = $this->doUpdate();
176
        }
177 2
        $this->afterSave();
178 2
        return $result;
179
    }
180
181
    /**
182
     * Insert row to Db
183
     *
184
     * @return mixed The primary key value(s), as an associative array if the
185
     *               key is compound, or a scalar if the key is single-column
186
     */
187 1
    protected function doInsert()
188
    {
189
        /**
190
         * Run pre-INSERT logic
191
         */
192 1
        $this->beforeInsert();
193
194 1
        $data = $this->toArray();
195
196
        /**
197
         * Execute validator logic
198
         * Can throw ValidatorException
199
         */
200 1
        $this->assert($data);
201
202 1
        $table = $this->getTable();
203
204
        /**
205
         * Execute the INSERT (this may throw an exception)
206
         */
207 1
        $primaryKey = $table::insert($data);
208
209
        /**
210
         * Normalize the result to an array indexed by primary key column(s)
211
         */
212 1
        $tempPrimaryKey = $table->getPrimaryKey();
213 1
        $newPrimaryKey = [current($tempPrimaryKey) => $primaryKey];
214
215
        /**
216
         * Save the new primary key value in object. The primary key may have
217
         * been generated by a sequence or auto-increment mechanism, and this
218
         * merge should be done before the afterInsert() method is run, so the
219
         * new values are available for logging, etc.
220
         */
221 1
        $this->setFromArray($newPrimaryKey);
222
223
        /**
224
         * Run post-INSERT logic
225
         */
226 1
        $this->afterInsert();
227
228
        /**
229
         * Update the "clean" to reflect that the data has been inserted.
230
         */
231 1
        $this->clean = $this->toArray();
232
233 1
        return $newPrimaryKey;
234
    }
235
236
    /**
237
     * Update row
238
     *
239
     * @return integer The number of rows updated
240
     */
241 1
    protected function doUpdate()
242
    {
243
        /**
244
         * Run pre-UPDATE logic
245
         */
246 1
        $this->beforeUpdate();
247
248 1
        $data = $this->toArray();
249
250
        /**
251
         * Execute validator logic
252
         * Can throw ValidatorException
253
         */
254 1
        $this->assert($data);
255
256 1
        $primaryKey = $this->getPrimaryKey();
257
258
        /**
259
         * Compare the data to the modified fields array to discover
260
         * which columns have been changed.
261
         */
262 1
        $diffData = array_diff_assoc($data, $this->clean);
263
264 1
        $table = $this->getTable();
265 1
        $diffData = $table::filterColumns($diffData);
266
267
        /**
268
         * Execute the UPDATE (this may throw an exception)
269
         * Do this only if data values were changed.
270
         * Use the $diffData variable, so the UPDATE statement
271
         * includes SET terms only for data values that changed.
272
         */
273 1
        if (count($diffData) > 0) {
274 1
            $result = $table::update($diffData, $primaryKey);
275
        } else {
276
            $result = 0;
277
        }
278
279
        /**
280
         * Run post-UPDATE logic.  Do this before the _refresh()
281
         * so the _afterUpdate() function can tell the difference
282
         * between changed data and clean (pre-changed) data.
283
         */
284 1
        $this->afterUpdate();
285
286
        /**
287
         * Refresh the data just in case triggers in the RDBMS changed
288
         * any columns.  Also this resets the "clean".
289
         */
290 1
        $this->clean = $this->toArray();
291
292 1
        return $result;
293
    }
294
295
    /**
296
     * Delete existing row
297
     *
298
     * @return integer The number of deleted rows
299
     */
300 1
    public function delete()
301
    {
302
        /**
303
         * Execute pre-DELETE logic
304
         */
305 1
        $this->beforeDelete();
306
307 1
        $primaryKey = $this->getPrimaryKey();
308
309
        /**
310
         * Execute the DELETE (this may throw an exception)
311
         */
312 1
        $table = $this->getTable();
313 1
        $result = $table::delete($primaryKey);
314
315
        /**
316
         * Execute post-DELETE logic
317
         */
318 1
        $this->afterDelete();
319
320
        /**
321
         * Reset all fields to null to indicate that the row is not there
322
         */
323 1
        $this->resetArray();
324
325 1
        return $result;
326
    }
327
328
    /**
329
     * Retrieves an associative array of primary keys, if it exists
330
     *
331
     * @throws InvalidPrimaryKeyException
332
     * @return array
333
     */
334 3
    protected function getPrimaryKey()
335
    {
336 3
        $primary = array_flip($this->getTable()->getPrimaryKey());
337
338 3
        return array_intersect_key($this->toArray(), $primary);
339
    }
340
341
    /**
342
     * Refreshes properties from the database
343
     *
344
     * @return void
345
     */
346
    public function refresh()
347
    {
348
        $this->setFromArray($this->clean);
349
        $this->afterRead();
350
    }
351
352
    /**
353
     * After read data from Db
354
     *
355
     * @return void
356
     */
357 16
    protected function afterRead()
358
    {
359 16
    }
360
361
    /**
362
     * Allows pre-insert and pre-update logic to be applied to row.
363
     * Subclasses may override this method
364
     *
365
     * @return void
366
     */
367
    protected function beforeSave()
368
    {
369
    }
370
371
    /**
372
     * Allows post-insert and post-update logic to be applied to row.
373
     * Subclasses may override this method
374
     *
375
     * @return void
376
     */
377 2
    protected function afterSave()
378
    {
379 2
    }
380
381
    /**
382
     * Allows pre-insert logic to be applied to row.
383
     * Subclasses may override this method
384
     *
385
     * @return void
386
     */
387 1
    protected function beforeInsert()
388
    {
389 1
    }
390
391
    /**
392
     * Allows post-insert logic to be applied to row.
393
     * Subclasses may override this method
394
     *
395
     * @return void
396
     */
397 1
    protected function afterInsert()
398
    {
399 1
    }
400
401
    /**
402
     * Allows pre-update logic to be applied to row.
403
     * Subclasses may override this method
404
     *
405
     * @return void
406
     */
407 1
    protected function beforeUpdate()
408
    {
409 1
    }
410
411
    /**
412
     * Allows post-update logic to be applied to row.
413
     * Subclasses may override this method
414
     *
415
     * @return void
416
     */
417 1
    protected function afterUpdate()
418
    {
419 1
    }
420
421
    /**
422
     * Allows pre-delete logic to be applied to row.
423
     * Subclasses may override this method
424
     *
425
     * @return void
426
     */
427 1
    protected function beforeDelete()
428
    {
429 1
    }
430
431
    /**
432
     * Allows post-delete logic to be applied to row.
433
     * Subclasses may override this method
434
     *
435
     * @return void
436
     */
437 1
    protected function afterDelete()
438
    {
439 1
    }
440
441
    /**
442
     * Setup Table instance
443
     *
444
     * @param  Table $table
445
     *
446
     * @return self
447
     */
448 3
    public function setTable(Table $table)
449
    {
450 3
        $this->table = $table;
451 3
        return $this;
452
    }
453
454
    /**
455
     * Returns the table object, or null if this is disconnected row
456
     *
457
     * @throws TableNotFoundException
458
     * @return Table
459
     */
460 3
    public function getTable()
461
    {
462 3
        if ($this->table instanceof Table) {
463
            return $this->table;
464
        }
465
466 3
        if ($this->tableClass) {
467 3
            $tableClass = $this->tableClass;
468
        } else {
469
            // try to guess table class
470
            /**
471
             * @var string $tableClass is child of \Bluz\Db\Table
472
             */
473
            $tableClass = class_namespace(static::class) . '\\Table';
474
        }
475
476
        // check class initialization
477 3
        if (!class_exists($tableClass) || !is_subclass_of($tableClass, Table::class)) {
478 1
            throw new TableNotFoundException('`Table` class is not exists or not initialized');
479
        }
480
481
        /**
482
         * @var Table $tableClass
483
         */
484 2
        $table = $tableClass::getInstance();
485
486 2
        $this->setTable($table);
487
488 2
        return $table;
489
    }
490
491
    /**
492
     * initTable
493
     *
494
     * @return void
495
     */
496
    protected function initTable()
497
    {
498
499
    }
500
501
    /**
502
     * Set relation
503
     *
504
     * @param  Row $row
505
     *
506
     * @return Row
507
     */
508
    public function setRelation(Row $row)
509
    {
510
        $modelName = $row->getTable()->getModel();
511
        $this->relations[$modelName] = [$row];
512
        return $this;
513
    }
514
515
    /**
516
     * Get relation by model name
517
     *
518
     * @param  string $modelName
519
     *
520
     * @return Row|false
521
     * @throws RelationNotFoundException
522
     */
523 1
    public function getRelation($modelName)
524
    {
525 1
        $relations = $this->getRelations($modelName);
526
        return empty($relations) ? false : current($relations);
527
    }
528
529
    /**
530
     * Get relations by model name
531
     *
532
     * @param  string $modelName
533
     *
534
     * @return array
535
     * @throws RelationNotFoundException
536
     */
537 1
    public function getRelations($modelName)
538
    {
539 1
        if (!isset($this->relations[$modelName])) {
540 1
            $this->relations[$modelName] = Relations::findRelation($this, $modelName);
541
        }
542
543
        return $this->relations[$modelName];
544
    }
545
}
546