Completed
Push — master ( 6f2bb4...6400d0 )
by Anton
11s
created

Table::getMeta()   B

Complexity

Conditions 3
Paths 3

Size

Total Lines 27
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 13
nc 3
nop 0
dl 0
loc 27
ccs 13
cts 13
cp 1
crap 3
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\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
    protected $rowClass;
82
83
    /**
84
     * Create and initialize Table instance
85
     */
86 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...
87
    {
88 2
        $tableClass = static::class;
89 2
        $namespace = class_namespace($tableClass);
90
91
        // autodetect model name
92 2
        if (!$this->model) {
93 2
            $this->model = substr($namespace, strrpos($namespace, '\\') + 1);
94
        }
95
96
        // autodetect table name - camelCase to uppercase
97 2
        if (!$this->name) {
98
            $table = preg_replace('/(?<=\\w)(?=[A-Z])/', '_$1', $this->model);
99
            $this->name = strtolower($table);
100
        }
101
102
        // autodetect row class
103 2
        if (!$this->rowClass) {
104 1
            $this->rowClass = $namespace . '\\Row';
105
        }
106
107
        // setup default select query
108 2
        if (empty($this->select)) {
109 2
            $this->select = 'SELECT * ' .
110 2
                'FROM ' . DbProxy::quoteIdentifier($this->name);
111
        }
112
113 2
        Relations::addClassMap($this->model, $tableClass);
114
115 2
        $this->init();
116 2
    }
117
118
    /**
119
     * Initialization hook.
120
     * Subclasses may override this method
121
     *
122
     * @return void
123
     */
124 2
    public function init() : void
125
    {
126 2
    }
127
128
    /**
129
     * Get primary key(s)
130
     *
131
     * @return array
132
     * @throws InvalidPrimaryKeyException if primary key was not set or has wrong format
133
     */
134 39
    public function getPrimaryKey() : array
135
    {
136 39
        if (!is_array($this->primary)) {
137 1
            throw new InvalidPrimaryKeyException('The primary key must be set as an array');
138
        }
139 38
        return $this->primary;
140
    }
141
142
    /**
143
     * Get table name
144
     *
145
     * @return string
146
     */
147 3
    public function getName() : string
148
    {
149 3
        return $this->name;
150
    }
151
152
    /**
153
     * Get model name
154
     *
155
     * @return string
156
     */
157 2
    public function getModel() : string
158
    {
159 2
        return $this->model;
160
    }
161
162
    /**
163
     * Return information about table columns
164
     *
165
     * @return array
166
     */
167 8
    public static function getMeta() : array
168
    {
169 8
        $self = static::getInstance();
170 8
        if (empty($self->meta)) {
171 1
            $cacheKey = "db.table.{$self->name}";
172 1
            $meta = Cache::get($cacheKey);
173 1
            if (!$meta) {
174 1
                $schema = DbProxy::getOption('connect', 'name');
175
176 1
                $meta = DbProxy::fetchUniqueGroup(
177 1
                    '
178
                    SELECT 
179
                      COLUMN_NAME AS `name`,
180
                      DATA_TYPE AS `type`,
181
                      COLUMN_DEFAULT AS `default`,
182
                      COLUMN_KEY AS `key`
183
                    FROM INFORMATION_SCHEMA.COLUMNS
184
                    WHERE TABLE_SCHEMA = ?
185
                      AND TABLE_NAME = ?',
186 1
                    [$schema, $self->getName()]
187
                );
188 1
                Cache::set($cacheKey, $meta, Cache::TTL_NO_EXPIRY, ['system', 'db']);
189
            }
190 1
            $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...
191
        }
192 8
        return $self->meta;
193
    }
194
195
    /**
196
     * Return names of table columns
197
     *
198
     * @return array
199
     */
200 7
    public static function getColumns() : array
201
    {
202 7
        $self = static::getInstance();
203 7
        return array_keys($self::getMeta());
204
    }
205
206
    /**
207
     * Filter columns for insert/update queries by table columns definition
208
     *
209
     * @param  array $data
210
     *
211
     * @return array
212
     */
213 6
    public static function filterColumns($data) : array
214
    {
215 6
        return array_intersect_key($data, array_flip(static::getColumns()));
216
    }
217
218
    /**
219
     * Fetching rows by SQL query
220
     *
221
     * @param  string $sql    SQL query with placeholders
222
     * @param  array  $params Params for query placeholders
223
     *
224
     * @return RowInterface[] of rows results in FETCH_CLASS mode
225
     */
226 10
    protected static function fetch($sql, $params = []) : array
227
    {
228 10
        $self = static::getInstance();
229 10
        return DbProxy::fetchObjects($sql, $params, $self->rowClass);
230
    }
231
232
    /**
233
     * {@inheritdoc}
234
     *
235
     * @throws DbException
236
     * @throws InvalidPrimaryKeyException if wrong count of values passed
237
     * @throws \InvalidArgumentException
238
     */
