Completed
Push — master ( ffb215...f806ab )
by Anton
11s
created

Table::linkToMany()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 2
dl 0
loc 4
ccs 0
cts 3
cp 0
crap 2
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
/**
10
 * @namespace
11
 */
12
namespace Bluz\Db;
13
14
use Bluz\Db\Exception\DbException;
15
use Bluz\Db\Exception\InvalidPrimaryKeyException;
16
use Bluz\Proxy\Cache;
17
use Bluz\Proxy\Db as DbProxy;
18
19
/**
20
 * Table
21
 *
22
 * Example of Users\Table
23
 * <code>
24
 *     namespace Application\Users;
25
 *     class Table extends \Bluz\Db\Table
26
 *     {
27
 *        protected $table = 'users';
28
 *        protected $primary = array('id');
29
 *     }
30
 *
31
 *     $userRows = \Application\Users\Table::find(1,2,3,4,5);
32
 *     foreach ($userRows as $userRow) {
33
 *        $userRow -> description = 'In first 5';
34
 *        $userRow -> save();
35
 *     }
36
 * </code>
37
 *
38
 * @package  Bluz\Db
39
 * @author   Anton Shevchuk
40
 * @link     https://github.com/bluzphp/framework/wiki/Db-Table
41
 */
42
abstract class Table
43
{
44
    /**
45
     * @var string the table name
46
     */
47
    protected $table;
48
49
    /**
50
     * @var string the model name
51
     */
52
    protected $model;
53
54
    /**
55
     * @var array table columns
56
     */
57
    protected $columns = [];
58
59
    /**
60
     * @var string default SQL query for select
61
     */
62
    protected $select = "";
63
64
    /**
65
     * @var array the primary key column or columns (only as array).
66
     */
67
    protected $primary;
68
69
    /**
70
     * @var string the sequence name, required for PostgreSQL
71
     */
72
    protected $sequence;
73
74
    /**
75
     * @var string row class name
76
     */
77
    protected $rowClass;
78
79
    /**
80
     * Create and initialize Table instance
81
     */
82 1
    private function __construct()
83
    {
84 1
        $tableClass = static::class;
85
86
        // autodetect model name
87 1
        if (!$this->model) {
88 1
            $model = substr($tableClass, strpos($tableClass, '\\') + 1);
89 1
            $model = substr($model, 0, strpos($model, '\\', 2));
90 1
            $this->model = $model;
91
        }
92
93
        // autodetect table name - camelCase to uppercase
94 1
        if (!$this->table) {
95
            $table = preg_replace('/(?<=\\w)(?=[A-Z])/', "_$1", $this->model);
96
            $this->table = strtolower($table);
97
        }
98
99
        // autodetect row class
100 1
        if (!$this->rowClass) {
101 1
            $rowClass = substr($tableClass, 0, strrpos($tableClass, '\\', 1) + 1);
102 1
            $this->rowClass = $rowClass . 'Row';
103
        }
104
105
        // setup default select query
106 1
        if (empty($this->select)) {
107 1
            $this->select = "SELECT * ".
108 1
                "FROM " . DbProxy::quoteIdentifier($this->table);
109
        }
110
111 1
        Relations::addClassMap($this->model, $tableClass);
112
113 1
        $this->init();
114 1
    }
115
116
    /**
117
     * Initialization hook.
118
     * Subclasses may override this method
119
     */
120 1
    public function init()
121
    {
122 1
    }
123
124
    /**
125
     * Get Table instance
126
     *
127
     * @return static
128
     */
129 7
    public static function getInstance()
130
    {
131 7
        static $instance;
132 7
        if (null === $instance) {
133 2
            $instance = new static();
134
        }
135
136 7
        return $instance;
137
    }
138
139
    /**
140
     * Set select query
141
     *
142
     * @param  string $select SQL query
143
     * @return Table
144
     */
145
    public function setSelectQuery($select)
146
    {
147
        $this->select = $select;
148
        return $this;
149
    }
150
151
    /**
152
     * Get select query
153
     *
154
     * @return string
155
     */
156
    public function getSelectQuery()
157
    {
158
        return $this->select;
159
    }
160
161
    /**
162
     * Get primary key(s)
163
     *
164
     * @return array
165
     * @throws InvalidPrimaryKeyException if primary key was not set or has wrong format
166
     */
167 2
    public function getPrimaryKey()
168
    {
169 2
        if (!is_array($this->primary)) {
170 1
            throw new InvalidPrimaryKeyException("The primary key must be set as an array");
171
        }
172 1
        return $this->primary;
173
    }
174
175
    /**
176
     * Get table name
177
     *
178
     * @return string
179
     */
180
    public function getName()
181
    {
182
        return $this->table;
183
    }
184
185
    /**
186
     * Get model name
187
     *
188
     * @return string
189
     */
190 1
    public function getModel()
191
    {
192 1
        return $this->model;
193
    }
194
195
    /**
196
     * Return information about tables columns
197
     *
198
     * @return array
199
     */
200
    public function getColumns()
201
    {
202
        if (empty($this->columns)) {
203
            $columns = Cache::get('table:columns:'. $this->table);
204
            if (!$columns) {
205
                $connect = DbProxy::getOption('connect');
206
207
                $columns = DbProxy::fetchColumn(
208
                    '
209
                    SELECT COLUMN_NAME
210
                    FROM INFORMATION_SCHEMA.COLUMNS
211
                    WHERE TABLE_SCHEMA = ?
212
                      AND TABLE_NAME = ?',
213
                    [$connect['name'], $this->getName()]
214
                );
215
                Cache::set('table:columns:'. $this->table, $columns);
216
                Cache::addTag('table:columns:'. $this->table, 'db');
217
            }
218
            $this->columns = $columns;
219
        }
220
        return $this->columns;
221
    }
222
223
    /**
224
     * Filter columns for insert/update queries by table columns definition
225
     *
226
     * @param  array $data
227
     * @return array
228
     */
229
    public static function filterColumns($data)
230
    {
231
        $self = static::getInstance();
232
        return array_intersect_key($data, array_flip($self->getColumns()));
233
    }
234
235
    /**
236
     * Fetching rows by SQL query
237
     *
238
     * @param  string $sql    SQL query with placeholders
239
     * @param  array  $params Params for query placeholders
240
     * @return array of rows results in FETCH_CLASS mode
241
     */
242 1
    public static function fetch($sql, $params = array())
243
    {
244 1
        $self = static::getInstance();
245 1
        return DbProxy::fetchObjects($sql, $params, $self->rowClass);
246
    }
247
248
    /**
249
     * Fetch all rows from table
250
     * Be carefully with this method, can be very slow
251
     *
252
     * @return array of rows results in FETCH_CLASS mode
253
     */
254
    public static function fetchAll()
255
    {
256
        $self = static::getInstance();
257
        return DbProxy::fetchObjects($self->select, [], $self->rowClass);
258
    }
259
260
    /**
261
     * Fetches rows by primary key.  The argument specifies one or more primary
262
     * key value(s).  To find multiple rows by primary key, the argument must
263
     * be an array.
264
     *
265
     * This method accepts a variable number of arguments.  If the table has a
266
     * multi-column primary key, the number of arguments must be the same as
267
     * the number of columns in the primary key.  To find multiple rows in a
268
     * table with a multi-column primary key, each argument must be an array
269
     * with the same number of elements.
270
     *
271
     * The find() method always returns a array
272
     *
273
     * Row by primary key, return array
274
     *     Table::find(123);
275
     *
276
     * Row by compound primary key, return array
277
     *     Table::find([123, 'abc']);
278
     *
279
     * Multiple rows by primary key
280
     *     Table::find(123, 234, 345);
281
     *
282
     * Multiple rows by compound primary key
283
     *     Table::find([123, 'abc'], [234, 'def'], [345, 'ghi'])
284
     *
285
     * @param  mixed ...$keys The value(s) of the primary keys.
286
     * @return array
287
     * @throws InvalidPrimaryKeyException if wrong count of values passed
288
     */
289 3
    public static function find(...$keys)
290
    {
291 3
        $self = static::getInstance();
292
293 3
        $keyNames = array_values((array)$self->primary);
294 3
        $whereList = array();
295 3
        foreach ($keys as $keyValues) {
296 3
            $keyValues = (array)$keyValues;
297 3
            if (count($keyValues) < count($keyNames)) {
298 2
                throw new InvalidPrimaryKeyException(
299
                    "Too few columns for the primary key.\n" .
300 2
                    "Please check " . static::class . " initialization or usage.\n" .
301 2
                    "Settings described at https://github.com/bluzphp/framework/wiki/Db-Table"
302
                );
303
            }
304
305 1
            if (count($keyValues) > count($keyNames)) {
306
                throw new InvalidPrimaryKeyException(
307
                    "Too many columns for the primary key.\n" .
308
                    "Please check " . static::class . " initialization or usage.\n" .
309
                    "Settings described at https://github.com/bluzphp/framework/wiki/Db-Table"
310
                );
311
            }
312
313 1
            if (array_keys($keyValues)[0] === 0) {
314
                // for numerical array
315
                $whereList[] = array_combine($keyNames, $keyValues);
316
            } else {
317
                // for assoc array
318 1
                $whereList[] = $keyValues;
319
            }
320
        }
321 1
        return $self::findWhere(...$whereList);
322
    }
323
324
    /**
325
     * Find row by primary key
326
     *
327
     * @param  mixed $primaryKey
328
     * @return Row
329
     */
330 1
    public static function findRow($primaryKey)
331
    {
332 1
        if (!$primaryKey) {
333
            return null;
334
        }
335 1
        $result = static::getInstance()->find($primaryKey);
336 1
        return current($result);
337
    }
338
339
    /**
340
     * Find rows by WHERE
341
     *     // WHERE alias = 'foo'
342
     *     Table::findWhere(['alias'=>'foo']);
343
     *     // WHERE alias = 'foo' OR 'alias' = 'bar'
344
     *     Table::findWhere(['alias'=>'foo'], ['alias'=>'bar']);
345
     *     // WHERE (alias = 'foo' AND userId = 2) OR ('alias' = 'bar' AND userId = 4)
346
     *     Table::findWhere(['alias'=>'foo', 'userId'=> 2], ['alias'=>'foo', 'userId'=>4]);
347
     *     // WHERE alias IN ('foo', 'bar')
348
     *     Table::findWhere(['alias'=> ['foo', 'bar']]);
349
     *
350
     * @param  mixed ...$where
351
     * @return array
352
     * @throws \InvalidArgumentException
353
     * @throws Exception\DbException
354
     */
355 1
    public static function findWhere(...$where)
356
    {
357 1
        $self = static::getInstance();
358
359 1
        $whereClause = null;
360 1
        $whereParams = array();
361
362 1
        if (sizeof($where) == 2 && is_string($where[0])) {
363
            $whereClause = $where[0];
364
            $whereParams = (array)$where[1];
365 1
        } elseif (sizeof($where)) {
366 1
            $whereOrTerms = array();
367 1
            foreach ($where as $keyValueSets) {
368 1
                $whereAndTerms = array();
369 1
                foreach ($keyValueSets as $keyName => $keyValue) {
370 1
                    if (is_array($keyValue)) {
371
                        $keyValue = array_map(
372
                            function ($value) use ($self) {
373
                                return DbProxy::quote($value);
374
                            },
375
                            $keyValue
376
                        );
377
                        $keyValue = join(',', $keyValue);
378
                        $whereAndTerms[] = $self->table . '.' . $keyName . ' IN ('.$keyValue.')';
379 1
                    } elseif (is_null($keyValue)) {
380
                        $whereAndTerms[] = $self->table . '.' . $keyName . ' IS NULL';
381
                    } else {
382 1
                        $whereAndTerms[] = $self->table . '.' . $keyName . ' = ?';
383 1
                        $whereParams[] = $keyValue;
384
                    }
385 1
                    if (!is_scalar($keyValue) && !is_null($keyValue)) {
386
                        throw new \InvalidArgumentException(
387
                            "Wrong arguments of method 'findWhere'.\n" .
388 1
                            "Please use syntax described at https://github.com/bluzphp/framework/wiki/Db-Table"
389
                        );
390
                    }
391
                }
392 1
                $whereOrTerms[] = '(' . implode(' AND ', $whereAndTerms) . ')';
393
            }
394 1
            $whereClause = '(' . implode(' OR ', $whereOrTerms) . ')';
395
        } elseif (!sizeof($where)) {
396
            throw new DbException(
397
                "Method `Table::findWhere()` can't return all records from table,\n".
398
                "please use `Table::fetchAll()` instead"
399
            );
400
        }
401
402 1
        return $self->fetch($self->select . ' WHERE ' . $whereClause, $whereParams);
403
    }
404
405
    /**
406
     * Find row by where condition
407
     *
408
     * @param  array $whereList
409
     * @return Row
410
     */
411
    public static function findRowWhere($whereList)
412
    {
413
        $result = static::getInstance()->findWhere($whereList);
414
        return current($result);
415
    }
416
417
    /**
418
     * Prepare array for WHERE or SET statements
419
     *
420
     * @param  array $where
421
     * @return array
422
     * @throws \Bluz\Common\Exception\ConfigurationException
423
     */
424
    private static function prepareStatement($where)
425
    {
426
        $keys = array_keys($where);
427
        foreach ($keys as &$key) {
428
            $key = DbProxy::quoteIdentifier($key) . ' = ?';
429
        }
430
        return $keys;
431
    }
432
433
    /**
434
     * Prepare Db\Query\Select for current table:
435
     *  - predefine "select" section as "*" from current table
436
     *  - predefine "from" section as current table name and first letter as alias
437
     *  - predefine fetch type
438
     *
439
     * <code>
440
     *     // use default select "*"
441
     *     $select = Users\Table::select();
442
     *     $arrUsers = $select->where('u.id = ?', $id)
443
     *         ->execute();
444
     *
445
     *     // setup custom select "u.id, u.login"
446
     *     $select = Users\Table::select();
447
     *     $arrUsers = $select->select('u.id, u.login')
448
     *         ->where('u.id = ?', $id)
449
     *         ->execute();
450
     * </code>
451
     *
452
     * @return Query\Select
453
     */
454
    public static function select()
455
    {
456
        $self = static::getInstance();
457
458
        $select = new Query\Select();
459
        $select->select($self->table.'.*')
460
            ->from($self->table, $self->table)
461
            ->setFetchType($self->rowClass);
462
463
        return $select;
464
    }
465
466
    /**
467
     * Create Row instance
468
     *
469
     * @param  array $data
470
     * @return Row
471
     */
472
    public static function create(array $data = [])
473
    {
474
        $rowClass = static::getInstance()->rowClass;
475
        /** @var Row $row */
476
        $row = new $rowClass($data);
477
        $row->setTable(static::getInstance());
478
        return $row;
479
    }
480
481
    /**
482
     * Insert new record to table and return last insert Id
483
     *
484
     * <code>
485
     *     Table::insert(['login' => 'Man', 'email' => '[email protected]'])
486
     * </code>
487
     *
488
     * @param  array $data Column-value pairs
489
     * @return string|null Primary key or null
490
     * @throws Exception\DbException
491
     */
492
    public static function insert(array $data)
493
    {
494
        $self = static::getInstance();
495
496
        $data = $self->filterColumns($data);
497
498
        if (!sizeof($data)) {
499
            throw new DbException(
500
                "Invalid field names of table `{$self->table}`. Please check use of `insert()` method"
501
            );
502
        }
503
504
        $table = DbProxy::quoteIdentifier($self->table);
505
506
        $sql = "INSERT INTO $table SET " . join(',', self::prepareStatement($data));
507
        $result = DbProxy::query($sql, array_values($data));
508
        if (!$result) {
509
            return null;
510
        }
511
512
        /**
513
         * If a sequence name was not specified for the name parameter, PDO::lastInsertId()
514
         * returns a string representing the row ID of the last row that was inserted into the database.
515
         *
516
         * If a sequence name was specified for the name parameter, PDO::lastInsertId()
517
         * returns a string representing the last value retrieved from the specified sequence object.
518
         *
519
         * If the PDO driver does not support this capability, PDO::lastInsertId() triggers an IM001 SQLSTATE.
520
         */
521
        return DbProxy::handler()->lastInsertId($self->sequence);
522
    }
523
524
    /**
525
     * Updates existing rows
526
     *
527
     * <code>
528
     *     Table::insert(['login' => 'Man', 'email' => '[email protected]'], ['id' => 42])
529
     * </code>
530
     *
531
     * @param  array $data  Column-value pairs.
532
     * @param  array $where An array of SQL WHERE clause(s)
533
     * @return integer The number of rows updated
534
     * @throws Exception\DbException
535
     */
536
    public static function update(array $data, array $where)
537
    {
538
        if (!sizeof($where)) {
539
            throw new DbException(
540
                "Method `Table::update()` can't update all records in table,\n".
541
                "please use `Db::query()` instead (of cause if you know what are you doing)"
542
            );
543
        }
544
545
        $self = static::getInstance();
546
547
        $data = $self->filterColumns($data);
548
549
        $where = $self->filterColumns($where);
550
551
        if (!sizeof($data) || !sizeof($where)) {
552
            throw new DbException(
553
                "Invalid field names of table `{$self->table}`. Please check use of `update()` method"
554
            );
555
        }
556
557
        $table = DbProxy::quoteIdentifier($self->table);
558
559
        $sql = "UPDATE $table"
560
            . " SET " . join(',', self::prepareStatement($data))
561
            . " WHERE " . join(' AND ', self::prepareStatement($where));
562
563
        return DbProxy::query($sql, array_merge(array_values($data), array_values($where)));
564
    }
565
566
    /**
567
     * Deletes existing rows
568
     *
569
     * <code>
570
     *     Table::delete(['login' => 'Man'])
571
     * </code>
572
     *
573
     * @param  array $where An array of SQL WHERE clause(s)
574
     * @return integer The number of rows deleted
575
     * @throws Exception\DbException
576
     */
577
    public static function delete(array $where)
578
    {
579
        if (!sizeof($where)) {
580
            throw new DbException(
581
                "Method `Table::delete()` can't delete all records in table,\n".
582
                "please use `Db::query()` instead (of cause if you know what are you doing)"
583
            );
584
        }
585
586
        $self = static::getInstance();
587
588
        $where = $self->filterColumns($where);
589
590
        if (!sizeof($where)) {
591
            throw new DbException(
592
                "Invalid field names of table `{$self->table}`. Please check use of `delete()` method"
593
            );
594
        }
595
596
        $table = DbProxy::quoteIdentifier($self->table);
597
598
        $sql = "DELETE FROM $table"
599
            . " WHERE " . join(' AND ', self::prepareStatement($where));
600
        return DbProxy::query($sql, array_values($where));
601
    }
602
603
    /**
604
     * Setup relation "one to one" or "one to many"
605
     *
606
     * @param  string $key
607
     * @param  string $model
608
     * @param  string $foreign
609
     * @return void
610
     */
611
    public function linkTo($key, $model, $foreign)
612
    {
613
        Relations::setRelation($this->model, $key, $model, $foreign);
614
    }
615
616
    /**
617
     * Setup relation "many to many"
618
     * [table1-key] [table1_key-table2-table3_key] [table3-key]
619
     *
620
     * @param  string $model
621
     * @param  string $link
622
     * @return void
623
     */
624
    public function linkToMany($model, $link)
625
    {
626
        Relations::setRelations($this->model, $model, [$link]);
627
    }
628
}
629