Passed
Push — develop ( ec7ed8...af61f1 )
by Anton
04:52
created

Table::fetch()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

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