239 10
    public static function find(...$keys) : array
240
    {
241 10
        $keyNames = array_values(static::getInstance()->getPrimaryKey());
242 10
        $whereList = [];
243
244 10
        foreach ($keys as $keyValues) {
245 10
            $keyValues = (array)$keyValues;
246 10
            if (count($keyValues) !== count($keyNames)) {
247 2
                throw new InvalidPrimaryKeyException(
248
                    "Invalid columns for the primary key.\n" .
249 2
                    "Please check " . static::class . " initialization or usage.\n" .
250 2
                    "Settings described at https://github.com/bluzphp/framework/wiki/Db-Table"
251
                );
252
            }
253
254 8
            if (array_keys($keyValues)[0] === 0) {
255
                // for numerical array
256 7
                $whereList[] = array_combine($keyNames, $keyValues);
257
            } else {
258
                // for assoc array
259 8
                $whereList[] = $keyValues;
260
            }
261
        }
262 8
        return static::findWhere(...$whereList);
263
    }
264
265
    /**
266
     * {@inheritdoc}
267
     *
268
     * @throws \InvalidArgumentException
269
     * @throws Exception\DbException
270
     */
271 10
    public static function findWhere(...$where) : array
272
    {
273 10
        $self = static::getInstance();
274
275 10
        $whereParams = [];
276
277 10
        if (count($where) === 2 && is_string($where[0])) {
278
            $whereClause = $where[0];
279
            $whereParams = (array)$where[1];
280 10
        } elseif (count($where)) {
281 10
            $whereOrTerms = [];
282 10
            foreach ($where as $keyValueSets) {
283 10
                $whereAndTerms = [];
284 10
                foreach ($keyValueSets as $keyName => $keyValue) {
285 10
                    if (is_array($keyValue)) {
286
                        $keyValue = array_map(
287
                            function ($value) use ($self) {
288
                                return DbProxy::quote($value);
289
                            },
290
                            $keyValue
291
                        );
292
                        $keyValue = implode(',', $keyValue);
293
                        $whereAndTerms[] = $self->name . '.' . $keyName . ' IN (' . $keyValue . ')';
294 10
                    } elseif (null === $keyValue) {
295
                        $whereAndTerms[] = $self->name . '.' . $keyName . ' IS NULL';
296 10
                    } elseif ('%' === $keyValue{0} || '%' === $keyValue{-1}) {
297
                        $whereAndTerms[] = $self->name . '.' . $keyName . ' LIKE ?';
298
                        $whereParams[] = $keyValue;
299
                    } else {
300 10
                        $whereAndTerms[] = $self->name . '.' . $keyName . ' = ?';
301 10
                        $whereParams[] = $keyValue;
302
                    }
303 10
                    if (!is_scalar($keyValue) && !is_null($keyValue)) {
304
                        throw new \InvalidArgumentException(
305
                            "Wrong arguments of method 'findWhere'.\n" .
306 10
                            "Please use syntax described at https://github.com/bluzphp/framework/wiki/Db-Table"
307
                        );
308
                    }
309
                }
310 10
                $whereOrTerms[] = '(' . implode(' AND ', $whereAndTerms) . ')';
311
            }
312 10
            $whereClause = '(' . implode(' OR ', $whereOrTerms) . ')';
313
        } else {
314
            throw new DbException(
315
                "Method `Table::findWhere()` can't return all records from table"
316
            );
317
        }
318
319 10
        return self::fetch($self->select . ' WHERE ' . $whereClause, $whereParams);
320
    }
321
322
    /**
323
     * {@inheritdoc}
324
     *
325
     * @throws DbException
326
     * @throws \InvalidArgumentException
327
     * @throws InvalidPrimaryKeyException
328
     */
329 8
    public static function findRow($primaryKey) : ?RowInterface
330
    {
331 8
        $result = static::find($primaryKey);
332 8
        return current($result) ?: null;
333
    }
334
335
    /**
336
     * {@inheritdoc}
337
     *
338
     * @throws DbException
339
     * @throws \InvalidArgumentException
340
     */
341 2
    public static function findRowWhere(array $whereList) : ?RowInterface
342
    {
343 2
        $result = static::findWhere($whereList);
344 2
        return current($result) ?: null;
345
    }
346
347
    /**
348
     * Prepare array for WHERE or SET statements
349
     *
350
     * @param  array $where
351
     *
352
     * @return array
353
     * @throws \Bluz\Common\Exception\ConfigurationException
354
     */
355 6
    private static function prepareStatement(array $where) : array
356
    {
357 6
        $keys = array_keys($where);
358 6
        foreach ($keys as &$key) {
359 6
            $key = DbProxy::quoteIdentifier($key) . ' = ?';
360
        }
361 6
        return $keys;
362
    }
