Completed
Pull Request — master (#363)
by Anton
05:32
created

Row::validate()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 4
ccs 0
cts 2
cp 0
crap 2
rs 10
c 1
b 0
f 1
1
<?php
2
/**
3
 * Bluz Framework Component
4
 *
5
 * @copyright Bluz PHP Team
6
 * @link https://github.com/bluzphp/framework
7
 */
8
9
/**
10
 * @namespace
11
 */
12
namespace Bluz\Db;
13
14
use Bluz\Common\Container;
15
use Bluz\Db\Exception\InvalidPrimaryKeyException;
16
use Bluz\Db\Exception\RelationNotFoundException;
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()
28
 *        {
29
 *            $this->created = gmdate('Y-m-d H:i:s');
30
 *        }
31
 *
32
 *        public function beforeUpdate()
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
class Row implements \JsonSerializable, \ArrayAccess
48
{
49
    use Container\Container;
50
    use Container\ArrayAccess;
51
    use Container\JsonSerialize;
52
    use Container\MagicAccess;
53
54
    /**
55
     * @var Table instance of Table class
56
     */
57
    protected $table;
58
59
    /**
60
     * @var string name of Table class
61
     */
62
    protected $tableClass;
63
64
    /**
65
     * This is set to a copy of $data when the data is fetched from
66
     * a database, specified as a new tuple in the constructor, or
67
     * when dirty data is posted to the database with save().
68
     *
69
     * @var array
70
     */
71
    protected $clean = array();
72
73
    /**
74
     * @var array relations rows
75
     */
76
    protected $relations = array();
77
78
    /**
79
     * Create Row instance
80
     *
81
     * @param array $data
82
     */
83 8
    public function __construct($data = array())
84
    {
85
        // original cleaner data
86 8
        $this->clean = $this->toArray();
87
88
        // not clean data, but not modified
89 8
        if (sizeof($data)) {
90
            $this->setFromArray($data);
91
        }
92 8
        $this->afterRead();
93 8
    }
94
95
    /**
96
     * List of required for serialization properties
97
     *
98
     * @return string[]
99
     */
100
    public function __sleep()
101
    {
102
        return array('container', 'clean');
103
    }
104
105
    /**
106
     * Cast to string as class name
107
     *
108
     * @return string
109
     */
110
    public function __toString()
111
    {
112
        return static::class;
113
    }
114
115
    /**
116
     * Magic method for var_dump()
117
     *
118
     * @return array
119
     * @see var_dump()
120
     */
121
    public function __debugInfo()
122
    {
123
        return [
124
            'TABLE' => $this->getTable()->getName(),
125
            'DATA::CLEAN' => $this->clean,
126
            'DATA::RAW' => $this->container,
127
            'RELATIONS' => $this->relations
128
        ];
129
    }
130
131
    /**
132
     * Validate input data
133
     *
134
     * @param  array|object $data
135
     * @return boolean
136
     */
137
    public function validate($data)
0 ignored issues
show
Unused Code introduced by
The parameter $data is not used and could be removed.

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

Loading history...
138
    {
139
        return true;
140
    }
141
142
    /**
143
     * Assert input data
144
     *
145
     * @param  array|object $data
146
     * @return boolean
147
     */
148
    public function assert($data)
0 ignored issues
show
Unused Code introduced by
The parameter $data is not used and could be removed.

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

Loading history...
149
    {
150
        return true;
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
    public function save()
163
    {
164
        $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
        if (!sizeof(array_filter($this->getPrimaryKey()))) {
171
            $result = $this->doInsert();
172
        } elseif (sizeof(array_diff_assoc($this->getPrimaryKey(), $this->clean))) {
173
            $result = $this->doInsert();
174
        } else {
175
            $result = $this->doUpdate();
176
        }
177
        $this->afterSave();
178
        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
    protected function doInsert()
188
    {
189
        /**
190
         * Run pre-INSERT logic
191
         */
192
        $this->beforeInsert();
193
194
        $data = $this->toArray();
195
196
        /**
197
         * Execute validator logic
198
         * Can throw ValidatorException
199
         */
200
        $this->assert($data);
0 ignored issues
show
Unused Code introduced by
The call to the method Bluz\Db\Row::assert() seems un-needed as the method has no side-effects.

PHP Analyzer performs a side-effects analysis of your code. A side-effect is basically anything that might be visible after the scope of the method is left.

Let’s take a look at an example:

class User
{
    private $email;

    public function getEmail()
    {
        return $this->email;
    }

    public function setEmail($email)
    {
        $this->email = $email;
    }
}

If we look at the getEmail() method, we can see that it has no side-effect. Whether you call this method or not, no future calls to other methods are affected by this. As such code as the following is useless:

$user = new User();
$user->getEmail(); // This line could safely be removed as it has no effect.

On the hand, if we look at the setEmail(), this method _has_ side-effects. In the following case, we could not remove the method call:

$user = new User();
$user->setEmail('email@domain'); // This line has a side-effect (it changes an
                                 // instance variable).
Loading history...
201
202
        $table = $this->getTable();
203
204
        /**
205
         * Execute the INSERT (this may throw an exception)
206
         */
207
        $primaryKey = $table->insert($data);
208
209
        /**
210
         * Normalize the result to an array indexed by primary key column(s)
211
         */
212
        $tempPrimaryKey = $table->getPrimaryKey();
213
        $newPrimaryKey = array(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
        $this->setFromArray($newPrimaryKey);
222
223
        /**
224
         * Run post-INSERT logic
225
         */
226
        $this->afterInsert();
227
228
        /**
229
         * Update the "clean" to reflect that the data has been inserted.
230
         */
231
        $this->clean = $this->toArray();
232
233
        return $newPrimaryKey;
234
    }
235
236
    /**
237
     * Update row
238
     *
239
     * @return integer The number of rows updated
240
     */
241
    protected function doUpdate()
242
    {
243
        /**
244
         * Run pre-UPDATE logic
245
         */
246
        $this->beforeUpdate();
247
248
        $data = $this->toArray();
249
250
        /**
251
         * Execute validator logic
252
         * Can throw ValidatorException
253
         */
254
        $this->assert($data);
0 ignored issues
show
Unused Code introduced by
The call to the method Bluz\Db\Row::assert() seems un-needed as the method has no side-effects.

PHP Analyzer performs a side-effects analysis of your code. A side-effect is basically anything that might be visible after the scope of the method is left.

Let’s take a look at an example:

class User
{
    private $email;

    public function getEmail()
    {
        return $this->email;
    }

    public function setEmail($email)
    {
        $this->email = $email;
    }
}

If we look at the getEmail() method, we can see that it has no side-effect. Whether you call this method or not, no future calls to other methods are affected by this. As such code as the following is useless:

$user = new User();
$user->getEmail(); // This line could safely be removed as it has no effect.

On the hand, if we look at the setEmail(), this method _has_ side-effects. In the following case, we could not remove the method call:

$user = new User();
$user->setEmail('email@domain'); // This line has a side-effect (it changes an
                                 // instance variable).
Loading history...
255
256
        $primaryKey = $this->getPrimaryKey();
257
258
        /**
259
         * Compare the data to the modified fields array to discover
260
         * which columns have been changed.
261
         */
262
        $diffData = array_diff_assoc($data, $this->clean);
263
264
        $table = $this->getTable();
265
        $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
        if (sizeof($diffData) > 0) {
274
            $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
        $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
        $this->clean = $this->toArray();
291
292
        return $result;
293
    }
294
295
    /**
296
     * Delete existing row
297
     *
298
     * @return integer The number of deleted rows
299
     */
300
    public function delete()
301
    {
302
        /**
303
         * Execute pre-DELETE logic
304
         */
305
        $this->beforeDelete();
306
307
        $primaryKey = $this->getPrimaryKey();
308
309
        /**
310
         * Execute the DELETE (this may throw an exception)
311
         */
312
        $table = $this->getTable();
313
        $result = $table->delete($primaryKey);
314
315
        /**
316
         * Execute post-DELETE logic
317
         */
318
        $this->afterDelete();
319
320
        /**
321
         * Reset all fields to null to indicate that the row is not there
322
         */
323
        $this->resetArray();
324
325
        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
    protected function getPrimaryKey()
335
    {
336
        $primary = array_flip($this->getTable()->getPrimaryKey());
337
338
        $array = array_intersect_key($this->toArray(), $primary);
339
340
        return $array;
341
    }
342
343
    /**
344
     * Refreshes properties from the database
345
     *
346
     * @return void
347
     */
348
    public function refresh()
349
    {
350
        $this->setFromArray($this->clean);
351
        $this->afterRead();
352
    }
353
354
    /**
355
     * After read data from Db
356
     *
357
     * @return void
358
     */
359 8
    protected function afterRead()
360
    {
361 8
    }
362
363
    /**
364
     * Allows pre-insert and pre-update logic to be applied to row.
365
     * Subclasses may override this method
366
     *
367
     * @return void
368
     */
369
    protected function beforeSave()
370
    {
371
    }
372
373
    /**
374
     * Allows post-insert and post-update logic to be applied to row.
375
     * Subclasses may override this method
376
     *
377
     * @return void
378
     */
379
    protected function afterSave()
380
    {
381
    }
382
383
    /**
384
     * Allows pre-insert logic to be applied to row.
385
     * Subclasses may override this method
386
     *
387
     * @return void
388
     */
389
    protected function beforeInsert()
390
    {
391
    }
392
393
    /**
394
     * Allows post-insert logic to be applied to row.
395
     * Subclasses may override this method
396
     *
397
     * @return void
398
     */
399
    protected function afterInsert()
400
    {
401
    }
402
403
    /**
404
     * Allows pre-update logic to be applied to row.
405
     * Subclasses may override this method
406
     *
407
     * @return void
408
     */
409
    protected function beforeUpdate()
410
    {
411
    }
412
413
    /**
414
     * Allows post-update logic to be applied to row.
415
     * Subclasses may override this method
416
     *
417
     * @return void
418
     */
419
    protected function afterUpdate()
420
    {
421
    }
422
423
    /**
424
     * Allows pre-delete logic to be applied to row.
425
     * Subclasses may override this method
426
     *
427
     * @return void
428
     */
429
    protected function beforeDelete()
430
    {
431
    }
432
433
    /**
434
     * Allows post-delete logic to be applied to row.
435
     * Subclasses may override this method
436
     *
437
     * @return void
438
     */
439
    protected function afterDelete()
440
    {
441
    }
442
443
    /**
444
     * Setup Table instance
445
     *
446
     * @param  Table $table
447
     * @return self
448
     */
449 1
    public function setTable(Table $table)
450
    {
451 1
        $this->table = $table;
452 1
        return $this;
453
    }
454
455
    /**
456
     * Returns the table object, or null if this is disconnected row
457
     *
458
     * @throws TableNotFoundException
459
     * @return Table
460
     */
461 3
    public function getTable()
462
    {
463 3
        if ($this->table instanceof Table) {
464
            return $this->table;
465
        }
466
467 3
        if ($this->tableClass) {
468 3
            $tableClass = $this->tableClass;
469
        } else {
470
            // try to guess table class
471
            $rowClass = get_class($this);
472
            /**
473
             * @var string $tableClass is child of \Bluz\Db\Table
474
             */
475
            $tableClass = substr($rowClass, 0, strrpos($rowClass, '\\', 1) + 1) . 'Table';
476
        }
477
478
        // check class initialization
479 3
        if (!class_exists($tableClass) || !is_subclass_of($tableClass, '\\Bluz\\Db\\Table')) {
480 1
            throw new TableNotFoundException("`Table` class is not exists or not initialized");
481
        }
482
483
        /**
484
         * @var Table $tableClass
485
         */
486 2
        $table = $tableClass::getInstance();
487
488 2
        $this->setTable($table);
489
490 2
        return $table;
491
    }
492
493
    /**
494
     * Set relation
495
     *
496
     * @param  Row $row
497
     * @return Row
498
     */
499
    public function setRelation(Row $row)
500
    {
501
        $modelName = $row->getTable()->getModel();
502
        $this->relations[$modelName] = [$row];
503
        return $this;
504
    }
505
506
    /**
507
     * Get relation by model name
508
     *
509
     * @param  string $modelName
510
     * @return Row|false
511
     * @throws RelationNotFoundException
512
     */
513 1
    public function getRelation($modelName)
514
    {
515 1
        $relations = $this->getRelations($modelName);
516
        if (!empty($relations)) {
517
            return current($relations);
518
        } else {
519
            return false;
520
        }
521
    }
522
523
    /**
524
     * Get relations by model name
525
     *
526
     * @param  string $modelName
527
     * @return array
528
     * @throws RelationNotFoundException
529
     */
530 1
    public function getRelations($modelName)
531
    {
532 1
        if (!isset($this->relations[$modelName])) {
533 1
            $this->relations[$modelName] = Relations::findRelation($this, $modelName);
534
        }
535
536
        return $this->relations[$modelName];
537
    }
538
}
539