Completed
Pull Request — master (#384)
by Anton
05:26
created

Table::insert()   B

Complexity

Conditions 3
Paths 3

Size

Total Lines 31
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
cc 3
eloc 12
nc 3
nop 1
dl 0
loc 31
ccs 0
cts 12
cp 0
crap 12
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
/**
10
 * @namespace
11
 */
12
namespace Bluz\Db;
13
14
use Bluz\Db\Exception\DbException;
15
use Bluz\Db\Exception\InvalidPrimaryKeyException;
16
use Bluz\Proxy\Cache;
17
use Bluz\Proxy\Db as DbProxy;
18
19
/**
20
 * Table
21
 *
22
 * Example of Users\Table
23
 * <code>
24
 *     namespace Application\Users;
25
 *     class Table extends \Bluz\Db\Table
26
 *     {
27
 *        protected $table = 'users';
28
 *        protected $primary = ['id'];
29
 *     }
30
 *
31
 *     $userRows = \Application\Users\Table::find(1,2,3,4,5);
32
 *     foreach ($userRows as $userRow) {
33
 *        $userRow -> description = 'In first 5';
34
 *        $userRow -> save();
35
 *     }
36
 * </code>
37
 *
38
 * @package  Bluz\Db
39
 * @author   Anton Shevchuk
40
 * @link     https://github.com/bluzphp/framework/wiki/Db-Table
41
 */
42
abstract class Table
43
{
44
    /**
45
     * @var string the table name
46
     */
47
    protected $name;
48
49
    /**
50
     * @var string the model name
51
     */
52
    protected $model;
53
54
    /**
55
     * @var array table columns
56
     */
57
    protected $columns = [];
58
59
    /**
60
     * @var string default SQL query for select
61
     */
62
    protected $select = "";
63
64
    /**
65
     * @var array the primary key column or columns (only as array).
66
     */
67
    protected $primary;
68
69
    /**
70
     * @var string the sequence name, required for PostgreSQL
71
     */
72
    protected $sequence;
73
74
    /**
75
     * @var string row class name
76
     */
77
    protected $rowClass;
78
79
    /**
80
     * Create and initialize Table instance
81
     */
82 1
    private function __construct()
83
    {
84 1
        $tableClass = static::class;
85
86
        // autodetect model name
87 1
        if (!$this->model) {
88 1
            $model = substr($tableClass, strpos($tableClass, '\\') + 1);
89 1
            $model = substr($model, 0, strpos($model, '\\', 2));
90 1
            $this->model = $model;
91
        }
92
93
        // autodetect table name - camelCase to uppercase
94 1
        if (!$this->name) {
95
            $table = preg_replace('/(?<=\\w)(?=[A-Z])/', "_$1", $this->model);
96
            $this->name = strtolower($table);
97
        }
98
99
        // autodetect row class
100 1
        if (!$this->rowClass) {
101 1
            $rowClass = substr($tableClass, 0, strrpos($tableClass, '\\', 1) + 1);
102 1
            $this->rowClass = $rowClass . 'Row';
103
        }
104
105
        // setup default select query
106 1
        if (empty($this->select)) {
107 1
            $this->select = "SELECT * ".
108 1
                "FROM " . DbProxy::quoteIdentifier($this->name);
109
        }
110
111 1
        Relations::addClassMap($this->model, $tableClass);
112
113 1
        $this->init();
114 1
    }
115
116
    /**
117
     * Initialization hook.
118
     * Subclasses may override this method
119
     */
120 1
    public function init()
121
    {
122 1
    }
123
124
    /**
125
     * Get Table instance
126
     *
127
     * @return static
128
     */
129 7
    public static function getInstance()
130
    {
131 7
        static $instance;
132 7
        if (null === $instance) {
133 2
            $instance = new static();
134
        }
135
136 7
        return $instance;
137
    }
138
139
    /**
140
     * Set select query
141
     *
142
     * @param  string $select SQL query
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 5
    public function getPrimaryKey()
168
    {
169 5
        if (!is_array($this->primary)) {
170 1
            throw new InvalidPrimaryKeyException("The primary key must be set as an array");
171
        }
172 4
        return $this->primary;
173
    }
174
175
    /**
176
     * Get table name
177
     *
178
     * @return string
179
     */
180
    public function getName()
181
    {
182
        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
    public function getColumns()
201
    {
202
        if (empty($this->columns)) {
203
            $cacheKey = 'db.table.'. $this->name;
204
            $columns = Cache::get($cacheKey);
205
            if (!$columns) {
206
                $connect = DbProxy::getOption('connect');
207
208
                $columns = DbProxy::fetchColumn(
209
                    '
210
                    SELECT COLUMN_NAME
211
                    FROM INFORMATION_SCHEMA.COLUMNS
212
                    WHERE TABLE_SCHEMA = ?
213
                      AND TABLE_NAME = ?',
214
                    [$connect['name'], $this->getName()]
215
                );
216
                Cache::set($cacheKey, $columns, Cache::TTL_NO_EXPIRY, ['system', 'db']);
217
            }
218
            $this->columns = $columns;
219
        }
220
        return $this->columns;
221
    }
222
223
    /**
224
     * Filter columns for insert/update queries by table columns definition
225
     *
226
     * @param  array $data
227
     * @return array
228
     */
229
    public static function filterColumns($data)
230
    {
231
        $self = static::getInstance();
232
        return array_intersect_key($data, array_flip($self->getColumns()));
233
    }
234
235
    /**
236
     * Fetching rows by SQL query
237
     *
238
     * @param  string $sql    SQL query with placeholders
239
     * @param  array  $params Params for query placeholders
240
     * @return array of rows results in FETCH_CLASS mode
241
     */
242 1
    public static function fetch($sql, $params = [])
243
    {
244 1
        $self = static::getInstance();
245 1
        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
     * @return array
287
     * @throws InvalidPrimaryKeyException if wrong count of values passed
288
     */
289 3
    public static function find(...$keys)
290
    {
291 3
        $keyNames = array_values(static::getInstance()->getPrimaryKey());
292 3
        $whereList = [];
293 3
        foreach ($keys as $keyValues) {
294 3
            $keyValues = (array)$keyValues;
295 3
            if (count($keyValues) < count($keyNames)) {
296 2
                throw new InvalidPrimaryKeyException(
297
                    "Too few columns for the primary key.\n" .
298 2
                    "Please check " . static::class . " initialization or usage.\n" .
299 2
                    "Settings described at https://github.com/bluzphp/framework/wiki/Db-Table"
300
                );
301
            }
302
303 1
            if (count($keyValues) > count($keyNames)) {
304
                throw new InvalidPrimaryKeyException(
305
                    "Too many columns for the primary key.\n" .
306
                    "Please check " . static::class . " initialization or usage.\n" .
307
                    "Settings described at https://github.com/bluzphp/framework/wiki/Db-Table"
308
                );
309
            }
310
311 1
            if (array_keys($keyValues)[0] === 0) {
312
                // for numerical array
313
                $whereList[] = array_combine($keyNames, $keyValues);
314
            } else {
315
                // for assoc array
316 1
                $whereList[] = $keyValues;
317
            }
318
        }
319 1
        return static::findWhere(...$whereList);
320
    }
321
322
    /**
323
     * Find row by primary key
324
     *
325
     * @param  mixed $primaryKey
326
     * @return Row
327
     */
328 1
    public static function findRow($primaryKey)
329
    {
330 1
        if (!$primaryKey) {
331
            return null;
332
        }
333 1
        $result = static::find($primaryKey);
334 1
        return current($result);
335
    }
336
337
    /**
338
     * Find rows by WHERE
339
     *     // WHERE alias = 'foo'
340
     *     Table::findWhere(['alias'=>'foo']);
341
     *     // WHERE alias = 'foo' OR 'alias' = 'bar'
342
     *     Table::findWhere(['alias'=>'foo'], ['alias'=>'bar']);
343
     *     // WHERE (alias = 'foo' AND userId = 2) OR ('alias' = 'bar' AND userId = 4)
344
     *     Table::findWhere(['alias'=>'foo', 'userId'=> 2], ['alias'=>'foo', 'userId'=>4]);
345
     *     // WHERE alias IN ('foo', 'bar')
346
     *     Table::findWhere(['alias'=> ['foo', 'bar']]);
347
     *
348
     * @param  mixed ...$where
349
     * @return array
350
     * @throws \InvalidArgumentException
351
     * @throws Exception\DbException
352
     */
353 1
    public static function findWhere(...$where)
354
    {
355 1
        $self = static::getInstance();
356
357 1
        $whereClause = null;
358 1
        $whereParams = [];
359
360 1
        if (sizeof($where) == 2 && is_string($where[0])) {
361
            $whereClause = $where[0];
362
            $whereParams = (array)$where[1];
363 1
        } elseif (sizeof($where)) {
364 1
            $whereOrTerms = [];
365 1
            foreach ($where as $keyValueSets) {
366 1
                $whereAndTerms = [];
367 1
                foreach ($keyValueSets as $keyName => $keyValue) {
368 1
                    if (is_array($keyValue)) {
369
                        $keyValue = array_map(
370
                            function ($value) use ($self) {
371
                                return DbProxy::quote($value);
372
                            },
373
                            $keyValue
374
                        );
375
                        $keyValue = join(',', $keyValue);
376
                        $whereAndTerms[] = $self->name . '.' . $keyName . ' IN ('.$keyValue.')';
377 1
                    } elseif (is_null($keyValue)) {
378
                        $whereAndTerms[] = $self->name . '.' . $keyName . ' IS NULL';
379
                    } else {
380 1
                        $whereAndTerms[] = $self->name . '.' . $keyName . ' = ?';
381 1
                        $whereParams[] = $keyValue;
382
                    }
383 1
                    if (!is_scalar($keyValue) && !is_null($keyValue)) {
384
                        throw new \InvalidArgumentException(
385
                            "Wrong arguments of method 'findWhere'.\n" .
386 1
                            "Please use syntax described at https://github.com/bluzphp/framework/wiki/Db-Table"
387
                        );
388
                    }
389
                }
390 1
                $whereOrTerms[] = '(' . implode(' AND ', $whereAndTerms) . ')';
391
            }
392 1
            $whereClause = '(' . implode(' OR ', $whereOrTerms) . ')';
393
        } elseif (!sizeof($where)) {
394
            throw new DbException(
395
                "Method `Table::findWhere()` can't return all records from table,\n".
396
                "please use `Table::fetchAll()` instead"
397
            );
398
        }
399
400 1
        return static::fetch($self->select . ' WHERE ' . $whereClause, $whereParams);
401
    }
402
403
    /**
404
     * Find row by where condition
405
     *
406
     * @param  array $whereList
407
     * @return Row
408
     */
409
    public static function findRowWhere($whereList)
410
    {
411
        $result = static::findWhere($whereList);
412
        return current($result);
413
    }
414
415
    /**
416
     * Prepare array for WHERE or SET statements
417
     *
418
     * @param  array $where
419
     * @return array
420
     * @throws \Bluz\Common\Exception\ConfigurationException
421
     */
422
    private static function prepareStatement($where)
423
    {
424
        $keys = array_keys($where);
425
        foreach ($keys as &$key) {
426
            $key = DbProxy::quoteIdentifier($key) . ' = ?';
427
        }
428
        return $keys;
429
    }
430
431
    /**
432
     * Prepare Db\Query\Select for current table:
433
     *  - predefine "select" section as "*" from current table
434
     *  - predefine "from" section as current table name and first letter as alias
435
     *  - predefine fetch type
436
     *
437
     * <code>
438
     *     // use default select "*"
439
     *     $select = Users\Table::select();
440
     *     $arrUsers = $select->where('u.id = ?', $id)
441
     *         ->execute();
442
     *
443
     *     // setup custom select "u.id, u.login"
444
     *     $select = Users\Table::select();
445
     *     $arrUsers = $select->select('u.id, u.login')
446
     *         ->where('u.id = ?', $id)
447
     *         ->execute();
448
     * </code>
449
     *
450
     * @return Query\Select
451
     */
452
    public static function select()
453
    {
454
        $self = static::getInstance();
455
456
        $select = new Query\Select();
457
        $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...
458
            ->from($self->name, $self->name)
459
            ->setFetchType($self->rowClass);
460
461
        return $select;
462
    }
463
464
    /**
465
     * Create Row instance
466
     *
467
     * @param  array $data
468
     * @return Row
469
     */
470
    public static function create(array $data = [])
471
    {
472
        $rowClass = static::getInstance()->rowClass;
473
        /** @var Row $row */
474
        $row = new $rowClass($data);
475
        $row->setTable(static::getInstance());
476
        return $row;
477
    }
478
479
    /**
480
     * Insert new record to table and return last insert Id
481
     *
482
     * <code>
483
     *     Table::insert(['login' => 'Man', 'email' => '[email protected]'])
484
     * </code>
485
     *
486
     * @param  array $data Column-value pairs
487
     * @return string|null Primary key or null
488
     * @throws Exception\DbException
489
     */
490
    public static function insert(array $data)
491
    {
492
        $self = static::getInstance();
493
494
        $data = static::filterColumns($data);
495
496
        if (!sizeof($data)) {
497
            throw new DbException(
498
                "Invalid field names of table `{$self->name}`. Please check use of `insert()` method"
499
            );
500
        }
501
502
        $table = DbProxy::quoteIdentifier($self->name);
503
504
        $sql = "INSERT INTO $table SET " . join(',', self::prepareStatement($data));
505
        $result = DbProxy::query($sql, array_values($data));
506
        if (!$result) {
507
            return null;
508
        }
509
510
        /**
511
         * If a sequence name was not specified for the name parameter, PDO::lastInsertId()
512
         * returns a string representing the row ID of the last row that was inserted into the database.
513
         *
514
         * If a sequence name was specified for the name parameter, PDO::lastInsertId()
515
         * returns a string representing the last value retrieved from the specified sequence object.
516
         *
517
         * If the PDO driver does not support this capability, PDO::lastInsertId() triggers an IM001 SQLSTATE.
518
         */
519
        return DbProxy::handler()->lastInsertId($self->sequence);
520
    }
521
522
    /**
523
     * Updates existing rows
524
     *
525
     * <code>
526
     *     Table::insert(['login' => 'Man', 'email' => '[email protected]'], ['id' => 42])
527
     * </code>
528
     *
529
     * @param  array $data  Column-value pairs.
530
     * @param  array $where An array of SQL WHERE clause(s)
531
     * @return integer The number of rows updated
532
     * @throws Exception\DbException
533
     */
534
    public static function update(array $data, array $where)
535
    {
536
        if (!sizeof($where)) {
537
            throw new DbException(
538
                "Method `Table::update()` can't update all records in table,\n".
539
                "please use `Db::query()` instead (of cause if you know what are you doing)"
540
            );
541
        }
542
543
        $self = static::getInstance();
544
545
        $data = static::filterColumns($data);
546
547
        $where = static::filterColumns($where);
548
549
        if (!sizeof($data) || !sizeof($where)) {
550
            throw new DbException(
551
                "Invalid field names of table `{$self->name}`. Please check use of `update()` method"
552
            );
553
        }
554
555
        $table = DbProxy::quoteIdentifier($self->name);
556
557
        $sql = "UPDATE $table"
558
            . " SET " . join(',', self::prepareStatement($data))
559
            . " WHERE " . join(' AND ', self::prepareStatement($where));
560
561
        return DbProxy::query($sql, array_merge(array_values($data), array_values($where)));
562
    }
563
564
    /**
565
     * Deletes existing rows
566
     *
567
     * <code>
568
     *     Table::delete(['login' => 'Man'])
569
     * </code>
570
     *
571
     * @param  array $where An array of SQL WHERE clause(s)
572
     * @return integer The number of rows deleted
573
     * @throws Exception\DbException
574
     */
575
    public static function delete(array $where)
576
    {
577
        if (!sizeof($where)) {
578
            throw new DbException(
579
                "Method `Table::delete()` can't delete all records in table,\n".
580
                "please use `Db::query()` instead (of cause if you know what are you doing)"
581
            );
582
        }
583
584
        $self = static::getInstance();
585
586
        $where = static::filterColumns($where);
587
588
        if (!sizeof($where)) {
589
            throw new DbException(
590
                "Invalid field names of table `{$self->name}`. Please check use of `delete()` method"
591
            );
592
        }
593
594
        $table = DbProxy::quoteIdentifier($self->name);
595
596
        $sql = "DELETE FROM $table"
597
            . " WHERE " . join(' AND ', self::prepareStatement($where));
598
        return DbProxy::query($sql, array_values($where));
599
    }
600
601
    /**
602
     * Setup relation "one to one" or "one to many"
603
     *
604
     * @param  string $key
605
     * @param  string $model
606
     * @param  string $foreign
607
     * @return void
608
     */
609
    public function linkTo($key, $model, $foreign)
610
    {
611
        Relations::setRelation($this->model, $key, $model, $foreign);
612
    }
613
614
    /**
615
     * Setup relation "many to many"
616
     * [table1-key] [table1_key-table2-table3_key] [table3-key]
617
     *
618
     * @param  string $model
619
     * @param  string $link
620
     * @return void
621
     */
622
    public function linkToMany($model, $link)
623
    {
624
        Relations::setRelations($this->model, $model, [$link]);
625
    }
626
}
627