Completed
Pull Request — master (#451)
by Anton
11:31
created

Table::setSelectQuery()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 1
dl 0
loc 5
ccs 0
cts 3
cp 0
crap 2
rs 9.4285
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\Common\Instance;
14
use Bluz\Db\Exception\DbException;
15
use Bluz\Db\Exception\InvalidPrimaryKeyException;
16
use Bluz\Db\Traits\TableRelations;
17
use Bluz\Proxy\Cache;
18
use Bluz\Proxy\Db as DbProxy;
19
20
/**
21
 * Table
22
 *
23
 * Example of Users\Table
24
 * <code>
25
 *     namespace Application\Users;
26
 *     class Table extends \Bluz\Db\Table
27
 *     {
28
 *        protected $table = 'users';
29
 *        protected $primary = ['id'];
30
 *     }
31
 *
32
 *     $userRows = \Application\Users\Table::find(1,2,3,4,5);
33
 *     foreach ($userRows as $userRow) {
34
 *        $userRow -> description = 'In first 5';
35
 *        $userRow -> save();
36
 *     }
37
 * </code>
38
 *
39
 * @package  Bluz\Db
40
 * @author   Anton Shevchuk
41
 * @link     https://github.com/bluzphp/framework/wiki/Db-Table
42
 */
43
abstract class Table implements TableInterface
44
{
45
    use Instance;
46
    use TableRelations;
47
48
    /**
49
     * @var string the table name
50
     */
51
    protected $name;
52
53
    /**
54
     * @var string the model name
55
     */
56
    protected $model;
57
58
    /**
59
     * @var array table meta
60
     */
61
    protected $meta = [];
62
63
    /**
64
     * @var string default SQL query for select
65
     */
66
    protected $select = '';
67
68
    /**
69
     * @var array the primary key column or columns (only as array).
70
     */
71
    protected $primary;
72
73
    /**
74
     * @var string the sequence name, required for PostgreSQL
75
     */
76
    protected $sequence;
77
78
    /**
79
     * @var string row class name
80
     */
81 2
    protected $rowClass;
82
83 2
    /**
84 2
     * Create and initialize Table instance
85
     */
86
    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...
87 2
    {
88 2
        $tableClass = static::class;
89
        $namespace = class_namespace($tableClass);
90
91
        // autodetect model name
92 2
        if (!$this->model) {
93
            $this->model = substr($namespace, strrpos($namespace, '\\') + 1);
94
        }
95
96
        // autodetect table name - camelCase to uppercase
97
        if (!$this->name) {
98 2
            $table = preg_replace('/(?<=\\w)(?=[A-Z])/', '_$1', $this->model);
99 1
            $this->name = strtolower($table);
100
        }
101
102
        // autodetect row class
103 2
        if (!$this->rowClass) {
104 2
            $this->rowClass = $namespace . '\\Row';
105 2
        }
106
107
        // setup default select query
108 2
        if (empty($this->select)) {
109
            $this->select = 'SELECT * ' .
110 2
                'FROM ' . DbProxy::quoteIdentifier($this->name);
111 2
        }
112
113
        Relations::addClassMap($this->model, $tableClass);
114
115
        $this->init();
116
    }
117 2
118
    /**
119 2
     * Initialization hook.
120
     * Subclasses may override this method
121
     */
122
    public function init()
123
    {
124
    }
125
126 47
    /**
127
     * Get primary key(s)
128 47
     *
129 47
     * @return array
130 3
     * @throws InvalidPrimaryKeyException if primary key was not set or has wrong format
131
     */
132
    public function getPrimaryKey() : array
133 47
    {
134
        if (!is_array($this->primary)) {
135
            throw new InvalidPrimaryKeyException('The primary key must be set as an array');
136
        }
137
        return $this->primary;
138
    }
139
140
    /**
141
     * Get table name
142
     *
143
     * @return string
144
     */
145
    public function getName() : string
146
    {
147
        return $this->name;
148
    }
149
150
    /**
151
     * Get model name
152
     *
153
     * @return string
154
     */
155
    public function getModel() : string
156
    {
157
        return $this->model;
158
    }
159
160
    /**
161
     * Return information about table columns
162
     *
163
     * @return array
164
     */
165 39
    public static function getMeta()
166
    {
167 39
        $self = static::getInstance();
168 1
        if (empty($self->meta)) {
169
            $cacheKey = "db.table.{$self->name}";
170 38
            $meta = Cache::get($cacheKey);
171
            if (!$meta) {
172
                $schema = DbProxy::getOption('connect', 'name');
173
174
                $meta = DbProxy::fetchUniqueGroup(
175
                    '
176
                    SELECT COLUMN_NAME, DATA_TYPE, COLUMN_DEFAULT, COLUMN_KEY
177
                    FROM INFORMATION_SCHEMA.COLUMNS
178 3
                    WHERE TABLE_SCHEMA = ?
179
                      AND TABLE_NAME = ?',
180 3
                    [$schema, $self->getName()]
181
                );
182
                Cache::set($cacheKey, $meta, Cache::TTL_NO_EXPIRY, ['system', 'db']);
183
            }
184
            $self->meta = $meta;
0 ignored issues
show
Documentation Bug introduced by
It seems like $meta of type * is incompatible with the declared type array of property $meta.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
185
        }
186
        return $self->meta;
187
    }
188 2
189
    /**
190 2
     * Return names of table columns
191
     *
192
     * @return array
193
     */
194
    public static function getColumns()
195
    {
196
        $self = static::getInstance();
197
        return array_keys($self::getMeta());
198 5
    }
199
200 5
    /**
201 5
     * Filter columns for insert/update queries by table columns definition
202 1
     *
203 1
     * @param  array $data
204 1
     *
205 1
     * @return array
206
     */
207 1
    protected static function filterColumns($data) : array
208 1
    {
209
        return array_intersect_key($data, array_flip(static::getColumns()));
210
    }
211
212
    /**
213 1
     * Fetching rows by SQL query
214
     *
215 1
     * @param  string $sql    SQL query with placeholders
216
     * @param  array  $params Params for query placeholders
217 1
     *
218
     * @return RowInterface[] of rows results in FETCH_CLASS mode
219 5
     */
220
    protected static function fetch($sql, $params = []) : array
221
    {
222
        $self = static::getInstance();
223
        return DbProxy::fetchObjects($sql, $params, $self->rowClass);
224
    }
225
226
    /**
227 4
     * {@inheritdoc}
228
     *
229 4
     * @throws DbException
230 4
     * @throws InvalidPrimaryKeyException if wrong count of values passed
231
     * @throws \InvalidArgumentException
232
     */
233
    public static function find(...$keys) : array
234
    {
235
        $keyNames = array_values(static::getInstance()->getPrimaryKey());
236
        $whereList = [];
237
        foreach ($keys as $keyValues) {
238
            $keyValues = (array)$keyValues;
239
            if (count($keyValues) !== count($keyNames)) {
240 3
                throw new InvalidPrimaryKeyException(
241
                    "Invalid columns for the primary key.\n" .
242 3
                    "Please check " . static::class . " initialization or usage.\n" .
243
                    "Settings described at https://github.com/bluzphp/framework/wiki/Db-Table"
244
                );
245
            }
246
247
            if (array_keys($keyValues)[0] === 0) {
248
                // for numerical array
249
                $whereList[] = array_combine($keyNames, $keyValues);
250
            } else {
251
                // for assoc array
252
                $whereList[] = $keyValues;
253 8
            }
254
        }
255 8
        return static::findWhere(...$whereList);
256 8
    }
257
258
    /**
259
     * {@inheritdoc}
260
     *
261
     * @throws \InvalidArgumentException
262
     * @throws Exception\DbException
263
     */
264
    public static function findWhere(...$where) : array
265
    {
266
        $self = static::getInstance();
267
268
        $whereParams = [];
269
270
        if (count($where) === 2 && is_string($where[0])) {
271
            $whereClause = $where[0];
272
            $whereParams = (array)$where[1];
273
        } elseif (count($where)) {
274
            $whereOrTerms = [];
275
            foreach ($where as $keyValueSets) {
276
                $whereAndTerms = [];
277
                foreach ($keyValueSets as $keyName => $keyValue) {
278
                    if (is_array($keyValue)) {
279
                        $keyValue = array_map(
280
                            function ($value) use ($self) {
281
                                return DbProxy::quote($value);
282
                            },
283
                            $keyValue
284
                        );
285
                        $keyValue = implode(',', $keyValue);
286
                        $whereAndTerms[] = $self->name . '.' . $keyName . ' IN (' . $keyValue . ')';
287
                    } elseif (is_null($keyValue)) {
288
                        $whereAndTerms[] = $self->name . '.' . $keyName . ' IS NULL';
289
                    } else {
290
                        $whereAndTerms[] = $self->name . '.' . $keyName . ' = ?';
291
                        $whereParams[] = $keyValue;
292
                    }
293
                    if (!is_scalar($keyValue) && !is_null($keyValue)) {
294
                        throw new \InvalidArgumentException(
295
                            "Wrong arguments of method 'findWhere'.\n" .
296
                            "Please use syntax described at https://github.com/bluzphp/framework/wiki/Db-Table"
297
                        );
298
                    }
299
                }
300
                $whereOrTerms[] = '(' . implode(' AND ', $whereAndTerms) . ')';
301 10
            }
302
            $whereClause = '(' . implode(' OR ', $whereOrTerms) . ')';
303 10
        } else {
304 10
            throw new DbException(
305 10
                "Method `Table::findWhere()` can't return all records from table"
306 10
            );
307 10
        }
308 2
309
        return self::fetch($self->select . ' WHERE ' . $whereClause, $whereParams);
310 2
    }
311 2
312
    /**
313
     * {@inheritdoc}
314
     *
315 8
     * @throws DbException
316
     * @throws \InvalidArgumentException
317
     * @throws InvalidPrimaryKeyException
318
     */
319
    public static function findRow($primaryKey) : ?RowInterface
320
    {
321
        $result = static::find($primaryKey);
322
        return current($result) ?: null;
323 8
    }
324
325 7
    /**
326
     * {@inheritdoc}
327
     *
328 8
     * @throws DbException
329
     * @throws \InvalidArgumentException
330
     */
331 8
    public static function findRowWhere(array $whereList) : ?RowInterface
332
    {
333
        $result = static::findWhere($whereList);
334
        return current($result) ?: null;
335
    }
336
337
    /**
338
     * Prepare array for WHERE or SET statements
339
     *
340
     * @param  array $where
341 8
     *
342
     * @return array
343 8
     * @throws \Bluz\Common\Exception\ConfigurationException
344
     */
345
    private static function prepareStatement(array $where) : array
346 8
    {
347 8
        $keys = array_keys($where);
348
        foreach ($keys as &$key) {
349
            $key = DbProxy::quoteIdentifier($key) . ' = ?';
350
        }
351
        return $keys;
352
    }
353
354
    /**
355
     * Prepare Db\Query\Select for current table:
356
     *  - predefine "select" section as "*" from current table
357
     *  - predefine "from" section as current table name and first letter as alias
358
     *  - predefine fetch type
359
     *
360
     * <code>
361
     *     // use default select "*"
362
     *     $select = Users\Table::select();
363
     *     $arrUsers = $select->where('u.id = ?', $id)
364
     *         ->execute();
365
     *
366
     *     // setup custom select "u.id, u.login"
367 8
     *     $select = Users\Table::select();
368
     *     $arrUsers = $select->select('u.id, u.login')
369 8
     *         ->where('u.id = ?', $id)
370
     *         ->execute();
371 8
     * </code>
372
     *
373 8
     * @return Query\Select
374
     */
375
    public static function select() : Query\Select
376 8
    {
377 8
        $self = static::getInstance();
378 8
379 8
        $select = new Query\Select();
380 8
        $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...
381 8
            ->from($self->name, $self->name)
382
            ->setFetchType($self->rowClass);
383
384
        return $select;
385
    }
386
387
    /**
388
     * {@inheritdoc}
389
     */
390 8
    public static function create(array $data = []) : RowInterface
391
    {
392
        $rowClass = static::getInstance()->rowClass;
393 8
        /** @var Row $row */
394 8
        $row = new $rowClass($data);
395
        $row->setTable(static::getInstance());
396 8
        return $row;
397
    }
398
399 8
    /**
400
     * {@inheritdoc}
401
     *
402
     * @throws \Bluz\Common\Exception\ConfigurationException
403 8
     * @throws Exception\DbException
404
     */
405 8
    public static function insert(array $data)
406
    {
407
        $self = static::getInstance();
408
409
        $data = static::filterColumns($data);
410
411
        if (!count($data)) {
412
            throw new DbException(
413 8
                "Invalid field names of table `{$self->name}`. Please check use of `insert()` method"
414
            );
415
        }
416
417
        $table = DbProxy::quoteIdentifier($self->name);
418
419
        $sql = "INSERT INTO $table SET " . implode(',', self::prepareStatement($data));
420
        $result = DbProxy::query($sql, array_values($data));
421
        if (!$result) {
422
            return null;
423
        }
424
425
        /**
426
         * If a sequence name was not specified for the name parameter, PDO::lastInsertId()
427
         * returns a string representing the row ID of the last row that was inserted into the database.
428
         *
429
         * If a sequence name was specified for the name parameter, PDO::lastInsertId()
430
         * returns a string representing the last value retrieved from the specified sequence object.
431
         *
432
         * If the PDO driver does not support this capability, PDO::lastInsertId() triggers an IM001 SQLSTATE.
433
         */
434
        return DbProxy::handler()->lastInsertId($self->sequence);
435
    }
436
437 3
    /**
438
     * {@inheritdoc}
439 3
     *
440 3
     * @throws \Bluz\Common\Exception\ConfigurationException
441 3
     * @throws Exception\DbException
442
     */
443 3
    public static function update(array $data, array $where) : int
444
    {
445
        $self = static::getInstance();
446
447
        $data = static::filterColumns($data);
448
449
        if (!count($data)) {
450
            throw new DbException(
451
                "Invalid field names of table `{$self->name}`. Please check use of `update()` method"
452
            );
453
        }
454
455
        $where = static::filterColumns($where);
456
457
        if (!count($where)) {
458
            throw new DbException(
459
                "Method `Table::update()` can't update all records in the table `{$self->name}`,\n" .
460
                "please use `Db::query()` instead (of cause if you know what are you doing)"
461
            );
462
        }
463
464
        $table = DbProxy::quoteIdentifier($self->name);
465
466
        $sql = "UPDATE $table SET " . implode(',', self::prepareStatement($data))
467 2
            . " WHERE " . implode(' AND ', self::prepareStatement($where));
468
469 2
        return DbProxy::query($sql, array_merge(array_values($data), array_values($where)));
470
    }
471 2
472 2
    /**
473 2
     * {@inheritdoc}
474 2
     *
475
     * @throws \Bluz\Common\Exception\ConfigurationException
476 2
     * @throws \Bluz\Db\Exception\DbException
477
     */
478
    public static function delete(array $where) : int
479
    {
480
        $self = static::getInstance();
481
482
        if (!count($where)) {
483
            throw new DbException(
484
                "Method `Table::delete()` can't delete all records in the table `{$self->name}`,\n" .
485
                "please use `Db::query()` instead (of cause if you know what are you doing)"
486 2
            );
487
        }
488 2
489
490 2
        $where = static::filterColumns($where);
491 2
492 2
        if (!count($where)) {
493
            throw new DbException(
494
                "Invalid field names of table `{$self->name}`. Please check use of `delete()` method"
495
            );
496
        }
497
498
        $table = DbProxy::quoteIdentifier($self->name);
499
500
        $sql = "DELETE FROM $table WHERE " . implode(' AND ', self::prepareStatement($where));
501
        return DbProxy::query($sql, array_values($where));
502
    }
503
}
504