Completed
Push — master ( 83cea8...e47c19 )
by Anton
17s queued 12s
created

Row   A

Complexity

Total Complexity 25

Size/Duplication

Total Lines 391
Duplicated Lines 0 %

Test Coverage

Coverage 78.02%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 64
c 3
b 0
f 0
dl 0
loc 391
ccs 71
cts 91
cp 0.7802
rs 10
wmc 25

21 Methods

Rating   Name   Duplication   Size   Complexity  
A afterSave() 0 2 1
A beforeUpdate() 0 2 1
A afterInsert() 0 2 1
A beforeDelete() 0 2 1
A beforeSave() 0 2 1
A __construct() 0 10 2
A afterDelete() 0 2 1
A refresh() 0 4 1
A beforeInsert() 0 2 1
A afterUpdate() 0 2 1
A afterRead() 0 2 1
A __toString() 0 3 1
A validate() 0 3 1
A __sleep() 0 3 1
A getPrimaryKey() 0 5 1
A __debugInfo() 0 6 1
A doInsert() 0 46 1
A save() 0 17 3
A assert() 0 3 1
A doUpdate() 0 53 2
A delete() 0 26 1
1
<?php
2
3
/**
4
 * Bluz Framework Component
5
 *
6
 * @copyright Bluz PHP Team
7
 * @link      https://github.com/bluzphp/framework
8
 */
9
10
declare(strict_types=1);
11
12
namespace Bluz\Db;
13
14
use Bluz\Common\Container;
15
use Bluz\Db\Exception\DbException;
16
use Bluz\Db\Exception\InvalidPrimaryKeyException;
17
use Bluz\Db\Exception\TableNotFoundException;
18
19
/**
20
 * Db Table Row
21
 *
22
 * Example of Users\Row
23
 * <code>
24
 *     namespace Application\Users;
25
 *     class Row extends \Bluz\Db\Row
26
 *     {
27
 *        public function beforeInsert(): void
28
 *        {
29
 *            $this->created = gmdate('Y-m-d H:i:s');
30
 *        }
31
 *
32
 *        public function beforeUpdate(): void
33
 *        {
34
 *            $this->updated = gmdate('Y-m-d H:i:s');
35
 *        }
36
 *     }
37
 *
38
 *     $userRow = new \Application\Users\Row();
39
 *     $userRow -> login = 'username';
40
 *     $userRow -> save();
41
 * </code>
42
 *
43
 * @package  Bluz\Db
44
 * @author   Anton Shevchuk
45
 * @link     https://github.com/bluzphp/framework/wiki/Db-Row
46
 */
