Completed
Push — master ( a4c193...4ecd26 )
by Anton
10s
created

Table::delete()   B

Complexity

Conditions 3
Paths 3

Size

Total Lines 25
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 3.2621

Importance

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