363
364
    /**
365
     * Prepare Db\Query\Select for current table:
366
     *  - predefine "select" section as "*" from current table
367
     *  - predefine "from" section as current table name and first letter as alias
368
     *  - predefine fetch type
369
     *
370
     * <code>
371
     *     // use default select "*"
372
     *     $select = Users\Table::select();
373
     *     $arrUsers = $select->where('u.id = ?', $id)
374
     *         ->execute();
375
     *
376
     *     // setup custom select "u.id, u.login"
377
     *     $select = Users\Table::select();
378
     *     $arrUsers = $select->select('u.id, u.login')
379
     *         ->where('u.id = ?', $id)
380
     *         ->execute();
381
     * </code>
382
     *
383
     * @return Query\Select
384
     */
385 2
    public static function select() : Query\Select
386
    {
387 2
        $self = static::getInstance();
388
389 2
        $select = new Query\Select();
390 2
        $select->select($self->name . '.*')
391 2
            ->from($self->name, $self->name)
392 2
            ->setFetchType($self->rowClass);
393
394 2
        return $select;
395
    }
396
397
    /**
398
     * {@inheritdoc}
399
     */
400 2
    public static function create(array $data = []) : RowInterface
401
    {
402 2
        $rowClass = static::getInstance()->rowClass;
403
        /** @var Row $row */
404 2
        $row = new $rowClass($data);
405 2
        $row->setTable(static::getInstance());
406 2
        return $row;
407
    }
408
409
    /**
410
     * {@inheritdoc}
411
     *
412
     * @throws \Bluz\Common\Exception\ConfigurationException
413
     * @throws Exception\DbException
414
     */
415 2
    public static function insert(array $data)
416
    {
417 2
        $self = static::getInstance();
418
419 2
        $data = static::filterColumns($data);
420
421 2
        if (!count($data)) {
422
            throw new DbException(
423
                "Invalid field names of table `{$self->name}`. Please check use of `insert()` method"
424
            );
425
        }
426
427 2
        $table = DbProxy::quoteIdentifier($self->name);
428
429 2
        $sql = "INSERT INTO $table SET " . implode(',', self::prepareStatement($data));
430 2
        $result = DbProxy::query($sql, array_values($data));
431 2
        if (!$result) {
432
            return null;
433
        }
434
435
        /**
436
         * If a sequence name was not specified for the name parameter, PDO::lastInsertId()
437
         * returns a string representing the row ID of the last row that was inserted into the database.
438
         *
439
         * If a sequence name was specified for the name parameter, PDO::lastInsertId()
440
         * returns a string representing the last value retrieved from the specified sequence object.
441
         *
442
         * If the PDO driver does not support this capability, PDO::lastInsertId() triggers an IM001 SQLSTATE.
443
         */
444 2
        return DbProxy::handler()->lastInsertId($self->sequence);
445
    }
446
447
    /**
448
     * {@inheritdoc}
449
     *
450
     * @throws \Bluz\Common\Exception\ConfigurationException
451
     * @throws Exception\DbException
452
     */
453 2
    public static function update(array $data, array $where) : int
454
    {
455 2
        $self = static::getInstance();
456
457 2
        $data = static::filterColumns($data);
458
459 2
        if (!count($data)) {
460
            throw new DbException(
461
                "Invalid field names of table `{$self->name}`. Please check use of `update()` method"
462
            );
463
        }
464
465 2
        $where = static::filterColumns($where);
466
467 2
        if (!count($where)) {
468
            throw new DbException(
469
                "Method `Table::update()` can't update all records in the table `{$self->name}`,\n" .
470
                "please use `Db::query()` instead (of cause if you know what are you doing)"
471
            );
472
        }
473
474 2
        $table = DbProxy::quoteIdentifier($self->name);
475
476 2
        $sql = "UPDATE $table SET " . implode(',', self::prepareStatement($data))
477 2
            . " WHERE " . implode(' AND ', self::prepareStatement($where));
478
479 2
        return DbProxy::query($sql, array_merge(array_values($data), array_values($where)));
480
    }
481
482
    /**
483
     * {@inheritdoc}
484
     *
485
     * @throws \Bluz\Common\Exception\ConfigurationException
486
     * @throws \Bluz\Db\Exception\DbException
487
     */
488 2
    public static function delete(array $where) : int
489
    {
490 2
        $self = static::getInstance();
491
492 2
        if (!count($where)) {
493
            throw new DbException(
494
                "Method `Table::delete()` can't delete all records in the table `{$self->name}`,\n" .
495
                "please use `Db::query()` instead (of cause if you know what are you doing)"
496
            );
497
        }
498
499
500 2
        $where = static::filterColumns($where);
501
502 2
        if (!count($where)) {
503
            throw new DbException(
504
                "Invalid field names of table `{$self->name}`. Please check use of `delete()` method"
505
            );
506
        }
507
508 2
        $table = DbProxy::quoteIdentifier($self->name);
509
510 2
        $sql = "DELETE FROM $table WHERE " . implode(' AND ', self::prepareStatement($where));
511 2
        return DbProxy::query($sql, array_values($where));
512
    }
513
}
514