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

Table   B

Complexity

Total Complexity 50

Size/Duplication

Total Lines 463
Duplicated Lines 0 %

Test Coverage

Coverage 82.05%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 157
dl 0
loc 463
ccs 128
cts 156
cp 0.8205
rs 8.4
c 2
b 0
f 0
wmc 50

19 Methods

Rating   Name   Duplication   Size   Complexity  
A findRowWhere() 0 4 2
A delete() 0 24 3
A select() 0 10 1
A update() 0 27 3
A create() 0 7 1
A findRow() 0 4 2
A insert() 0 30 3
A prepareStatement() 0 7 2
A __construct() 0 30 5
A init() 0 2 1
A fetch() 0 4 1
A getPrimaryKey() 0 6 2
A getColumns() 0 4 1
A filterColumns() 0 3 1
A getName() 0 3 1
A getMeta() 0 26 3
A getModel() 0 3 1
A find() 0 24 4
C findWhere() 0 47 13

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.

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
/**
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\Instance;
15
use Bluz\Db\Exception\DbException;
16
use Bluz\Db\Exception\InvalidPrimaryKeyException;
17
use Bluz\Db\Traits\TableRelations;
18
use Bluz\Proxy\Cache;
19
use Bluz\Proxy\Db as DbProxy;
20
use InvalidArgumentException;
21
22
/**
23
 * Table
24
 *
25
 * Example of Users\Table
26
 * <code>
27
 *     namespace Application\Users;
28
 *     class Table extends \Bluz\Db\Table
29
 *     {
30
 *        protected $table = 'users';
31
 *        protected $primary = ['id'];
32
 *     }
33
 *
34
 *     $userRows = \Application\Users\Table::find(1,2,3,4,5);
35
 *     foreach ($userRows as $userRow) {
36
 *        $userRow -> description = 'In first 5';
37
 *        $userRow -> save();
38
 *     }
39
 * </code>
40
 *
41
 * @package  Bluz\Db
42
 * @author   Anton Shevchuk
43
 * @link     https://github.com/bluzphp/framework/wiki/Db-Table
44
 */
