Completed
Pull Request — master (#435)
by Anton
04:39
created

Table::update()   B

Complexity

Conditions 4
Paths 3

Size

Total Lines 29
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 4.3035

Importance

Changes 0
Metric Value
cc 4
eloc 16
nc 3
nop 2
dl 0
loc 29
ccs 11
cts 15
cp 0.7332
crap 4.3035
rs 8.5806
c 0
b 0
f 0
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 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 34
    public static function getInstance()
127
    {
128 34
        static $instance;
129 34
        if (null === $instance) {
130 3
            $instance = new static();
131
        }
132
133 34
        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 29
    public function getPrimaryKey()
166
    {
167 29
        if (!is_array($this->primary)) {
168 1
            throw new InvalidPrimaryKeyException('The primary key must be set as an array');
169
        }
170 28
        return $this->primary;
171
    }
172
173
    /**
174
     * Get table name
175
     *
176
     * @return string
177
     */
178 1
    public function getName()
179
    {
180 1
        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 tables columns
195
     *
196
     * @return array
197
     */
198 3
    public function getColumns()
199
    {
200 3
        if (empty($this->columns)) {
201 1
            $cacheKey = "db.table.{$this->name}";
202 1
            $columns = Cache::get($cacheKey);
203 1
            if (!$columns) {
204 1
                $schema = DbProxy::getOption('connect', 'name');
205
206 1
                $columns = DbProxy::fetchColumn(
207 1
                    '
208
                    SELECT COLUMN_NAME
209
                    FROM INFORMATION_SCHEMA.COLUMNS
210
                    WHERE TABLE_SCHEMA = ?
211
                      AND TABLE_NAME = ?',
212 1
                    [$schema, $this->getName()]
213
                );
214 1
                Cache::set($cacheKey, $columns, Cache::TTL_NO_EXPIRY, ['system', 'db']);
215
            }
216 1
            $this->columns = $columns;
217
        }
218 3
        return $this->columns;
219
    }
220
221
    /**
222
     * Filter columns for insert/update queries by table columns definition
223
     *
224
     * @param  array $data
225
     *
226
     * @return array
227
     */
228 3
    public static function filterColumns($data)
229
    {
230 3
        $self = static::getInstance();
231 3
        return array_intersect_key($data, array_flip($self->getColumns()));
232
    }
233
234
    /**
235
     * Fetching rows by SQL query
236
     *
237
     * @param  string $sql    SQL query with placeholders
238
     * @param  array  $params Params for query placeholders
239
     *
240
     * @return array of rows results in FETCH_CLASS mode
241
     */
242 7
    public static function fetch($sql, $params = [])
243
    {
244 7
        $self = static::getInstance();
245 7
        return DbProxy::fetchObjects($sql, $params, $self->rowClass);
246
    }
247
248
    /**
249
     * Fetch all rows from table
250
     * Be carefully with this method, can be very slow
251
     *
252
     * @return array of rows results in FETCH_CLASS mode
253
     */
254
    public static function fetchAll()
255
    {
256
        $self = static::getInstance();
257
        return DbProxy::fetchObjects($self->select, [], $self->rowClass);
258
    }
259
260
    /**
261
     * Fetches rows by primary key.  The argument specifies one or more primary
262
     * key value(s).  To find multiple rows by primary key, the argument must
263
     * be an array.
264
     *
265
     * This method accepts a variable number of arguments.  If the table has a
266
     * multi-column primary key, the number of arguments must be the same as
267
     * the number of columns in the primary key.  To find multiple rows in a
268
     * table with a multi-column primary key, each argument must be an array
269
     * with the same number of elements.
270
     *
271
     * The find() method always returns a array
272
     *
273
     * Row by primary key, return array
274
     *     Table::find(123);
275
     *
276
     * Row by compound primary key, return array
277
     *     Table::find([123, 'abc']);
278
     *
279
     * Multiple rows by primary key
280
     *     Table::find(123, 234, 345);
281
     *
282
     * Multiple rows by compound primary key
283
     *     Table::find([123, 'abc'], [234, 'def'], [345, 'ghi'])
284
     *
285
     * @param  mixed ...$keys The value(s) of the primary keys.
286
     *
287
     * @return array
288
     * @throws InvalidPrimaryKeyException if wrong count of values passed
289
     */
290 9
    public static function find(...$keys)
291
    {
292 9
        $keyNames = array_values(static::getInstance()->getPrimaryKey());
293 9
        $whereList = [];
294 9
        foreach ($keys as $keyValues) {
295 9
            $keyValues = (array)$keyValues;
296 9
            if (count($keyValues) < count($keyNames)) {
297 2
                throw new InvalidPrimaryKeyException(
298
                    "Too few columns for the primary key.\n" .
299 2
                    "Please check " . static::class . " initialization or usage.\n" .
300 2
                    "Settings described at https://github.com/bluzphp/framework/wiki/Db-Table"
301
                );
302
            }
303
304 7
            if (count($keyValues) > count($keyNames)) {
305
                throw new InvalidPrimaryKeyException(
306
                    "Too many columns for the primary key.\n" .
307
                    "Please check " . static::class . " initialization or usage.\n" .
308
                    "Settings described at https://github.com/bluzphp/framework/wiki/Db-Table"
309
                );
310
            }
311
312 7
            if (array_keys($keyValues)[0] === 0) {
313
                // for numerical array
314 6
                $whereList[] = array_combine($keyNames, $keyValues);
315
            } else {
316
                // for assoc array
317 7
                $whereList[] = $keyValues;
318
            }
319
        }
320 7
        return static::findWhere(...$whereList);
321
    }
322
323
    /**
324
     * Find row by primary key
325
     *
326
     * @param  mixed $primaryKey
327
     *
328
     * @return Row
329
     */
330 7
    public static function findRow($primaryKey)
331
    {
332 7
        if (!$primaryKey) {
333
            return null;
334
        }
335 7
        $result = static::find($primaryKey);
336 7
        return current($result);
337
    }
338
339
    /**
340
     * Find rows by WHERE
341
     *     // WHERE alias = 'foo'
342
     *     Table::findWhere(['alias'=>'foo']);
343
     *     // WHERE alias = 'foo' OR 'alias' = 'bar'
344
     *     Table::findWhere(['alias'=>'foo'], ['alias'=>'bar']);
345
     *     // WHERE (alias = 'foo' AND userId = 2) OR ('alias' = 'bar' AND userId = 4)
346
     *     Table::findWhere(['alias'=>'foo', 'userId'=> 2], ['alias'=>'foo', 'userId'=>4]);
347
     *     // WHERE alias IN ('foo', 'bar')
348
     *     Table::findWhere(['alias'=> ['foo', 'bar']]);
349
     *
350
     * @param  mixed ...$where
351
     *
352
     * @return array
353
     * @throws \InvalidArgumentException
354
     * @throws Exception\DbException
355
     */
356 7
    public static function findWhere(...$where)
357
    {
358 7
        $self = static::getInstance();
359
360 7
        $whereClause = null;
0 ignored issues
show
Unused Code introduced by
$whereClause is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
361 7
        $whereParams = [];
362
363 7
        if (count($where) === 2 && is_string($where[0])) {
364
            $whereClause = $where[0];
365
            $whereParams = (array)$where[1];
366 7
        } elseif (count($where)) {
367 7
            $whereOrTerms = [];
368 7
            foreach ($where as $keyValueSets) {
369 7
                $whereAndTerms = [];
370 7
                foreach ($keyValueSets as $keyName => $keyValue) {
371 7
                    if (is_array($keyValue)) {
372
                        $keyValue = array_map(
373
                            function ($value) use ($self) {
374
                                return DbProxy::quote($value);
375
                            },
376
                            $keyValue
377
                        );
378
                        $keyValue = implode(',', $keyValue);
379
                        $whereAndTerms[] = $self->name . '.' . $keyName . ' IN (' . $keyValue . ')';
380 7
                    } elseif (is_null($keyValue)) {
381
                        $whereAndTerms[] = $self->name . '.' . $keyName . ' IS NULL';
382
                    } else {
383 7
                        $whereAndTerms[] = $self->name . '.' . $keyName . ' = ?';
384 7
                        $whereParams[] = $keyValue;
385
                    }
386 7
                    if (!is_scalar($keyValue) && !is_null($keyValue)) {
387
                        throw new \InvalidArgumentException(
388
                            "Wrong arguments of method 'findWhere'.\n" .
389 7
                            "Please use syntax described at https://github.com/bluzphp/framework/wiki/Db-Table"
390
                        );
391
                    }
392
                }
393 7
                $whereOrTerms[] = '(' . implode(' AND ', $whereAndTerms) . ')';
394
            }
395 7
            $whereClause = '(' . implode(' OR ', $whereOrTerms) . ')';
396
        } else {
397
            throw new DbException(
398
                "Method `Table::findWhere()` can't return all records from table,\n" .
399
                "please use `Table::fetchAll()` instead"
400
            );
401
        }
402
403 7
        return static::fetch($self->select . ' WHERE ' . $whereClause, $whereParams);
404
    }
405
406
    /**
407
     * Find row by where condition
408
     *
409
     * @param  array $whereList
410
     *
411
     * @return Row
412
     */
413
    public static function findRowWhere($whereList)
414
    {
415
        $result = static::findWhere($whereList);
416
        return current($result);
417
    }
418
419
    /**
420
     * Prepare array for WHERE or SET statements
421
     *
422
     * @param  array $where
423
     *
424
     * @return array
425
     * @throws \Bluz\Common\Exception\ConfigurationException
426
     */
427 3
    private static function prepareStatement($where)
428
    {
429 3
        $keys = array_keys($where);
430 3
        foreach ($keys as &$key) {
431 3
            $key = DbProxy::quoteIdentifier($key) . ' = ?';
432
        }
433 3
        return $keys;
434
    }
435
436
    /**
437
     * Prepare Db\Query\Select for current table:
438
     *  - predefine "select" section as "*" from current table
439
     *  - predefine "from" section as current table name and first letter as alias
440
     *  - predefine fetch type
441
     *
442
     * <code>
443
     *     // use default select "*"
444
     *     $select = Users\Table::select();
445
     *     $arrUsers = $select->where('u.id = ?', $id)
446
     *         ->execute();
447
     *
448
     *     // setup custom select "u.id, u.login"
449
     *     $select = Users\Table::select();
450
     *     $arrUsers = $select->select('u.id, u.login')
451
     *         ->where('u.id = ?', $id)
452
     *         ->execute();
453
     * </code>
454
     *
455
     * @return Query\Select
456
     */
457 1
    public static function select()
458
    {
459 1
        $self = static::getInstance();
460
461 1
        $select = new Query\Select();
462 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...
463 1
            ->from($self->name, $self->name)
464 1
            ->setFetchType($self->rowClass);
465
466 1
        return $select;
467
    }
468
469
    /**
470
     * Create Row instance
471
     *
472
     * @param  array $data
473
     *
474
     * @return Row
475
     */
476 2
    public static function create(array $data = [])
477
    {
478 2
        $rowClass = static::getInstance()->rowClass;
479
        /** @var Row $row */
480 2
        $row = new $rowClass($data);
481 2
        $row->setTable(static::getInstance());
482 2
        return $row;
483
    }
484
485
    /**
486
     * Insert new record to table and return last insert Id
487
     *
488
     * <code>
489
     *     Table::insert(['login' => 'Man', 'email' => '[email protected]'])
490
     * </code>
491
     *
492
     * @param  array $data Column-value pairs
493
     *
494
     * @return string|null Primary key or null
495
     * @throws Exception\DbException
496
     */
497 1
    public static function insert(array $data)
498
    {
499 1
        $self = static::getInstance();
500
501 1
        $data = static::filterColumns($data);
502
503 1
        if (!count($data)) {
504
            throw new DbException(
505
                "Invalid field names of table `{$self->name}`. Please check use of `insert()` method"
506
            );
507
        }
508
509 1
        $table = DbProxy::quoteIdentifier($self->name);
510
511 1
        $sql = "INSERT INTO $table SET " . implode(',', self::prepareStatement($data));
512 1
        $result = DbProxy::query($sql, array_values($data));
513 1
        if (!$result) {
514
            return null;
515
        }
516
517
        /**
518
         * If a sequence name was not specified for the name parameter, PDO::lastInsertId()
519
         * returns a string representing the row ID of the last row that was inserted into the database.
520
         *
521
         * If a sequence name was specified for the name parameter, PDO::lastInsertId()
522
         * returns a string representing the last value retrieved from the specified sequence object.
523
         *
524
         * If the PDO driver does not support this capability, PDO::lastInsertId() triggers an IM001 SQLSTATE.
525
         */
526 1
        return DbProxy::handler()->lastInsertId($self->sequence);
527
    }
528
529
    /**
530
     * Updates existing rows
531
     *
532
     * <code>
533
     *     Table::insert(['login' => 'Man', 'email' => '[email protected]'], ['id' => 42])
534
     * </code>
535
     *
536
     * @param  array $data  Column-value pairs.
537
     * @param  array $where An array of SQL WHERE clause(s)
538
     *
539
     * @return integer The number of rows updated
540
     * @throws Exception\DbException
541
     */
542 1
    public static function update(array $data, array $where)
543
    {
544 1
        if (!count($where)) {
545
            throw new DbException(
546
                "Method `Table::update()` can't update all records in table,\n" .
547
                "please use `Db::query()` instead (of cause if you know what are you doing)"
548
            );
549
        }
550
551 1
        $self = static::getInstance();
552
553 1
        $data = static::filterColumns($data);
554
555 1
        $where = static::filterColumns($where);
556
557 1
        if (!count($data) || !count($where)) {
558
            throw new DbException(
559
                "Invalid field names of table `{$self->name}`. Please check use of `update()` method"
560
            );
561
        }
562
563 1
        $table = DbProxy::quoteIdentifier($self->name);
564
565 1
        $sql = "UPDATE $table"
566 1
            . " SET " . implode(',', self::prepareStatement($data))
567 1
            . " WHERE " . implode(' AND ', self::prepareStatement($where));
568
569 1
        return DbProxy::query($sql, array_merge(array_values($data), array_values($where)));
570
    }
571
572
    /**
573
     * Deletes existing rows
574
     *
575
     * <code>
576
     *     Table::delete(['login' => 'Man'])
577
     * </code>
578
     *
579
     * @param  array $where An array of SQL WHERE clause(s)
580
     *
581
     * @return integer The number of rows deleted
582
     * @throws Exception\DbException
583
     */
584 1
    public static function delete(array $where)
585
    {
586 1
        if (!count($where)) {
587
            throw new DbException(
588
                "Method `Table::delete()` can't delete all records in table,\n" .
589
                "please use `Db::query()` instead (of cause if you know what are you doing)"
590
            );
591
        }
592
593 1
        $self = static::getInstance();
594
595 1
        $where = static::filterColumns($where);
596
597 1
        if (!count($where)) {
598
            throw new DbException(
599
                "Invalid field names of table `{$self->name}`. Please check use of `delete()` method"
600
            );
601
        }
602
603 1
        $table = DbProxy::quoteIdentifier($self->name);
604
605 1
        $sql = "DELETE FROM $table"
606 1
            . " WHERE " . implode(' AND ', self::prepareStatement($where));
607 1
        return DbProxy::query($sql, array_values($where));
608
    }
609
610
    /**
611
     * Setup relation "one to one" or "one to many"
612
     *
613
     * @param  string $key
614
     * @param  string $model
615
     * @param  string $foreign
616
     *
617
     * @return void
618
     */
619
    public function linkTo($key, $model, $foreign)
620
    {
621
        Relations::setRelation($this->model, $key, $model, $foreign);
622
    }
623
624
    /**
625
     * Setup relation "many to many"
626
     * [table1-key] [table1_key-table2-table3_key] [table3-key]
627
     *
628
     * @param  string $model
629
     * @param  string $link
630
     *
631
     * @return void
632
     */
633
    public function linkToMany($model, $link)
634
    {
635
        Relations::setRelations($this->model, $model, [$link]);
636
    }
637
}
638