Completed
Branch master (6f5620)
by Anton
02:08
created

Table::findWhere()   C

Complexity

Conditions 11
Paths 7

Size

Total Lines 49
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 22.3602

Importance

Changes 0
Metric Value
cc 11
eloc 35
nc 7
nop 1
dl 0
loc 49
ccs 18
cts 33
cp 0.5455
crap 22.3602
rs 5.2653
c 0
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 columns
55
     */
56
    protected $columns = [];
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
85
        // autodetect model name
86 2
        if (!$this->model) {
87 2
            $model = substr($tableClass, strpos($tableClass, '\\') + 1);
88 2
            $model = substr($model, 0, strpos($model, '\\', 2));
89 2
            $this->model = $model;
90
        }
91
92
        // autodetect table name - camelCase to uppercase
93 2
        if (!$this->name) {
94
            $table = preg_replace('/(?<=\\w)(?=[A-Z])/', "_$1", $this->model);
95
            $this->name = strtolower($table);
96
        }
97
98
        // autodetect row class
99 2
        if (!$this->rowClass) {
100 1
            $rowClass = substr($tableClass, 0, strrpos($tableClass, '\\', 1) + 1);
101 1
            $this->rowClass = $rowClass . 'Row';
102
        }
103
104
        // setup default select query
105 2
        if (empty($this->select)) {
106 2
            $this->select = "SELECT * " .
107 2
                "FROM " . DbProxy::quoteIdentifier($this->name);
108
        }
109
110 2
        Relations::addClassMap($this->model, $tableClass);
111
112 2
        $this->init();
113 2
    }
114
115
    /**
116
     * Initialization hook.
117
     * Subclasses may override this method
118
     */
119 2
    public function init()
120
    {
121 2
    }
122
123
    /**
124
     * Get Table instance
125
     *
126
     * @return static
127
     */
128 19
    public static function getInstance()
129
    {
130 19
        static $instance;
131 19
        if (null === $instance) {
132 3
            $instance = new static();
133
        }
134
135 19
        return $instance;
136
    }
137
138
    /**
139
     * Set select query
140
     *
141
     * @param  string $select SQL query
142
     *
143
     * @return Table
144
     */
145
    public function setSelectQuery($select)
146
    {
147
        $this->select = $select;
148
        return $this;
149
    }
150
151
    /**
152
     * Get select query
153
     *
154
     * @return string
155
     */
156
    public function getSelectQuery()
157
    {
158
        return $this->select;
159
    }
160
161
    /**
162
     * Get primary key(s)
163
     *
164
     * @return array
165
     * @throws InvalidPrimaryKeyException if primary key was not set or has wrong format
166
     */
167 20
    public function getPrimaryKey()
168
    {
169 20
        if (!is_array($this->primary)) {
170 1
            throw new InvalidPrimaryKeyException("The primary key must be set as an array");
171
        }
172 19
        return $this->primary;
173
    }
174
175
    /**
176
     * Get table name
177
     *
178
     * @return string
179
     */
180 1
    public function getName()
181
    {
182 1
        return $this->name;
183
    }
184
185
    /**
186
     * Get model name
187
     *
188
     * @return string
189
     */
190 1
    public function getModel()
191
    {
192 1
        return $this->model;
193
    }
194
195
    /**
196
     * Return information about tables columns
197
     *
198
     * @return array
199
     */
200 3
    public function getColumns()
201
    {
202 3
        if (empty($this->columns)) {
203 1
            $cacheKey = "db.table.{$this->name}";
204 1
            $columns = Cache::get($cacheKey);
205 1
            if (!$columns) {
206 1
                $schema = DbProxy::getOption('connect', 'name');
207
208 1
                $columns = DbProxy::fetchColumn(
209 1
                    '
210
                    SELECT COLUMN_NAME
211
                    FROM INFORMATION_SCHEMA.COLUMNS
212
                    WHERE TABLE_SCHEMA = ?
213
                      AND TABLE_NAME = ?',
214 1
                    [$schema, $this->getName()]
215
                );
216 1
                Cache::set($cacheKey, $columns, Cache::TTL_NO_EXPIRY, ['system', 'db']);
217
            }
218 1
            $this->columns = $columns;
219
        }
220 3
        return $this->columns;
221
    }
222
223
    /**
224
     * Filter columns for insert/update queries by table columns definition
225
     *
226
     * @param  array $data
227
     *
228
     * @return array
229
     */
230 3
    public static function filterColumns($data)
231
    {
232 3
        $self = static::getInstance();
233 3
        return array_intersect_key($data, array_flip($self->getColumns()));
234
    }