45
abstract class Table implements TableInterface
46
{
47 1
    use Instance;
48 1
    use TableRelations;
49
50
    /**
51
     * @var string the table name
52
     */
53
    protected $name;
54
55
    /**
56
     * @var string the model name
57
     */
58
    protected $model;
59
60
    /**
61
     * @var array table meta
62
     */
63
    protected $meta = [];
64
65
    /**
66
     * @var string default SQL query for select
67
     */
68
    protected $select = '';
69
70
    /**
71
     * @var array the primary key column or columns (only as array).
72
     */
73
    protected $primary;
74
75
    /**
76
     * @var string the sequence name, required for PostgreSQL
77
     */
78
    protected $sequence;
79
80
    /**
81
     * @var string row class name
82
     */
83
    protected $rowClass;
84
85
    /**
86
     * Create and initialize Table instance
87
     */
88 2
    private function __construct()
89
    {
90 2
        $tableClass = static::class;
91 2
        $namespace = class_namespace($tableClass);
92
93
        // autodetect model name
94 2
        if (!$this->model) {
95 2
            $this->model = substr($namespace, strrpos($namespace, '\\') + 1);
96
        }
97
98
        // autodetect table name - camelCase to uppercase
99 2
        if (!$this->name) {
100
            $table = preg_replace('/(?<=\\w)(?=[A-Z])/', '_$1', $this->model);
101
            $this->name = strtolower($table);
102
        }
103
104
        // autodetect row class
105 2
        if (!$this->rowClass) {
106 1
            $this->rowClass = $namespace . '\\Row';
107
        }
108
109
        // setup default select query
110 2
        if (empty($this->select)) {
111 2
            $this->select = 'SELECT ' . DbProxy::quoteIdentifier($this->name) . '.* ' .
112 2
                'FROM ' . DbProxy::quoteIdentifier($this->name);
113
        }
114
115 2
        Relations::addClassMap($this->model, $tableClass);
116
117 2
        $this->init();
118 2
    }
119
120
    /**
121
     * Initialization hook.
122
     * Subclasses may override this method
123
     *
124
     * @return void
125
     */
126 2
    public function init(): void
127
    {
128 2
    }
129
130
    /**
131
     * Get primary key(s)
132
     *
133
     * @return array
134
     * @throws InvalidPrimaryKeyException if primary key was not set or has wrong format
135
     */
136 39
    public function getPrimaryKey(): array
137
    {
138 39
        if (!is_array($this->primary)) {
0 ignored issues
show
introduced by
The condition is_array($this->primary) is always true.
Loading history...
139 1
            throw new InvalidPrimaryKeyException('The primary key must be set as an array');
140
        }
141 38
        return $this->primary;
142
    }
143
144
    /**
145
     * Get table name
146
     *
147
     * @return string
148
     */
149 3
    public function getName(): string
150
    {
151 3
        return $this->name;
152
    }
153
154
    /**
155
     * Get model name
156
     *
157
     * @return string
158
     */
159 2
    public function getModel(): string
160
    {
161 2
        return $this->model;
162
    }
163
164
    /**
165
     * Return information about table columns
166
     *
167
     * @return array
168
     */
169 8
    public static function getMeta(): array
170
    {
171 8
        $self = static::getInstance();
172 8
        if (empty($self->meta)) {
173 1
            $cacheKey = "db.table.{$self->name}";
174 1
            $meta = Cache::get($cacheKey);
175 1
            if (!$meta) {
176 1
                $schema = DbProxy::getOption('connect', 'name');
177
178 1
                $meta = DbProxy::fetchUniqueGroup(
179 1
                    '
180
                    SELECT 
181
                      COLUMN_NAME AS `name`,
182
                      DATA_TYPE AS `type`,
183
                      COLUMN_DEFAULT AS `default`,
184
                      COLUMN_KEY AS `key`
185
                    FROM INFORMATION_SCHEMA.COLUMNS
186
                    WHERE TABLE_SCHEMA = ?
187
                      AND TABLE_NAME = ?',
188 1
                    [$schema, $self->getName()]
189
                );
190 1
                Cache::set($cacheKey, $meta, Cache::TTL_NO_EXPIRY, ['system', 'db']);
191
            }
192 1
            $self->meta = $meta;
193
        }
194 8
        return $self->meta;
195
    }
196
197
    /**
198
     * Return names of table columns
199
     *
200
     * @return array
201
     */
202 7
    public static function getColumns(): array
203
    {
204 7
        $self = static::getInstance();
205 7
        return array_keys($self::getMeta());
206
    }
207
208
    /**
209
     * Filter columns for insert/update queries by table columns definition
210
     *
211
     * @param array $data
212
     *
213
     * @return array
214
     */
215 6
    public static function filterColumns(array $data): array
216
    {
217 6
        return array_intersect_key($data, array_flip(static::getColumns()));
218
    }
219
220
    /**
221
     * Fetching rows by SQL query
222
     *
223
     * @param string $sql    SQL query with placeholders
224
     * @param array $params Params for query placeholders
225
     *
226
     * @return RowInterface[] of rows results in FETCH_CLASS mode
227
     */
228 10
    protected static function fetch(string $sql, array $params = []): array
229
    {
230 10
        $self = static::getInstance();
231 10
        return DbProxy::fetchObjects($sql, $params, $self->rowClass);
232
    }
233
234
    /**
235
     * {@inheritdoc}
236
     *
237
     * @throws DbException
238
     * @throws InvalidPrimaryKeyException if wrong count of values passed
239
     * @throws InvalidArgumentException
240
     */
241 10
    public static function find(...$keys): array
242
    {
243 10
        $keyNames = array_values(static::getInstance()->getPrimaryKey());
244 10
        $whereList = [];
245
246 10
        foreach ($keys as $keyValues) {
247 10
            $keyValues = (array)$keyValues;
248 10
            if (count($keyValues) !== count($keyNames)) {
249 2
                throw new InvalidPrimaryKeyException(
250
                    "Invalid columns for the primary key.\n" .
251 2
                    "Please check " . static::class . " initialization or usage.\n" .
252 2
                    "Settings described at https://github.com/bluzphp/framework/wiki/Db-Table"
253
                );
254
            }
255
256 8
            if (array_keys($keyValues)[0] === 0) {
257
                // for numerical array
258 7
                $whereList[] = array_combine($keyNames, $keyValues);
259
            } else {
260
                // for assoc array
261 1
                $whereList[] = $keyValues;
262
            }
263
        }
264 8
        return static::findWhere(...$whereList);
265
    }
266
267
    /**
268
     * {@inheritdoc}
269
     *
270
     * @throws InvalidArgumentException
271
     * @throws Exception\DbException
272
     */
273 10
    public static function findWhere(...$where): array
274
    {
275 10
        $self = static::getInstance();
276
277 10
        $whereParams = [];
278
279 10
        if (count($where) === 2 && is_string($where[0])) {
280
            $whereClause = $where[0];
281
            $whereParams = (array)$where[1];
282 10
        } elseif (count($where)) {
283 10
            $whereOrTerms = [];
284 10
            foreach ($where as $keyValueSets) {
285 10
                $whereAndTerms = [];
286 10
                foreach ($keyValueSets as $keyName => $keyValue) {
287 10
                    if (is_array($keyValue)) {
288
                        $keyValue = array_map(
289
                            ['DbProxy', 'quote'],
290
                            $keyValue
291
                        );
292
                        $keyValue = implode(',', $keyValue);
293
                        $whereAndTerms[] = $self->name . '.' . $keyName . ' IN (' . $keyValue . ')';
294 10
                    } elseif (null === $keyValue) {
295
                        $whereAndTerms[] = $self->name . '.' . $keyName . ' IS NULL';
296 10
                    } elseif (is_string($keyValue) && ('%' === $keyValue{0} || '%' === $keyValue{-1})) {
297
                        $whereAndTerms[] = $self->name . '.' . $keyName . ' LIKE ?';
298
                        $whereParams[] = $keyValue;
299
                    } else {
300 10
                        $whereAndTerms[] = $self->name . '.' . $keyName . ' = ?';
301 10
                        $whereParams[] = $keyValue;
302
                    }
303 10
                    if (!is_scalar($keyValue) && !is_null($keyValue)) {
304
                        throw new InvalidArgumentException(
305
                            "Wrong arguments of method 'findWhere'.\n" .
306
                            'Please use syntax described at https://github.com/bluzphp/framework/wiki/Db-Table'
307
                        );
308
                    }
309
                }
310 10
                $whereOrTerms[] = '(' . implode(' AND ', $whereAndTerms) . ')';
311
            }
312 10
            $whereClause = '(' . implode(' OR ', $whereOrTerms) . ')';
313
        } else {
314
            throw new DbException(
315
                "Method `Table::findWhere()` can't return all records from table"
316
            );
317
        }
318
319 10
        return self::fetch($self->select . ' WHERE ' . $whereClause, $whereParams);
320
    }
321
322
    /**
323
     * {@inheritdoc}
324
     *
325
     * @throws DbException
326
     * @throws InvalidArgumentException
327
     * @throws InvalidPrimaryKeyException
328
     */
329 8
    public static function findRow($primaryKey): ?RowInterface
330
    {
331 8
        $result = static::find($primaryKey);
332 8
        return current($result) ?: null;
333
    }
334
335
    /**
336
     * {@inheritdoc}
337
     *
338
     * @throws DbException
339
     * @throws InvalidArgumentException
340
     */
341 2
    public static function findRowWhere(array $whereList): ?RowInterface
342
    {
343 2
        $result = static::findWhere($whereList);
344 2
        return current($result) ?: null;
345
    }
346
347
    /**
348
     * Prepare array for WHERE or SET statements
349
     *
350
     * @param  array $where
351
     *
352
     * @return array
353
     */
354 6
    private static function prepareStatement(array $where): array
355
    {
356 6
        $keys = array_keys($where);
357 6
        foreach ($keys as &$key) {
358 6
            $key = DbProxy::quoteIdentifier($key) . ' = ?';
359
        }
360 6
        return $keys;
361
    }
362
363
    /**
364
     * Prepare Db\Query\Select for current table:
365
     *  - predefine "select" section as "*" from current table
366
     *  - predefine "from" section as current table name and first letter as alias
367
     *  - predefine fetch type
368
     *
369
     * <code>
370
     *     // use default select "*"
371
     *     $select = Users\Table::select();
372
     *     $arrUsers = $select->where('u.id = ?', $id)
373
     *         ->execute();
374
     *
375
     *     // setup custom select "u.id, u.login"
376
     *     $select = Users\Table::select();
377
     *     $arrUsers = $select->select('u.id, u.login')
378
     *         ->where('u.id = ?', $id)
379
     *         ->execute();
380
     * </code>
381
     *
382
     * @return Query\Select
383
     */
384 2
    public static function select(): Query\Select
385
    {
386 2
        $self = static::getInstance();
387
388 2
        $select = new Query\Select();
389 2
        $select->select(DbProxy::quoteIdentifier($self->name) . '.*')
390 2
            ->from($self->name, $self->name)
391 2
            ->setFetchType($self->rowClass);
392
393 2
        return $select;
394
    }
395
396
    /**
397
     * {@inheritdoc}
398
     */
399 2
    public static function create(array $data = []): RowInterface
400
    {
401 2
        $rowClass = static::getInstance()->rowClass;
402
        /** @var Row $row */
403 2
        $row = new $rowClass($data);
404 2
        $row->setTable(static::getInstance());
405 2
        return $row;
406
    }
407
408
    /**
409
     * {@inheritdoc}
410
     *
411
     * @throws Exception\DbException
412
     */
413 2
    public static function insert(array $data)
414
    {
415 2
        $self = static::getInstance();
416
417 2
        $data = static::filterColumns($data);
418
419 2
        if (!count($data)) {
420
            throw new DbException(
421
                "Invalid field names of table `{$self->name}`. Please check use of `insert()` method"
422
            );
423
        }
424
425 2
        $table = DbProxy::quoteIdentifier($self->name);
426
427 2
        $sql = "INSERT INTO $table SET " . implode(',', self::prepareStatement($data));
428 2
        $result = DbProxy::query($sql, array_values($data));
429 2
        if (!$result) {
430
            return null;
431
        }
432
433
        /**
434
         * If a sequence name was not specified for the name parameter, PDO::lastInsertId()
435
         * returns a string representing the row ID of the last row that was inserted into the database.
436
         *
437
         * If a sequence name was specified for the name parameter, PDO::lastInsertId()
438
         * returns a string representing the last value retrieved from the specified sequence object.
439
         *
440
         * If the PDO driver does not support this capability, PDO::lastInsertId() triggers an IM001 SQLSTATE.
441
         */
442 2
        return DbProxy::handler()->lastInsertId($self->sequence);
443
    }
444
445
    /**
446
     * {@inheritdoc}
447
     *
448
     * @throws Exception\DbException
449
     */
450 2
    public static function update(array $data, array $where): int
451
    {
452 2
        $self = static::getInstance();
453
454 2
        $data = static::filterColumns($data);
455
456 2
        if (!count($data)) {
457
            throw new DbException(
458
                "Invalid field names of table `{$self->name}`. Please check use of `update()` method"
459
            );
460
        }
461
462 2
        $where = static::filterColumns($where);
463
464 2
        if (!count($where)) {
465
            throw new DbException(
466
                "Method `Table::update()` can't update all records in the table `{$self->name}`,\n" .
467
                "please use `Db::query()` instead (of cause if you know what are you doing)"
468
            );
469
        }
470
471 2
        $table = DbProxy::quoteIdentifier($self->name);
472
473 2
        $sql = "UPDATE $table SET " . implode(',', self::prepareStatement($data))
474 2
            . " WHERE " . implode(' AND ', self::prepareStatement($where));
475
476 2
        return DbProxy::query($sql, array_merge(array_values($data), array_values($where)));
477
    }
478
479
    /**
480
     * {@inheritdoc}
481
     *
482
     * @throws DbException
483
     */
484 2
    public static function delete(array $where): int
485
    {
486 2
        $self = static::getInstance();
487
488 2
        if (!count($where)) {
489
            throw new DbException(
490
                "Method `Table::delete()` can't delete all records in the table `{$self->name}`,\n" .
491
                "please use `Db::query()` instead (of cause if you know what are you doing)"
492
            );
493
        }
494
495
496 2
        $where = static::filterColumns($where);
497
498 2
        if (!count($where)) {
499
            throw new DbException(
500
                "Invalid field names of table `{$self->name}`. Please check use of `delete()` method"
501
            );
502
        }
503
504 2
        $table = DbProxy::quoteIdentifier($self->name);
505
506 2
        $sql = "DELETE FROM $table WHERE " . implode(' AND ', self::prepareStatement($where));
507 2
        return DbProxy::query($sql, array_values($where));
508
    }
509
}
510