Completed
Push — master ( 467d47...6f2bb4 )
by Anton
14s
created

Table   B

Complexity

Total Complexity 47

Size/Duplication

Total Lines 464
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 9

Test Coverage

Coverage 81.82%

Importance

Changes 0
Metric Value
dl 0
loc 464
ccs 126
cts 154
cp 0.8182
c 0
b 0
f 0
rs 8.439
wmc 47
lcom 1
cbo 9

19 Methods

Rating   Name   Duplication   Size   Complexity  
B __construct() 0 31 5
A init() 0 3 1
A getPrimaryKey() 0 7 2
A getName() 0 4 1
A getModel() 0 4 1
A getMeta() 0 23 3
A getColumns() 0 5 1
A filterColumns() 0 4 1
A fetch() 0 5 1
B find() 0 25 4
C findWhere() 0 47 10
A findRow() 0 5 2
A findRowWhere() 0 5 2
A prepareStatement() 0 8 2
A select() 0 11 1
A create() 0 8 1
B insert() 0 31 3
B update() 0 28 3
B delete() 0 25 3

How to fix   Complexity   

Complex Class

Complex classes like Table often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Table, and based on these observations, apply Extract Interface, too.

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\Instance;
14
use Bluz\Db\Exception\DbException;
15
use Bluz\Db\Exception\InvalidPrimaryKeyException;
16
use Bluz\Db\Traits\TableRelations;
17
use Bluz\Proxy\Cache;
18
use Bluz\Proxy\Db as DbProxy;
19
20
/**
21
 * Table
22
 *
23
 * Example of Users\Table
24
 * <code>
25
 *     namespace Application\Users;
26
 *     class Table extends \Bluz\Db\Table
27
 *     {
28
 *        protected $table = 'users';
29
 *        protected $primary = ['id'];
30
 *     }
31
 *
32
 *     $userRows = \Application\Users\Table::find(1,2,3,4,5);
33
 *     foreach ($userRows as $userRow) {
34
 *        $userRow -> description = 'In first 5';
35
 *        $userRow -> save();
36
 *     }
37
 * </code>
38
 *
39
 * @package  Bluz\Db
40
 * @author   Anton Shevchuk
41
 * @link     https://github.com/bluzphp/framework/wiki/Db-Table
42
 */