235
236
    /**
237
     * Fetching rows by SQL query
238
     *
239
     * @param  string $sql    SQL query with placeholders
240
     * @param  array  $params Params for query placeholders
241
     *
242
     * @return array of rows results in FETCH_CLASS mode
243
     */
244 7
    public static function fetch($sql, $params = [])
245
    {
246 7
        $self = static::getInstance();
247 7
        return DbProxy::fetchObjects($sql, $params, $self->rowClass);
248
    }
249
250
    /**
251
     * Fetch all rows from table
252
     * Be carefully with this method, can be very slow
253
     *
254
     * @return array of rows results in FETCH_CLASS mode
255
     */
256
    public static function fetchAll()
257
    {
258
        $self = static::getInstance();
259
        return DbProxy::fetchObjects($self->select, [], $self->rowClass);
260
    }
261
262
    /**
263
     * Fetches rows by primary key.  The argument specifies one or more primary
264
     * key value(s).  To find multiple rows by primary key, the argument must
265
     * be an array.
266
     *
267
     * This method accepts a variable number of arguments.  If the table has a
268
     * multi-column primary key, the number of arguments must be the same as
269
     * the number of columns in the primary key.  To find multiple rows in a
270
     * table with a multi-column primary key, each argument must be an array
271
     * with the same number of elements.
272
     *
273
     * The find() method always returns a array
274
     *
275
     * Row by primary key, return array
276
     *     Table::find(123);
277
     *
278
     * Row by compound primary key, return array
279
     *     Table::find([123, 'abc']);
280
     *
281
     * Multiple rows by primary key
282
     *     Table::find(123, 234, 345);
283
     *
284
     * Multiple rows by compound primary key
285
     *     Table::find([123, 'abc'], [234, 'def'], [345, 'ghi'])
286
     *
287
     * @param  mixed ...$keys The value(s) of the primary keys.
288
     *
289
     * @return array
290
     * @throws InvalidPrimaryKeyException if wrong count of values passed
291
     */
292 9
    public static function find(...$keys)
293
    {
294 9
        $keyNames = array_values(static::getInstance()->getPrimaryKey());
295 9
        $whereList = [];
296 9
        foreach ($keys as $keyValues) {
297 9
            $keyValues = (array)$keyValues;
298 9
            if (count($keyValues) < count($keyNames)) {
299 2
                throw new InvalidPrimaryKeyException(
300
                    "Too few columns for the primary key.\n" .
301 2
                    "Please check " . static::class . " initialization or usage.\n" .
302 2
                    "Settings described at https://github.com/bluzphp/framework/wiki/Db-Table"
303
                );
304
            }
305
306 7
            if (count($keyValues) > count($keyNames)) {
307
                throw new InvalidPrimaryKeyException(
308
                    "Too many columns for the primary key.\n" .
309
                    "Please check " . static::class . " initialization or usage.\n" .
310
                    "Settings described at https://github.com/bluzphp/framework/wiki/Db-Table"
311
                );
312
            }
313
314 7
            if (array_keys($keyValues)[0] === 0) {
315
                // for numerical array
316 6
                $whereList[] = array_combine($keyNames, $keyValues);
317
            } else {
318
                // for assoc array
319 1
                $whereList[] = $keyValues;
320
            }
321
        }
322 7
        return static::findWhere(...$whereList);
323
    }
324
325
    /**
326
     * Find row by primary key
327
     *
328
     * @param  mixed $primaryKey
329
     *
330
     * @return Row
331
     */
332 7
    public static function findRow($primaryKey)
333
    {
334 7
        if (!$primaryKey) {
335
            return null;
336
        }
337 7
        $result = static::find($primaryKey);
338 7
        return current($result);
339
    }
340
341
    /**
342
     * Find rows by WHERE
343
     *     // WHERE alias = 'foo'
344
     *     Table::findWhere(['alias'=>'foo']);
345
     *     // WHERE alias = 'foo' OR 'alias' = 'bar'
346
     *     Table::findWhere(['alias'=>'foo'], ['alias'=>'bar']);
347
     *     // WHERE (alias = 'foo' AND userId = 2) OR ('alias' = 'bar' AND userId = 4)
348
     *     Table::findWhere(['alias'=>'foo', 'userId'=> 2], ['alias'=>'foo', 'userId'=>4]);
349
     *     // WHERE alias IN ('foo', 'bar')
350
     *     Table::findWhere(['alias'=> ['foo', 'bar']]);
351
     *
352
     * @param  mixed ...$where
353
     *
354
     * @return array
355
     * @throws \InvalidArgumentException
356
     * @throws Exception\DbException
357
     */
358 7
    public static function findWhere(...$where)
