Completed
Push — master ( 929e4f...acab5f )
by Anton
07:41
created

Table::update()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 28

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 3.3332

Importance

Changes 0
Metric Value
cc 3
nc 3
nop 2
dl 0
loc 28
ccs 10
cts 15
cp 0.6667
crap 3.3332
rs 9.472
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 ' . DbProxy::quoteIdentifier($this->name) . '.* ' .
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) {
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
     */
354 6
    private static function prepareStatement(array $where): array
355
    {
356 6
        $keys = array_keys($where);
357 6
        foreach ($keys as &$key) {
358 6
            $key = DbProxy::quoteIdentifier($key) . ' = ?';
359
        }
360 6
        return $keys;
361
    }
362
363
    /**
364
     * Prepare Db\Query\Select for current table:
365
     *  - predefine "select" section as "*" from current table
366
     *  - predefine "from" section as current table name and first letter as alias
367
     *  - predefine fetch type
368
     *
369
     * <code>
370
     *     // use default select "*"
371
     *     $select = Users\Table::select();
372
     *     $arrUsers = $select->where('u.id = ?', $id)
373
     *         ->execute();
374
     *
375
     *     // setup custom select "u.id, u.login"
376
     *     $select = Users\Table::select();
377
     *     $arrUsers = $select->select('u.id, u.login')
378
     *         ->where('u.id = ?', $id)
379
     *         ->execute();
380
     * </code>
381
     *
382
     * @return Query\Select
383
     */
384 2
    public static function select(): Query\Select
385
    {
386 2
        $self = static::getInstance();
387
388 2
        $select = new Query\Select();
389 2
        $select->select(DbProxy::quoteIdentifier($self->name) . '.*')
390 2
            ->from($self->name, $self->name)
391 2
            ->setFetchType($self->rowClass);
392
393 2
        return $select;
394
    }
395
396
    /**
397
     * {@inheritdoc}
398
     */
399 2
    public static function create(array $data = []): RowInterface
400
    {
401 2
        $rowClass = static::getInstance()->rowClass;
402
        /** @var Row $row */
403 2
        $row = new $rowClass($data);
404 2
        $row->setTable(static::getInstance());
405 2
        return $row;
406
    }
407
408
    /**
409
     * {@inheritdoc}
410
     *
411
     * @throws Exception\DbException
412
     */
413 2
    public static function insert(array $data)
414
    {
415 2
        $self = static::getInstance();
416
417 2
        $data = static::filterColumns($data);
418
419 2
        if (!\count($data)) {
420
            throw new DbException(
421
                "Invalid field names of table `{$self->name}`. Please check use of `insert()` method"
422
            );
423
        }
424
425 2
        $table = DbProxy::quoteIdentifier($self->name);
426
427 2
        $sql = "INSERT INTO $table SET " . implode(',', self::prepareStatement($data));
428 2
        $result = DbProxy::query($sql, array_values($data));
429 2
        if (!$result) {
430
            return null;
431
        }
432
433
        /**
434
         * If a sequence name was not specified for the name parameter, PDO::lastInsertId()
435
         * returns a string representing the row ID of the last row that was inserted into the database.
436
         *
437
         * If a sequence name was specified for the name parameter, PDO::lastInsertId()
438
         * returns a string representing the last value retrieved from the specified sequence object.
439
         *
440
         * If the PDO driver does not support this capability, PDO::lastInsertId() triggers an IM001 SQLSTATE.
441
         */
442 2
        return DbProxy::handler()->lastInsertId($self->sequence);
443
    }
444
445
    /**
446
     * {@inheritdoc}
447
     *
448
     * @throws Exception\DbException
449
     */
450 2
    public static function update(array $data, array $where): int
451
    {
452 2
        $self = static::getInstance();
453
454 2
        $data = static::filterColumns($data);
455
456 2
        if (!\count($data)) {
457
            throw new DbException(
458
                "Invalid field names of table `{$self->name}`. Please check use of `update()` method"
459
            );
460
        }
461
462 2
        $where = static::filterColumns($where);
463
464 2
        if (!\count($where)) {
465
            throw new DbException(
466
                "Method `Table::update()` can't update all records in the table `{$self->name}`,\n" .
467
                "please use `Db::query()` instead (of cause if you know what are you doing)"
468
            );
469
        }
470
471 2
        $table = DbProxy::quoteIdentifier($self->name);
472
473 2
        $sql = "UPDATE $table SET " . implode(',', self::prepareStatement($data))
474 2
            . " WHERE " . implode(' AND ', self::prepareStatement($where));
475
476 2
        return DbProxy::query($sql, array_merge(array_values($data), array_values($where)));
477
    }
478
479
    /**
480
     * {@inheritdoc}
481
     *
482
     * @throws \Bluz\Db\Exception\DbException
483
     */
484 2
    public static function delete(array $where): int
485
    {
486 2
        $self = static::getInstance();
487
488 2
        if (!\count($where)) {
489
            throw new DbException(
490
                "Method `Table::delete()` can't delete all records in the table `{$self->name}`,\n" .
491
                "please use `Db::query()` instead (of cause if you know what are you doing)"
492
            );
493
        }
494
495
496 2
        $where = static::filterColumns($where);
497
498 2
        if (!\count($where)) {
499
            throw new DbException(
500
                "Invalid field names of table `{$self->name}`. Please check use of `delete()` method"
501
            );
502
        }
503
504 2
        $table = DbProxy::quoteIdentifier($self->name);
505
506 2
        $sql = "DELETE FROM $table WHERE " . implode(' AND ', self::prepareStatement($where));
507 2
        return DbProxy::query($sql, array_values($where));
508
    }
509
}
510