47
abstract class Row implements RowInterface, \JsonSerializable, \ArrayAccess
48
{
49 1
    use Container\Container;
50 1
    use Container\ArrayAccess;
51 1
    use Container\JsonSerialize;
52 1
    use Container\MagicAccess;
53 1
    use Traits\TableProperty;
54 1
    use Traits\RowRelations;
55
56
    /**
57
     * This is set to a copy of $data when the data is fetched from
58
     * a database, specified as a new tuple in the constructor, or
59
     * when dirty data is posted to the database with save().
60
     *
61
     * @var array
62
     */
63
    protected $clean = [];
64
65
    /**
66
     * Create Row instance
67
     *
68
     * @param array $data
69
     */
70 19
    public function __construct(array $data = [])
71
    {
72
        // original cleaner data
73 19
        $this->clean = $this->toArray();
74
75
        // not clean data, but not modified
76 19
        if (count($data)) {
77
            $this->setFromArray($data);
78
        }
79 19
        $this->afterRead();
80 19
    }
81
82
    /**
83
     * List of required for serialization properties
84
     *
85
     * @return string[]
86
     */
87
    public function __sleep()
88
    {
89
        return ['container', 'clean'];
90
    }
91
92
    /**
93
     * Cast to string as class name
94
     *
95
     * @return string
96
     */
97
    public function __toString()
98
    {
99
        return static::class;
100
    }
101
102
    /**
103
     * Magic method for var_dump()
104
     *
105
     * @return array
106
     * @see var_dump()
107
     */
108
    public function __debugInfo()
109
    {
110
        return [
111
            'DATA::CLEAN' => $this->clean,
112
            'DATA::RAW' => $this->container,
113
            'RELATIONS' => $this->relations ?? []
114
        ];
115
    }
116
117
    /**
118
     * Validate input data
119
     *
120
     * @param  array $data
121
     *
122
     * @return bool
123
     */
124
    public function validate($data): bool
0 ignored issues
show
Unused Code introduced by
The parameter $data is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

124
    public function validate(/** @scrutinizer ignore-unused */ $data): bool

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
125
    {
126
        return true;
127
    }
128
129
    /**
130
     * Assert input data
131
     *
132
     * @param  array $data
133
     *
134
     * @return void
135
     */
136
    public function assert($data): void
0 ignored issues
show
Unused Code introduced by
The parameter $data is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

136
    public function assert(/** @scrutinizer ignore-unused */ $data): void

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
137
    {
138
        return;
139
    }
140
141
    /**
142
     * Saves the properties to the database.
143
     *
144
     * This performs an intelligent insert/update, and reloads the
145
     * properties with fresh data from the table on success.
146
     *
147
     * @return mixed The primary key value(s), as an associative array if the
148
     *               key is compound, or a scalar if the key is single-column
149
     * @throws DbException
150
     * @throws InvalidPrimaryKeyException
151
     * @throws TableNotFoundException
152
     */
153 2
    public function save()
154
    {
155 2
        $this->beforeSave();
156
        /**
157
         * If the primary key is empty, this is an INSERT of a new row.
158
         * Otherwise check primary key updated or not, if it changed - INSERT
159
         * otherwise UPDATE
160
         */
161 2
        if (!count(array_filter($this->getPrimaryKey()))) {
162 1
            $result = $this->doInsert();
163 1
        } elseif (count(array_diff_assoc($this->getPrimaryKey(), $this->clean))) {
164
            $result = $this->doInsert();
165
        } else {
166 1
            $result = $this->doUpdate();
167
        }
168 2
        $this->afterSave();
169 2
        return $result;
170
    }
171
172
    /**
173
     * Insert row to Db
174
     *
175
     * @return mixed The primary key value(s), as an associative array if the
176
     *               key is compound, or a scalar if the key is single-column
177
     * @throws InvalidPrimaryKeyException
178
     * @throws TableNotFoundException
179
     */
180 1
    protected function doInsert()
181
    {
182
        /**
183
         * Run pre-INSERT logic
184
         */
185 1
        $this->beforeInsert();
186
187 1
        $data = $this->toArray();
188
        /**
189
         * Execute validator logic
190
         * Can throw ValidatorException
191
         */
192 1
        $this->assert($data);
193
194 1
        $table = $this->getTable();
195
196
        /**
197
         * Execute the INSERT (this may throw an exception)
198
         */
199 1
        $primaryKey = $table::insert($data);
200
201
        /**
202
         * Normalize the result to an array indexed by primary key column(s)
203
         */
204 1
        $tempPrimaryKey = $table->getPrimaryKey();
205 1
        $newPrimaryKey = [current($tempPrimaryKey) => $primaryKey];
206
207
        /**
208
         * Save the new primary key value in object. The primary key may have
209
         * been generated by a sequence or auto-increment mechanism, and this
210
         * merge should be done before the afterInsert() method is run, so the
211
         * new values are available for logging, etc.
212
         */
213 1
        $this->setFromArray($newPrimaryKey);
214
215
        /**
216
         * Run post-INSERT logic
217
         */
218 1
        $this->afterInsert();
219
220
        /**
221
         * Update the "clean" to reflect that the data has been inserted.
222
         */
223 1
        $this->clean = $this->toArray();
224
225 1
        return $newPrimaryKey;
226
    }
227
228
    /**
229
     * Update row
230
     *
231
     * @return integer The number of rows updated
232
     * @throws InvalidPrimaryKeyException
233
     * @throws TableNotFoundException
234
     * @throws DbException
235
     */
236 1
    protected function doUpdate(): int
237
    {
238
        /**
239
         * Run pre-UPDATE logic
240
         */
241 1
        $this->beforeUpdate();
242
243 1
        $data = $this->toArray();
244
245
        /**
246
         * Execute validator logic
247
         * Can throw ValidatorException
248
         */
249 1
        $this->assert($data);
250
251 1
        $primaryKey = $this->getPrimaryKey();
252
253
        /**
254
         * Compare the data to the modified fields array to discover
255
         * which columns have been changed.
256
         */
257 1
        $diffData = array_diff_assoc($data, $this->clean);
258
259
        /* @var Table $table */
260 1
        $table = $this->getTable();
261
262 1
        $diffData = $table::filterColumns($diffData);
263
264
        /**
265
         * Execute the UPDATE (this may throw an exception)
266
         * Do this only if data values were changed.
267
         * Use the $diffData variable, so the UPDATE statement
268
         * includes SET terms only for data values that changed.
269
         */
270 1
        $result = 0;
271 1
        if (count($diffData) > 0) {
272 1
            $result = $table::update($diffData, $primaryKey);
273
        }
274
275
        /**
276
         * Run post-UPDATE logic.  Do this before the _refresh()
277
         * so the _afterUpdate() function can tell the difference
278
         * between changed data and clean (pre-changed) data.
279
         */
280 1
        $this->afterUpdate();
281
282
        /**
283
         * Refresh the data just in case triggers in the RDBMS changed
284
         * any columns.  Also this resets the "clean".
285
         */
286 1
        $this->clean = $this->toArray();
287
288 1
        return $result;
289
    }
290
291
    /**
292
     * Delete existing row
293
     *
294
     * @return bool Removed or not
295
     * @throws InvalidPrimaryKeyException
296
     * @throws TableNotFoundException
297
     */
298 1
    public function delete(): bool
299
    {
300
        /**
301
         * Execute pre-DELETE logic
302
         */
303 1
        $this->beforeDelete();
304
305 1
        $primaryKey = $this->getPrimaryKey();
306
307
        /**
308
         * Execute the DELETE (this may throw an exception)
309
         */
310 1
        $table = $this->getTable();
311 1
        $result = $table::delete($primaryKey);
312
313
        /**
314
         * Execute post-DELETE logic
315
         */
316 1
        $this->afterDelete();
317
318
        /**
319
         * Reset all fields to null to indicate that the row is not there
320
         */
321 1
        $this->resetArray();
322
323 1
        return $result > 0;
324
    }
325
326
    /**
327
     * Retrieves an associative array of primary keys, if it exists
328
     *
329
     * @return array
330
     * @throws InvalidPrimaryKeyException
331
     * @throws TableNotFoundException
332
     */
333 3
    protected function getPrimaryKey(): array
334
    {
335 3
        $primary = array_flip($this->getTable()->getPrimaryKey());
336
337 3
        return array_intersect_key($this->toArray(), $primary);
338
    }
339
340
    /**
341
     * Refreshes properties from the database
342
     *
343
     * @return void
344
     */
345
    public function refresh(): void
346
    {
347
        $this->setFromArray($this->clean);
348
        $this->afterRead();
349
    }
350
351
    /**
352
     * After read data from Db
353
     *
354
     * @return void
355
     */
356 19
    protected function afterRead(): void
357
    {
358 19
    }
359
360
    /**
361
     * Allows pre-insert and pre-update logic to be applied to row.
362
     * Subclasses may override this method
363
     *
364
     * @return void
365
     */
366
    protected function beforeSave(): void
367
    {
368
    }
369
370
    /**
371
     * Allows post-insert and post-update logic to be applied to row.
372
     * Subclasses may override this method
373
     *
374
     * @return void
375
     */
376 2
    protected function afterSave(): void
377
    {
378 2
    }
379
380
    /**
381
     * Allows pre-insert logic to be applied to row.
382
     * Subclasses may override this method
383
     *
384
     * @return void
385
     */
386 1
    protected function beforeInsert(): void
387
    {
388 1
    }
389
390
    /**
391
     * Allows post-insert logic to be applied to row.
392
     * Subclasses may override this method
393
     *
394
     * @return void
395
     */
396 1
    protected function afterInsert(): void
397
    {
398 1
    }
399
400
    /**
401
     * Allows pre-update logic to be applied to row.
402
     * Subclasses may override this method
403
     *
404
     * @return void
405
     */
406 1
    protected function beforeUpdate(): void
407
    {
408 1
    }
409
410
    /**
411
     * Allows post-update logic to be applied to row.
412
     * Subclasses may override this method
413
     *
414
     * @return void
415
     */
416 1
    protected function afterUpdate(): void
417
    {
418 1
    }
419
420
    /**
421
     * Allows pre-delete logic to be applied to row.
422
     * Subclasses may override this method
423
     *
424
     * @return void
425
     */
426 1
    protected function beforeDelete(): void
427
    {
428 1
    }
429
430
    /**
431
     * Allows post-delete logic to be applied to row.
432
     * Subclasses may override this method
433
     *
434
     * @return void
435
     */
436 1
    protected function afterDelete(): void
437
    {
438 1
    }
439
}
440