359
    {
360 7
        $self = static::getInstance();
361
362 7
        $whereClause = null;
363 7
        $whereParams = [];
364
365 7
        if (count($where) == 2 && is_string($where[0])) {
366
            $whereClause = $where[0];
367
            $whereParams = (array)$where[1];
368 7
        } elseif (count($where)) {
369 7
            $whereOrTerms = [];
370 7
            foreach ($where as $keyValueSets) {
371 7
                $whereAndTerms = [];
372 7
                foreach ($keyValueSets as $keyName => $keyValue) {
373 7
                    if (is_array($keyValue)) {
374
                        $keyValue = array_map(
375
                            function ($value) use ($self) {
376
                                return DbProxy::quote($value);
377
                            },
378
                            $keyValue
379
                        );
380
                        $keyValue = implode(',', $keyValue);
381
                        $whereAndTerms[] = $self->name . '.' . $keyName . ' IN (' . $keyValue . ')';
382 7
                    } elseif (is_null($keyValue)) {
383
                        $whereAndTerms[] = $self->name . '.' . $keyName . ' IS NULL';
384
                    } else {
385 7
                        $whereAndTerms[] = $self->name . '.' . $keyName . ' = ?';
386 7
                        $whereParams[] = $keyValue;
387
                    }
388 7
                    if (!is_scalar($keyValue) && !is_null($keyValue)) {
389
                        throw new \InvalidArgumentException(
390
                            "Wrong arguments of method 'findWhere'.\n" .
391
                            "Please use syntax described at https://github.com/bluzphp/framework/wiki/Db-Table"
392
                        );
393
                    }
394
                }
395 7
                $whereOrTerms[] = '(' . implode(' AND ', $whereAndTerms) . ')';
396
            }
397 7
            $whereClause = '(' . implode(' OR ', $whereOrTerms) . ')';
398
        } elseif (!count($where)) {
399
            throw new DbException(
400
                "Method `Table::findWhere()` can't return all records from table,\n" .
401
                "please use `Table::fetchAll()` instead"
402
            );
403
        }
404
405 7
        return static::fetch($self->select . ' WHERE ' . $whereClause, $whereParams);
406
    }
407
408
    /**
409
     * Find row by where condition
410
     *
411
     * @param  array $whereList
412
     *
413
     * @return Row
414
     */
415
    public static function findRowWhere($whereList)
416
    {
417
        $result = static::findWhere($whereList);
418
        return current($result);
419
    }
420
421
    /**
422
     * Prepare array for WHERE or SET statements
423
     *
424
     * @param  array $where
425
     *
426
     * @return array
427
     * @throws \Bluz\Common\Exception\ConfigurationException
428
     */
429 3
    private static function prepareStatement($where)
430
    {
431 3
        $keys = array_keys($where);
432 3
        foreach ($keys as &$key) {
433 3
            $key = DbProxy::quoteIdentifier($key) . ' = ?';
434
        }
435 3
        return $keys;
436
    }
437
438
    /**
439
     * Prepare Db\Query\Select for current table:
440
     *  - predefine "select" section as "*" from current table
441
     *  - predefine "from" section as current table name and first letter as alias
442
     *  - predefine fetch type
443
     *
444
     * <code>
445
     *     // use default select "*"
446
     *     $select = Users\Table::select();
447
     *     $arrUsers = $select->where('u.id = ?', $id)
448
     *         ->execute();
449
     *
450
     *     // setup custom select "u.id, u.login"
451
     *     $select = Users\Table::select();
452
     *     $arrUsers = $select->select('u.id, u.login')
453
     *         ->where('u.id = ?', $id)
454
     *         ->execute();
455
     * </code>
456
     *
457
     * @return Query\Select
458
     */
459 1
    public static function select()
460
    {
461 1
        $self = static::getInstance();
462
463 1
        $select = new Query\Select();
464 1
        $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...
465 1
            ->from($self->name, $self->name)
466 1
            ->setFetchType($self->rowClass);
467
468 1
        return $select;
469
    }
470
471
    /**
472
     * Create Row instance
473
     *
474
     * @param  array $data
475
     *
476
     * @return Row
477
     */
478 2
    public static function create(array $data = [])
479
    {
480 2
        $rowClass = static::getInstance()->rowClass;
481
        /** @var Row $row */
482 2
        $row = new $rowClass($data);
483 2
        $row->setTable(static::getInstance());
484 2
        return $row;
485
    }
486
487
    /**
488
     * Insert new record to table and return last insert Id
489
     *
490
     * <code>
491
     *     Table::insert(['login' => 'Man', 'email' => '[email protected]'])
492
     * </code>
493
     *
494
     * @param  array $data Column-value pairs
495
     *
496
     * @return string|null Primary key or null
497
     * @throws Exception\DbException
498
     */
