Completed
Push — master ( ccf0eb...14b48b )
by Anton
10s
created

Table::getModel()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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