Table::findWhere()   C
last analyzed

Complexity

Conditions 13
Paths 7

Size

Total Lines 50
Code Lines 36

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 19
CRAP Score 24.3267

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 13
eloc 36
c 1
b 0
f 0
nc 7
nop 1
dl 0
loc 50
ccs 19
cts 32
cp 0.5938
crap 24.3267
rs 6.6166

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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