499 1
    public static function insert(array $data)
500
    {
501 1
        $self = static::getInstance();
502
503 1
        $data = static::filterColumns($data);
504
505 1
        if (!count($data)) {
506
            throw new DbException(
507
                "Invalid field names of table `{$self->name}`. Please check use of `insert()` method"
508
            );
509
        }
510
511 1
        $table = DbProxy::quoteIdentifier($self->name);
512
513 1
        $sql = "INSERT INTO $table SET " . implode(',', self::prepareStatement($data));
514 1
        $result = DbProxy::query($sql, array_values($data));
515 1
        if (!$result) {
516
            return null;
517
        }
518
519
        /**
520
         * If a sequence name was not specified for the name parameter, PDO::lastInsertId()
521
         * returns a string representing the row ID of the last row that was inserted into the database.
522
         *
523
         * If a sequence name was specified for the name parameter, PDO::lastInsertId()
524
         * returns a string representing the last value retrieved from the specified sequence object.
525
         *
526
         * If the PDO driver does not support this capability, PDO::lastInsertId() triggers an IM001 SQLSTATE.
527
         */
528 1
        return DbProxy::handler()->lastInsertId($self->sequence);
529
    }
530
531
    /**
532
     * Updates existing rows
533
     *
534
     * <code>
535
     *     Table::insert(['login' => 'Man', 'email' => '[email protected]'], ['id' => 42])
536
     * </code>
537
     *
538
     * @param  array $data  Column-value pairs.
539
     * @param  array $where An array of SQL WHERE clause(s)
540
     *
541
     * @return integer The number of rows updated
542
     * @throws Exception\DbException
543
     */
544 1
    public static function update(array $data, array $where)
545
    {
546 1
        if (!count($where)) {
547
            throw new DbException(
548
                "Method `Table::update()` can't update all records in table,\n" .
549
                "please use `Db::query()` instead (of cause if you know what are you doing)"
550
            );
551
        }
552
553 1
        $self = static::getInstance();
554
555 1
        $data = static::filterColumns($data);
556
557 1
        $where = static::filterColumns($where);
558
559 1
        if (!count($data) || !count($where)) {
560
            throw new DbException(
561
                "Invalid field names of table `{$self->name}`. Please check use of `update()` method"
562
            );
563
        }
564
565 1
        $table = DbProxy::quoteIdentifier($self->name);
566
567 1
        $sql = "UPDATE $table"
568 1
            . " SET " . implode(',', self::prepareStatement($data))
569 1
            . " WHERE " . implode(' AND ', self::prepareStatement($where));
570
571 1
        return DbProxy::query($sql, array_merge(array_values($data), array_values($where)));
572
    }
573
574
    /**
575
     * Deletes existing rows
576
     *
577
     * <code>
578
     *     Table::delete(['login' => 'Man'])
579
     * </code>
580
     *
581
     * @param  array $where An array of SQL WHERE clause(s)
582
     *
583
     * @return integer The number of rows deleted
584
     * @throws Exception\DbException
585
     */
586 1
    public static function delete(array $where)
587
    {
588 1
        if (!count($where)) {
589
            throw new DbException(
590
                "Method `Table::delete()` can't delete all records in table,\n" .
591
                "please use `Db::query()` instead (of cause if you know what are you doing)"
592
            );
593
        }
594
595 1
        $self = static::getInstance();
596
597 1
        $where = static::filterColumns($where);
598
599 1
        if (!count($where)) {
600
            throw new DbException(
601
                "Invalid field names of table `{$self->name}`. Please check use of `delete()` method"
602
            );
603
        }
604
605 1
        $table = DbProxy::quoteIdentifier($self->name);
606
607 1
        $sql = "DELETE FROM $table"
608 1
            . " WHERE " . implode(' AND ', self::prepareStatement($where));
609 1
        return DbProxy::query($sql, array_values($where));
610
    }
611
612
    /**
613
     * Setup relation "one to one" or "one to many"
614
     *
615
     * @param  string $key
616
     * @param  string $model
617
     * @param  string $foreign
618
     *
619
     * @return void
620
     */
621
    public function linkTo($key, $model, $foreign)
622
    {
623
        Relations::setRelation($this->model, $key, $model, $foreign);
624
    }
625
626
    /**
627
     * Setup relation "many to many"
628
     * [table1-key] [table1_key-table2-table3_key] [table3-key]
629
     *
630
     * @param  string $model
631
     * @param  string $link
632
     *
633
     * @return void
634
     */
635
    public function linkToMany($model, $link)
636
    {
637
        Relations::setRelations($this->model, $model, [$link]);
638
    }
639
}
640