Completed
Push — master ( 264bad...d292ba )
by Anton
12s
created

Table   C

Complexity

Total Complexity 55

Size/Duplication

Total Lines 609
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 7

Test Coverage

Coverage 73.74%

Importance

Changes 0
Metric Value
dl 0
loc 609
ccs 132
cts 179
cp 0.7374
rs 6.6928
c 0
b 0
f 0
wmc 55
lcom 2
cbo 7

25 Methods

Rating   Name   Duplication   Size   Complexity  
B __construct() 0 31 5
A init() 0 3 1
A getInstance() 0 9 2
A setSelectQuery() 0 5 1
A getSelectQuery() 0 4 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
A fetchAll() 0 5 1
B find() 0 32 5
A findRow() 0 8 2
C findWhere() 0 48 10
A findRowWhere() 0 5 1
A prepareStatement() 0 8 2
A select() 0 11 1
A create() 0 8 1
B insert() 0 31 3
B update() 0 29 4
B delete() 0 25 3
A linkTo() 0 4 1
A linkToMany() 0 4 1

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

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
473 2
            ->from($self->name, $self->name)
474 2
            ->setFetchType($self->rowClass);
475
476 2
        return $select;
477
    }
478
479
    /**
480
     * Create Row instance
481
     *
482
     * @param  array $data
483
     *
484
     * @return Row
485
     */
486 2
    public static function create(array $data = [])
487
    {
488 2
        $rowClass = static::getInstance()->rowClass;
489
        /** @var Row $row */
490 2
        $row = new $rowClass($data);
491 2
        $row->setTable(static::getInstance());
492 2
        return $row;
493
    }
494
495
    /**
496
     * Insert new record to table and return last insert Id
497
     *
498
     * <code>
499
     *     Table::insert(['login' => 'Man', 'email' => '[email protected]'])
500
     * </code>
501
     *
502
     * @param  array $data Column-value pairs
503
     *
504
     * @return string|null Primary key or null
505
     * @throws \Bluz\Common\Exception\ConfigurationException
506
     * @throws Exception\DbException
507
     */
508 1
    public static function insert(array $data)
509
    {
510 1
        $self = static::getInstance();
511
512 1
        $data = static::filterColumns($data);
513
514 1
        if (!count($data)) {
515
            throw new DbException(
516
                "Invalid field names of table `{$self->name}`. Please check use of `insert()` method"
517
            );
518
        }
519
520 1
        $table = DbProxy::quoteIdentifier($self->name);
521
522 1
        $sql = "INSERT INTO $table SET " . implode(',', self::prepareStatement($data));
523 1
        $result = DbProxy::query($sql, array_values($data));
524 1
        if (!$result) {
525
            return null;
526
        }
527
528
        /**
529
         * If a sequence name was not specified for the name parameter, PDO::lastInsertId()
530
         * returns a string representing the row ID of the last row that was inserted into the database.
531
         *
532
         * If a sequence name was specified for the name parameter, PDO::lastInsertId()
533
         * returns a string representing the last value retrieved from the specified sequence object.
534
         *
535
         * If the PDO driver does not support this capability, PDO::lastInsertId() triggers an IM001 SQLSTATE.
536
         */
537 1
        return DbProxy::handler()->lastInsertId($self->sequence);
538
    }
539
540
    /**
541
     * Updates existing rows
542
     *
543
     * <code>
544
     *     Table::insert(['login' => 'Man', 'email' => '[email protected]'], ['id' => 42])
545
     * </code>
546
     *
547
     * @param  array $data  Column-value pairs.
548
     * @param  array $where An array of SQL WHERE clause(s)
549
     *
550
     * @return integer The number of rows updated
551
     * @throws \Bluz\Common\Exception\ConfigurationException
552
     * @throws Exception\DbException
553
     */
554 1
    public static function update(array $data, array $where)
555
    {
556 1
        if (!count($where)) {
557
            throw new DbException(
558
                "Method `Table::update()` can't update all records in table,\n" .
559
                "please use `Db::query()` instead (of cause if you know what are you doing)"
560
            );
561
        }
562
563 1
        $self = static::getInstance();
564
565 1
        $data = static::filterColumns($data);
566
567 1
        $where = static::filterColumns($where);
568
569 1
        if (!count($data) || !count($where)) {
570
            throw new DbException(
571
                "Invalid field names of table `{$self->name}`. Please check use of `update()` method"
572
            );
573
        }
574
575 1
        $table = DbProxy::quoteIdentifier($self->name);
576
577 1
        $sql = "UPDATE $table"
578 1
            . " SET " . implode(',', self::prepareStatement($data))
579 1
            . " WHERE " . implode(' AND ', self::prepareStatement($where));
580
581 1
        return DbProxy::query($sql, array_merge(array_values($data), array_values($where)));
582
    }
583
584
    /**
585
     * Deletes existing rows
586
     *
587
     * <code>
588
     *     Table::delete(['login' => 'Man'])
589
     * </code>
590
     *
591
     * @param  array $where An array of SQL WHERE clause(s)
592
     *
593
     * @return integer The number of rows deleted
594
     * @throws Exception\DbException
595
     */
596 1
    public static function delete(array $where)
597
    {
598 1
        if (!count($where)) {
599
            throw new DbException(
600
                "Method `Table::delete()` can't delete all records in table,\n" .
601
                "please use `Db::query()` instead (of cause if you know what are you doing)"
602
            );
603
        }
604
605 1
        $self = static::getInstance();
606
607 1
        $where = static::filterColumns($where);
608
609 1
        if (!count($where)) {
610
            throw new DbException(
611
                "Invalid field names of table `{$self->name}`. Please check use of `delete()` method"
612
            );
613
        }
614
615 1
        $table = DbProxy::quoteIdentifier($self->name);
616
617 1
        $sql = "DELETE FROM $table"
618 1
            . " WHERE " . implode(' AND ', self::prepareStatement($where));
619 1
        return DbProxy::query($sql, array_values($where));
620
    }
621
622
    /**
623
     * Setup relation "one to one" or "one to many"
624
     *
625
     * @param  string $key
626
     * @param  string $model
627
     * @param  string $foreign
628
     *
629
     * @return void
630
     */
631
    public function linkTo($key, $model, $foreign)
632
    {
633
        Relations::setRelation($this->model, $key, $model, $foreign);
634
    }
635
636
    /**
637
     * Setup relation "many to many"
638
     * [table1-key] [table1_key-table2-table3_key] [table3-key]
639
     *
640
     * @param  string $model
641
     * @param  string $link
642
     *
643
     * @return void
644
     */
645
    public function linkToMany($model, $link)
646
    {
647
        Relations::setRelations($this->model, $model, [$link]);
648
    }
649
}
650