43
abstract class Table implements TableInterface
44
{
45
    use Instance;
46
    use TableRelations;
47
48
    /**
49
     * @var string the table name
50
     */
51
    protected $name;
52
53
    /**
54
     * @var string the model name
55
     */
56
    protected $model;
57
58
    /**
59
     * @var array table meta
60
     */
61
    protected $meta = [];
62
63
    /**
64
     * @var string default SQL query for select
65
     */
66
    protected $select = '';
67
68
    /**
69
     * @var array the primary key column or columns (only as array).
70
     */
71
    protected $primary;
72
73
    /**
74
     * @var string the sequence name, required for PostgreSQL
75
     */
76
    protected $sequence;
77
78
    /**
79
     * @var string row class name
80
     */
81
    protected $rowClass;
82
83
    /**
84
     * Create and initialize Table instance
85
     */
86 2
    private function __construct()
0 ignored issues
show
introduced by
Something seems to be off here. Are you sure you want to declare the constructor as private, and the class as abstract?
Loading history...
87
    {
88 2
        $tableClass = static::class;
89 2
        $namespace = class_namespace($tableClass);
90
91
        // autodetect model name
92 2
        if (!$this->model) {
93 2
            $this->model = substr($namespace, strrpos($namespace, '\\') + 1);
94
        }
95
96
        // autodetect table name - camelCase to uppercase
97 2
        if (!$this->name) {
98
            $table = preg_replace('/(?<=\\w)(?=[A-Z])/', '_$1', $this->model);
99
            $this->name = strtolower($table);
100
        }
101
102
        // autodetect row class
103 2
        if (!$this->rowClass) {
104 1
            $this->rowClass = $namespace . '\\Row';
105
        }
106
107
        // setup default select query
108 2
        if (empty($this->select)) {
109 2
            $this->select = 'SELECT * ' .
110 2
                'FROM ' . DbProxy::quoteIdentifier($this->name);
111
        }
112
113 2
        Relations::addClassMap($this->model, $tableClass);
114
115 2
        $this->init();
116 2
    }
117
118
    /**
119
     * Initialization hook.
120
     * Subclasses may override this method
121
     *
122
     * @return void
123
     */
124 2
    public function init() : void
125
    {
126 2
    }
127
128
    /**
129
     * Get primary key(s)
130
     *
131
     * @return array
132
     * @throws InvalidPrimaryKeyException if primary key was not set or has wrong format
133
     */
134 39
    public function getPrimaryKey() : array
135
    {
136 39
        if (!is_array($this->primary)) {
137 1
            throw new InvalidPrimaryKeyException('The primary key must be set as an array');
138
        }
139 38
        return $this->primary;
140
    }
141
142
    /**
143
     * Get table name
144
     *
145
     * @return string
146
     */
147 3
    public function getName() : string
148
    {
149 3
        return $this->name;
150
    }
151
152
    /**
153
     * Get model name
154
     *
155
     * @return string
156
     */
157 2
    public function getModel() : string
158
    {
159 2
        return $this->model;
160
    }
161
162
    /**
163
     * Return information about table columns
164
     *
165
     * @return array
166
     */
167 8
    public static function getMeta() : array
168
    {
169 8
        $self = static::getInstance();
170 8
        if (empty($self->meta)) {
171 1
            $cacheKey = "db.table.{$self->name}";
172 1
            $meta = Cache::get($cacheKey);
173 1
            if (!$meta) {
174 1
                $schema = DbProxy::getOption('connect', 'name');
175
176 1
                $meta = DbProxy::fetchUniqueGroup(
177 1
                    '
178
                    SELECT COLUMN_NAME, DATA_TYPE, COLUMN_DEFAULT, COLUMN_KEY
179
                    FROM INFORMATION_SCHEMA.COLUMNS
180
                    WHERE TABLE_SCHEMA = ?
181
                      AND TABLE_NAME = ?',
182 1
                    [$schema, $self->getName()]
183
                );
184 1
                Cache::set($cacheKey, $meta, Cache::TTL_NO_EXPIRY, ['system', 'db']);
185
            }
186 1
            $self->meta = $meta;
0 ignored issues
show
Documentation Bug introduced by
It seems like $meta of type * is incompatible with the declared type array of property $meta.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
187
        }
188 8
        return $self->meta;
189
    }
190
191
    /**
192
     * Return names of table columns
193
     *
194
     * @return array
195
     */
196 7
    public static function getColumns() : array
197
    {
198 7
        $self = static::getInstance();
199 7
        return array_keys($self::getMeta());
200
    }
201
202
    /**
203
     * Filter columns for insert/update queries by table columns definition
204
     *
205
     * @param  array $data
206
     *
207
     * @return array
208
     */
209 6
    public static function filterColumns($data) : array
210
    {
211 6
        return array_intersect_key($data, array_flip(static::getColumns()));
212
    }
213
214
    /**
215
     * Fetching rows by SQL query
216
     *
217
     * @param  string $sql    SQL query with placeholders
218
     * @param  array  $params Params for query placeholders
219
     *
220
     * @return RowInterface[] of rows results in FETCH_CLASS mode
221
     */
222 10
    protected static function fetch($sql, $params = []) : array
223
    {
224 10
        $self = static::getInstance();
225 10
        return DbProxy::fetchObjects($sql, $params, $self->rowClass);
226
    }
227
228
    /**
229
     * {@inheritdoc}
230
     *
231
     * @throws DbException
232
     * @throws InvalidPrimaryKeyException if wrong count of values passed
233
     * @throws \InvalidArgumentException
234
     */
235 10
    public static function find(...$keys) : array
236
    {
237 10
        $keyNames = array_values(static::getInstance()->getPrimaryKey());
238 10
        $whereList = [];
239
240 10
        foreach ($keys as $keyValues) {
241 10
            $keyValues = (array)$keyValues;
242 10
            if (count($keyValues) !== count($keyNames)) {
243 2
                throw new InvalidPrimaryKeyException(
244
                    "Invalid columns for the primary key.\n" .
245 2
                    "Please check " . static::class . " initialization or usage.\n" .
246 2
                    "Settings described at https://github.com/bluzphp/framework/wiki/Db-Table"
247
                );
248
            }
249
250 8
            if (array_keys($keyValues)[0] === 0) {
251
                // for numerical array
252 7
                $whereList[] = array_combine($keyNames, $keyValues);
253
            } else {
254
                // for assoc array
255 8
                $whereList[] = $keyValues;
256
            }
257
        }
258 8
        return static::findWhere(...$whereList);
259
    }
260
261
    /**
262
     * {@inheritdoc}
263
     *
264
     * @throws \InvalidArgumentException
265
     * @throws Exception\DbException
266
     */
267 10
    public static function findWhere(...$where) : array
268
    {
269 10
        $self = static::getInstance();
270
271 10
        $whereParams = [];
272
273 10
        if (count($where) === 2 && is_string($where[0])) {
274
            $whereClause = $where[0];
275
            $whereParams = (array)$where[1];
276 10
        } elseif (count($where)) {
277 10
            $whereOrTerms = [];
278 10
            foreach ($where as $keyValueSets) {
279 10
                $whereAndTerms = [];
280 10
                foreach ($keyValueSets as $keyName => $keyValue) {
281 10
                    if (is_array($keyValue)) {
282
                        $keyValue = array_map(
283
                            function ($value) use ($self) {
284
                                return DbProxy::quote($value);
285
                            },
286
                            $keyValue
287
                        );
288
                        $keyValue = implode(',', $keyValue);
289
                        $whereAndTerms[] = $self->name . '.' . $keyName . ' IN (' . $keyValue . ')';
290 10
                    } elseif (is_null($keyValue)) {
291
                        $whereAndTerms[] = $self->name . '.' . $keyName . ' IS NULL';
292
                    } else {
293 10
                        $whereAndTerms[] = $self->name . '.' . $keyName . ' = ?';
294 10
                        $whereParams[] = $keyValue;
295
                    }
296 10
                    if (!is_scalar($keyValue) && !is_null($keyValue)) {
297
                        throw new \InvalidArgumentException(
298
                            "Wrong arguments of method 'findWhere'.\n" .
299 10
                            "Please use syntax described at https://github.com/bluzphp/framework/wiki/Db-Table"
300
                        );
301
                    }
302
                }
303 10
                $whereOrTerms[] = '(' . implode(' AND ', $whereAndTerms) . ')';
304
            }
305 10
            $whereClause = '(' . implode(' OR ', $whereOrTerms) . ')';
306
        } else {
307
            throw new DbException(
308
                "Method `Table::findWhere()` can't return all records from table"
309
            );
310
        }
311
312 10
        return self::fetch($self->select . ' WHERE ' . $whereClause, $whereParams);
313
    }
314
315
    /**
316
     * {@inheritdoc}
317
     *
318
     * @throws DbException
319
     * @throws \InvalidArgumentException
320
     * @throws InvalidPrimaryKeyException
321
     */
322 8
    public static function findRow($primaryKey) : ?RowInterface
323
    {
324 8
        $result = static::find($primaryKey);
325 8
        return current($result) ?: null;
326
    }
327
328
    /**
329
     * {@inheritdoc}
330
     *
331
     * @throws DbException
332
     * @throws \InvalidArgumentException
333
     */
334 2
    public static function findRowWhere(array $whereList) : ?RowInterface
335
    {
336 2
        $result = static::findWhere($whereList);
337 2
        return current($result) ?: null;
338
    }
339
340
    /**
341
     * Prepare array for WHERE or SET statements
342
     *
343
     * @param  array $where
344
     *
345
     * @return array
346
     * @throws \Bluz\Common\Exception\ConfigurationException
347
     */
348 6
    private static function prepareStatement(array $where) : array
349
    {
350 6
        $keys = array_keys($where);
351 6
        foreach ($keys as &$key) {
352 6
            $key = DbProxy::quoteIdentifier($key) . ' = ?';
353
        }
354 6
        return $keys;
355
    }
356
357
    /**
358
     * Prepare Db\Query\Select for current table:
359
     *  - predefine "select" section as "*" from current table
360
     *  - predefine "from" section as current table name and first letter as alias
361
     *  - predefine fetch type
362
     *
363
     * <code>
364
     *     // use default select "*"
365
     *     $select = Users\Table::select();
366
     *     $arrUsers = $select->where('u.id = ?', $id)
367
     *         ->execute();
368
     *
369
     *     // setup custom select "u.id, u.login"
370
     *     $select = Users\Table::select();
371
     *     $arrUsers = $select->select('u.id, u.login')
372
     *         ->where('u.id = ?', $id)
373
     *         ->execute();
374
     * </code>
375
     *
376
     * @return Query\Select
377
     */
378 2
    public static function select() : Query\Select
379
    {
380 2
        $self = static::getInstance();
381
382 2
        $select = new Query\Select();
383 2
        $select->select($self->name . '.*')
384 2
            ->from($self->name, $self->name)
385 2
            ->setFetchType($self->rowClass);
386
387 2
        return $select;
388
    }
389
390
    /**
391
     * {@inheritdoc}
392
     */
393 2
    public static function create(array $data = []) : RowInterface
394
    {
395 2
        $rowClass = static::getInstance()->rowClass;
396
        /** @var Row $row */
397 2
        $row = new $rowClass($data);
398 2
        $row->setTable(static::getInstance());
399 2
        return $row;
400
    }
401
402
    /**
403
     * {@inheritdoc}
404
     *
405
     * @throws \Bluz\Common\Exception\ConfigurationException
406
     * @throws Exception\DbException
407
     */
408 2
    public static function insert(array $data)
409
    {
410 2
        $self = static::getInstance();
411
412 2
        $data = static::filterColumns($data);
413
414 2
        if (!count($data)) {
415
            throw new DbException(
416
                "Invalid field names of table `{$self->name}`. Please check use of `insert()` method"
417
            );
418
        }
419
420 2
        $table = DbProxy::quoteIdentifier($self->name);
421
422 2
        $sql = "INSERT INTO $table SET " . implode(',', self::prepareStatement($data));
423 2
        $result = DbProxy::query($sql, array_values($data));
424 2
        if (!$result) {
425
            return null;
426
        }
427
428
        /**
429
         * If a sequence name was not specified for the name parameter, PDO::lastInsertId()
430
         * returns a string representing the row ID of the last row that was inserted into the database.
431
         *
432
         * If a sequence name was specified for the name parameter, PDO::lastInsertId()
433
         * returns a string representing the last value retrieved from the specified sequence object.
434
         *
435
         * If the PDO driver does not support this capability, PDO::lastInsertId() triggers an IM001 SQLSTATE.
436
         */
437 2
        return DbProxy::handler()->lastInsertId($self->sequence);
438
    }
439
440
    /**
441
     * {@inheritdoc}
442
     *
443
     * @throws \Bluz\Common\Exception\ConfigurationException
444
     * @throws Exception\DbException
445
     */
446 2
    public static function update(array $data, array $where) : int
447
    {
448 2
        $self = static::getInstance();
449
450 2
        $data = static::filterColumns($data);
451
452 2
        if (!count($data)) {
453
            throw new DbException(
454
                "Invalid field names of table `{$self->name}`. Please check use of `update()` method"
455
            );
456
        }
457
458 2
        $where = static::filterColumns($where);
459
460 2
        if (!count($where)) {
461
            throw new DbException(
462
                "Method `Table::update()` can't update all records in the table `{$self->name}`,\n" .
463
                "please use `Db::query()` instead (of cause if you know what are you doing)"
464
            );
465
        }
466
467 2
        $table = DbProxy::quoteIdentifier($self->name);
468
469 2
        $sql = "UPDATE $table SET " . implode(',', self::prepareStatement($data))
470 2
            . " WHERE " . implode(' AND ', self::prepareStatement($where));
471
472 2
        return DbProxy::query($sql, array_merge(array_values($data), array_values($where)));
473
    }
474
475
    /**
476
     * {@inheritdoc}
477
     *
478
     * @throws \Bluz\Common\Exception\ConfigurationException
479
     * @throws \Bluz\Db\Exception\DbException
480
     */
481 2
    public static function delete(array $where) : int
482
    {
483 2
        $self = static::getInstance();
484
485 2
        if (!count($where)) {
486
            throw new DbException(
487
                "Method `Table::delete()` can't delete all records in the table `{$self->name}`,\n" .
488
                "please use `Db::query()` instead (of cause if you know what are you doing)"
489
            );
490
        }
491
492
493 2
        $where = static::filterColumns($where);
494
495 2
        if (!count($where)) {
496
            throw new DbException(
497
                "Invalid field names of table `{$self->name}`. Please check use of `delete()` method"
498
            );
499
        }
500
501 2
        $table = DbProxy::quoteIdentifier($self->name);
502
503 2
        $sql = "DELETE FROM $table WHERE " . implode(' AND ', self::prepareStatement($where));
504 2
        return DbProxy::query($sql, array_values($where));
505
    }
506
}
507