Passed
Pull Request — master (#15)
by Todd
02:05
created

Db   F

Complexity

Total Complexity 119

Size/Duplication

Total Lines 933
Duplicated Lines 0 %

Test Coverage

Coverage 86.93%

Importance

Changes 5
Bugs 0 Features 0
Metric Value
eloc 322
c 5
b 0
f 0
dl 0
loc 933
ccs 246
cts 283
cp 0.8693
rs 2
wmc 119

30 Methods

Rating   Name   Duplication   Size   Complexity  
A driverClass() 0 9 3
A __construct() 0 6 2
A fetchTableNames() 0 14 3
A fetchTableDef() 0 21 6
A fetchColumnDefs() 0 14 5
A findPrimaryKeyIndex() 0 7 3
A queryModify() 0 4 1
A escapeLike() 0 2 1
A load() 0 3 2
A getPDO() 0 2 1
A indexCompare() 0 10 5
A getPx() 0 2 1
A queryID() 0 6 2
C fixIndexes() 0 42 14
A setPx() 0 2 1
A getOne() 0 5 2
A stripPrefix() 0 6 2
A reset() 0 4 1
A dropTable() 0 6 1
A query() 0 21 3
A escape() 0 5 2
A prefixTable() 0 6 4
A setPDO() 0 3 1
A queryDefine() 0 3 1
A buildIndexName() 0 12 3
B val() 0 22 9
A dbType() 0 20 6
C defineTable() 0 91 17
F typeDef() 0 72 15
A quote() 0 6 2

How to fix   Complexity   

Complex Class

Complex classes like Db often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Db, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * @author Todd Burry <[email protected]>
4
 * @copyright 2009-2014 Vanilla Forums Inc.
5
 * @license MIT
6
 */
7
8
namespace Garden\Db;
9
10
use PDO;
11
use Garden\Db\Drivers\MySqlDb;
12
use Garden\Db\Drivers\SqliteDb;
13
14
/**
15
 * Defines a standard set of methods that all database drivers must conform to.
16
 */
17
abstract class Db {
18
    use Utils\FetchModeTrait;
19
20
    const QUERY_DEFINE = 'define';
21
    const QUERY_READ = 'read';
22
    const QUERY_WRITE = 'write';
23
24
    const INDEX_PK = 'primary';
25
    const INDEX_IX = 'index';
26
    const INDEX_UNIQUE = 'unique';
27
28
    const OPTION_REPLACE = 'replace';
29
    const OPTION_IGNORE = 'ignore';
30
    const OPTION_UPSERT = 'upsert';
31
    const OPTION_TRUNCATE = 'truncate';
32
    const OPTION_DROP = 'drop';
33
    const OPTION_FETCH_MODE = 'fetchMode';
34
35
    const OP_EQ = '=';
36
    const OP_GT = '>';
37
    const OP_GTE = '>=';
38
    const OP_IN = '$in';
39
    const OP_LIKE = '$like';
40
    const OP_LT = '<';
41
    const OP_LTE = '<=';
42
    const OP_NEQ = '<>';
43
44
    const OP_AND = '$and';
45
    const OP_OR = '$or';
46
47
    /**
48
     * @var string[] Maps PDO drivers to db classes.
49
     */
50
    private static $drivers = [
51
        'mysql' => MySqlDb::class,
52
        'sqlite' => SqliteDb::class
53
    ];
54
55
    /**
56
     * @var array The canonical database types.
57
     */
58
    private static $types = [
59
        // String
60
        'char' => ['type' => 'string', 'length' => true],
61
        'varchar' => ['type' => 'string', 'length' => true],
62
        'tinytext' => ['type' => 'string', 'schema' => ['maxLength' => 255]],
63
        'text' => ['type' => 'string', 'schema' => ['maxLength' =>  65535]],
64
        'mediumtext' => ['type' => 'string', 'schema' => ['maxLength' => 16777215]],
65
        'longtext' => ['type' => 'string', 'schema' => ['maxLength' => 4294967295]],
66
        'binary' => ['type' => 'string', 'length' => true],
67
        'varbinary' => ['type' => 'string', 'length' => true],
68
69
        // Boolean
70
        'bool' => ['type' => 'boolean'],
71
72
        // Integer
73
        'byte' => ['type' => 'integer', 'schema' => ['maximum' => 127, 'minimum' => -128]],
74
        'short' => ['type' => 'integer', 'schema' => ['maximum' => 32767, 'minimum' => -32768]],
75
        'int' => ['type' => 'integer', 'schema' => ['maximum' => 2147483647, 'minimum' => -2147483648]],
76
        'long' => ['type' => 'integer'],
77
78
        // Number
79
        'float' => ['type' => 'number'],
80
        'double' => ['type' => 'number'],
81
        'decimal' => ['type' => 'number', 'precision' => true],
82
        'numeric' => ['type' => 'number', 'precision' => true],
83
84
        // Date/Time
85
        'datetime' => ['type' => 'datetime'],
86
        'timestamp' => ['type' => 'datetime'],
87
88
        // Enum
89
        'enum' => ['type' => 'string', 'enum' => true],
90
91
        // Schema types
92
        'string' => 'varchar',
93
        'boolean' => 'bool',
94
        'integer' => 'int',
95
        'number' => 'float',
96
97
        // Other aliases
98
        'character' => 'char',
99
        'tinyint' => 'byte',
100
        'int8' => 'byte',
101
        'smallint' => 'short',
102
        'int16' => 'short',
103
        'int32' => 'int',
104
        'bigint' => 'long',
105
        'int64' => 'long',
106
        'real' => 'double'
107
    ];
108
109
    /**
110
     * @var string The database prefix.
111
     */
112
    private $px = '';
113
114
    /**
115
     * @var array A cached copy of the table schemas indexed by lowercase name.
116
     */
117
    private $tables = [];
118
119
    /**
120
     * @var array|null A cached copy of the table names indexed by lowercase name.
121
     */
122
    private $tableNames = null;
123
124
    /**
125
     * @var \PDO
126
     */
127
    private $pdo;
128
129
    /**
130
     * Initialize an instance of the {@link MySqlDb} class.
131
     *
132
     * @param PDO $pdo The connection to the database.
133
     * @param string $px The database prefix.
134
     */
135
    public function __construct(PDO $pdo, string $px = '') {
136
        $this->pdo = $pdo;
137
        $this->px = $px;
138
139
        $fetchMode = $this->pdo->getAttribute(PDO::ATTR_DEFAULT_FETCH_MODE);
140
        $this->setFetchMode(in_array($fetchMode, [0, PDO::FETCH_BOTH], true) ? PDO::FETCH_ASSOC: $fetchMode);
141
    }
142
143
    /**
144
     * Get the name of the class that handles a database driver.
145
     *
146
     * @param string|PDO $driver The name of the driver or a database connection.
147
     * @return null|string Returns the driver classname or **null** if one isn't found.
148
     */
149
    public static function driverClass($driver) {
150
        if ($driver instanceof PDO) {
151
            $name = $driver->getAttribute(PDO::ATTR_DRIVER_NAME);
152
        } else {
153
            $name = (string)$driver;
154
        }
155
156
        $name = strtolower($name);
157
        return isset(self::$drivers[$name]) ? self::$drivers[$name] : null;
158
    }
159
160
    /**
161
     * Add a table to the database.
162
     *
163
     * @param array $tableDef The table definition.
164
     * @param array $options An array of additional options when adding the table.
165
     */
166
    abstract protected function createTableDb(array $tableDef, array $options = []);
167
168
    /**
169
     * Alter a table in the database.
170
     *
171
     * When altering a table you pass an array with three optional keys: add, drop, and alter.
172
     * Each value is consists of a table definition in a format that would be passed to {@link Db::setTableDef()}.
173
     *
174
     * @param array $alterDef The alter definition.
175
     * @param array $options An array of additional options when adding the table.
176
     */
177
    abstract protected function alterTableDb(array $alterDef, array $options = []);
178
179
    /**
180
     * Drop a table.
181
     *
182
     * @param string $table The name of the table to drop.
183
     * @param array $options An array of additional options when adding the table.
184
     */
185 10
    final public function dropTable(string $table, array $options = []) {
186 10
        $options += [Db::OPTION_IGNORE => false];
187 10
        $this->dropTableDb($table, $options);
188
189 10
        $tableKey = strtolower($table);
190 10
        unset($this->tables[$tableKey], $this->tableNames[$tableKey]);
191 10
    }
192
193
    /**
194
     * Perform the actual table drop.
195
     *
196
     * @param string $table The name of the table to drop.
197
     * @param array $options An array of additional options when adding the table.
198
     */
199
    abstract protected function dropTableDb(string $table, array $options = []);
200
201
    /**
202
     * Get the names of all the tables in the database.
203
     *
204
     * @return string[] Returns an array of table names without prefixes.
205
     */
206 12
    final public function fetchTableNames() {
207 12
        if ($this->tableNames !== null) {
208 2
            return array_values($this->tableNames);
209
        }
210
211 12
        $names = $this->fetchTableNamesDb();
212
213 12
        $this->tableNames = [];
214 12
        foreach ($names as $name) {
215 12
            $name = $this->stripPrefix($name);
216 12
            $this->tableNames[strtolower($name)] = $name;
217
        }
218
219 12
        return array_values($this->tableNames);
220
    }
221
222
    /**
223
     * Fetch the table names from the underlying database layer.
224
     *
225
     * The driver should return all table names. It doesn't have to strip the prefix.
226
     *
227
     * @return string[]
228
     */
229
    abstract protected function fetchTableNamesDb();
230
231
    /**
232
     * Get a table definition.
233
     *
234
     * @param string $table The name of the table.
235
     * @return array|null Returns the table definition or null if the table does not exist.
236
     */
237 80
    final public function fetchTableDef(string $table) {
238 80
        $tableKey = strtolower($table);
239
240
        // First check the table cache.
241 80
        if (isset($this->tables[$tableKey])) {
242 57
            $tableDef = $this->tables[$tableKey];
243
244 57
            if (isset($tableDef['columns'], $tableDef['indexes'])) {
245 57
                return $tableDef;
246
            }
247 44
        } elseif ($this->tableNames !== null && !isset($this->tableNames[$tableKey])) {
248 36
            return null;
249
        }
250
251 14
        $tableDef = $this->fetchTableDefDb($table);
252 14
        if ($tableDef !== null) {
253 12
            $this->fixIndexes($tableDef['name'], $tableDef);
254 12
            $this->tables[$tableKey] = $tableDef;
255
        }
256
257 14
        return $tableDef;
258
    }
259
260
    /**
261
     * Fetch the table definition from the database.
262
     *
263
     * @param string $table The name of the table to get.
264
     * @return array|null Returns the table def or **null** if the table doesn't exist.
265
     */
266
    abstract protected function fetchTableDefDb(string $table);
267
268
269
    /**
270
     * Get the column definitions for a table.
271
     *
272
     * @param string $table The name of the table to get the columns for.
273
     * @return array|null Returns an array of column definitions.
274
     */
275 3
    final public function fetchColumnDefs(string $table) {
276 3
        $tableKey = strtolower($table);
277
278 3
        if (!empty($this->tables[$tableKey]['columns'])) {
279 1
            $this->tables[$tableKey]['columns'];
280 2
        } elseif ($this->tableNames !== null && !isset($this->tableNames[$tableKey])) {
281
            return null;
282
        }
283
284 3
        $columnDefs = $this->fetchColumnDefsDb($table);
285 3
        if ($columnDefs !== null) {
286 3
            $this->tables[$tableKey]['columns'] = $columnDefs;
287
        }
288 3
        return $columnDefs;
289
    }
290
291
    /**
292
     * Get the column definitions from the database.
293
     *
294
     * @param string $table The name of the table to fetch the columns for.
295
     * @return array|null
296
     */
297
    abstract protected function fetchColumnDefsDb(string $table);
298
299
    /**
300
     * Get the canonical type based on a type string.
301
     *
302
     * @param string $type A type string.
303
     * @return array|null Returns the type schema array or **null** if a type isn't found.
304
     */
305 65
    public static function typeDef(string $type) {
306
        // Check for the unsigned signifier.
307 65
        $unsigned = null;
308 65
        if ($type[0] === 'u') {
309 6
            $unsigned = true;
310 6
            $type = substr($type, 1);
311 63
        } elseif (preg_match('`(.+)\s+unsigned`i', $type, $m)) {
312 2
            $unsigned = true;
313 2
            $type = $m[1];
314
        }
315
316
        // Remove brackets from the type.
317 65
        $brackets = null;
318 65
        if (preg_match('`^(.*)\((.*)\)$`', $type, $m)) {
319 41
            $brackets = $m[2];
320 41
            $type = $m[1];
321
        }
322
323
        // Look for the type.
324 65
        $type = strtolower($type);
325 65
        if (isset(self::$types[$type])) {
326 65
            $row = self::$types[$type];
327 65
            $dbtype = $type;
328
329
            // Resolve an alias.
330 65
            if (is_string($row)) {
331 2
                $dbtype = $row;
332 65
                $row = self::$types[$row];
333
            }
334
        } else {
335
            return null;
336
        }
337
338
        // Now that we have a type row we can build a schema for it.
339
        $schema = [
340 65
            'type' => $row['type'],
341 65
            'dbtype' => $dbtype
342
        ];
343
344 65
        if (!empty($row['schema'])) {
345 44
            $schema += $row['schema'];
346
        }
347
348 65
        if ($row['type'] === 'integer' && $unsigned) {
349 6
            $schema['unsigned'] = true;
350
351 6
            if (!empty($schema['maximum'])) {
352 6
                $schema['maximum'] = $schema['maximum'] * 2 + 1;
353 6
                $schema['minimum'] = 0;
354
            }
355
        }
356
357 65
        if (!empty($row['length'])) {
358 32
            $schema['maxLength'] = (int)$brackets ?: 255;
359
        }
360
361 65
        if (!empty($row['precision'])) {
362 2
            $parts = array_map('trim', explode(',', $brackets));
363 2
            $schema['precision'] = (int)$parts[0];
364 2
            if (isset($parts[1])) {
365 2
                $schema['scale'] = (int)$parts[1];
366
            }
367
        }
368
369 65
        if (!empty($row['enum'])) {
370 3
            $enum = explode(',', $brackets);
371
            $schema['enum'] = array_map(function ($str) {
372 3
                return trim($str, "'\" \t\n\r\0\x0B");
373 3
            }, $enum);
374
        }
375
376 65
        return $schema;
377
    }
378
379
    /**
380
     * Get the database type string from a type definition.
381
     *
382
     * This is the opposite of {@link Db::typeDef()}.
383
     *
384
     * @param array $typeDef The type definition array.
385
     * @return string Returns a db type string.
386
     */
387 44
    protected static function dbType(array $typeDef) {
388 44
        $dbtype = $typeDef['dbtype'];
389
390 44
        if (!empty($typeDef['maxLength'])) {
391 22
            $dbtype .= "({$typeDef['maxLength']})";
392 37
        } elseif (!empty($typeDef['unsigned'])) {
393
            $dbtype = 'u'.$dbtype;
394 37
        } elseif (!empty($typeDef['precision'])) {
395
            $dbtype .= "({$typeDef['precision']}";
396
            if (!empty($typeDef['scale'])) {
397
                $dbtype .= ",{$typeDef['scale']}";
398
            }
399
            $dbtype .= ')';
400 37
        } elseif (!empty($typeDef['enum'])) {
401
            $parts = array_map(function ($str) {
402 1
                return "'{$str}'";
403 1
            }, $typeDef['enum']);
404 1
            $dbtype .= '('.implode(',', $parts).')';
405
        }
406 44
        return $dbtype;
407
    }
408
409
410
    /**
411
     * Get the native database type based on a type schema.
412
     *
413
     * The default implementation of this method returns the canonical db types. Individual database classes will have
414
     * to override to provide any differences.
415
     *
416
     * @param array $type The type schema.
417
     * @return string
418
     */
419
    abstract protected function nativeDbType(array $type);
420
421
    /**
422
     * Set a table definition to the database.
423
     *
424
     * @param array $tableDef The table definition.
425
     * @param array $options An array of additional options when adding the table.
426
     * @throws \Exception Throws an exception if there is a mismatch in the primary key column and index.
427
     */
428 80
    final public function defineTable(array $tableDef, array $options = []) {
429 80
        $options += [Db::OPTION_DROP => false];
430
431 80
        $tableName = $tableDef['name'];
432 80
        $tableKey = strtolower($tableName);
433 80
        $tableDef['name'] = $tableName;
434 80
        $curTable = $this->fetchTableDef($tableName);
435
436 80
        $this->fixIndexes($tableName, $tableDef, $curTable);
437
438 80
        if ($this->tableNames === null) {
439
            // Fetch all tables here now so the cache knows all tables that exist.
440 12
            $this->fetchTableNames();
441
        }
442
443 80
        if (!$curTable) {
444 44
            $this->createTableDb($tableDef, $options);
445 44
            $this->tables[$tableKey] = $tableDef;
446 44
            $this->tableNames[$tableKey] = $tableDef['name'];
447 44
            return;
448
        }
449
        // This is the alter statement.
450 55
        $alterDef = ['name' => $tableName];
451
452
        // Figure out the columns that have changed.
453 55
        $curColumns = (array)$curTable['columns'];
454 55
        $newColumns = (array)$tableDef['columns'];
455
456 55
        $alterDef['add']['columns'] = array_diff_key($newColumns, $curColumns);
457
        $alterDef['alter']['columns'] = array_uintersect_assoc($newColumns, $curColumns, function ($new, $curr) {
458 55
            $search = ['dbtype', 'allowNull', 'default', 'maxLength'];
459 55
            foreach ($search as $key) {
460 55
                if (self::val($key, $curr) !== self::val($key, $new)) {
461
                    // Return 0 if the values are different, not the same.
462 8
                    return 0;
463
                }
464
            }
465
466
            // Enum checking.
467 53
            if (isset($curr['enum']) xor isset($new['enum'])) {
468
                return 0;
469 53
            } elseif (isset($curr['enum']) && isset($new['enum'])
470
                && (
471 2
                    count($curr['enum']) !== count($new['enum'])
472 53
                    || !empty(array_diff($curr['enum'], $new['enum']))
473
                )
474
            ) {
475 2
                return 0;
476
            }
477
478 51
            return 1;
479 55
        });
480
481
        // Figure out the indexes that have changed.
482 55
        $curIndexes = (array)self::val('indexes', $curTable, []);
483 55
        $newIndexes = (array)self::val('indexes', $tableDef, []);
484
485 55
        $alterDef['add']['indexes'] = array_udiff($newIndexes, $curIndexes, [$this, 'indexCompare']);
486
487 55
        $dropIndexes = array_udiff($curIndexes, $newIndexes, [$this, 'indexCompare']);
488 55
        if ($options[Db::OPTION_DROP]) {
489 2
            $alterDef['drop']['columns'] = array_diff_key($curColumns, $newColumns);
490 2
            $alterDef['drop']['indexes'] = $dropIndexes;
491
        } else {
492 53
            $alterDef['drop']['columns'] = [];
493 53
            $alterDef['drop']['indexes'] = [];
494
495
            // If the primary key has changed then the old one needs to be dropped.
496 53
            if ($pk = $this->findPrimaryKeyIndex($dropIndexes)) {
497 4
                $alterDef['drop']['indexes'][] = $pk;
498
            }
499
        }
500
501
        // Check to see if any alterations at all need to be made.
502 55
        if (empty($alterDef['add']['columns']) && empty($alterDef['add']['indexes']) &&
503 55
            empty($alterDef['drop']['columns']) && empty($alterDef['drop']['indexes']) &&
504 55
            empty($alterDef['alter']['columns'])
505
        ) {
506 39
            return;
507
        }
508
509 16
        $alterDef['def'] = $tableDef;
510
511
        // Alter the table.
512 16
        $this->alterTableDb($alterDef, $options);
513
514
        // Update the cached schema.
515 16
        $tableDef['name'] = $tableName;
516 16
        $this->tables[$tableKey] = $tableDef;
517
518 16
        $this->tableNames[$tableKey] = $tableName;
519 16
    }
520
521
    /**
522
     * Find the primary key in an array of indexes.
523
     *
524
     * @param array $indexes The indexes to search.
525
     * @return array|null Returns the primary key or **null** if there isn't one.
526
     */
527 64
    protected function findPrimaryKeyIndex(array $indexes) {
528 64
        foreach ($indexes as $index) {
529 15
            if ($index['type'] === Db::INDEX_PK) {
530 12
                return $index;
531
            }
532
        }
533 53
        return null;
534
    }
535
536
    /**
537
     * Move the primary key index into the correct place for database drivers.
538
     *
539
     * @param string $tableName The name of the table.
540
     * @param array &$tableDef The table definition.
541
     * @param array|null $curTableDef The current database table def used to resolve conflicts in some names.
542
     * @throws \Exception Throws an exception when there is a mismatch between the primary index and the primary key
543
     * defined on the columns themselves.
544
     */
545 80
    private function fixIndexes(string $tableName, array &$tableDef, $curTableDef = null) {
546 80
        $tableDef += ['indexes' => []];
547
548
        // Loop through the columns and add the primary key index.
549 80
        $primaryColumns = [];
550 80
        foreach ($tableDef['columns'] as $cname => $cdef) {
551 80
            if (!empty($cdef['primary'])) {
552 28
                $primaryColumns[] = $cname;
553
            }
554
        }
555
556
        // Massage the primary key index.
557 80
        $primaryFound = false;
558 80
        foreach ($tableDef['indexes'] as &$indexDef) {
559 68
            $indexDef += ['name' => $this->buildIndexName($tableName, $indexDef), 'type' => null];
560
561 68
            if ($indexDef['type'] === Db::INDEX_PK) {
562 32
                $primaryFound = true;
563
564 32
                if (empty($primaryColumns)) {
565 10
                    foreach ($indexDef['columns'] as $cname) {
566 10
                        $tableDef['columns'][$cname]['primary'] = true;
567
                    }
568 24
                } elseif (array_diff($primaryColumns, $indexDef['columns'])) {
569 32
                    throw new \Exception("There is a mismatch in the primary key index and primary key columns.", 500);
570
                }
571 58
            } elseif (isset($curTableDef['indexes'])) {
572 41
                foreach ($curTableDef['indexes'] as $curIndexDef) {
573 41
                    if ($this->indexCompare($indexDef, $curIndexDef) === 0) {
574 41
                        if (!empty($curIndexDef['name'])) {
575 41
                            $indexDef['name'] = $curIndexDef['name'];
576
                        }
577 41
                        break;
578
                    }
579
                }
580
            }
581
        }
582
583 80
        if (!$primaryFound && !empty($primaryColumns)) {
584 4
            $tableDef['indexes'][] = [
585 4
                'columns' => $primaryColumns,
586
                'type' => Db::INDEX_PK
587
            ];
588
        }
589 80
    }
590
591
    /**
592
     * Get the database prefix.
593
     *
594
     * @return string Returns the current db prefix.
595
     */
596 12
    public function getPx(): string {
597 12
        return $this->px;
598
    }
599
600
    /**
601
     * Set the database prefix.
602
     *
603
     * @param string $px The new database prefix.
604
     */
605
    public function setPx(string $px) {
606
        $this->px = $px;
607
    }
608
609
    /**
610
     * Compare two index definitions to see if they have the same columns and same type.
611
     *
612
     * @param array $a The first index.
613
     * @param array $b The second index.
614
     * @return int Returns an integer less than, equal to, or greater than zero if {@link $a} is
615
     * considered to be respectively less than, equal to, or greater than {@link $b}.
616
     */
617 47
    private function indexCompare(array $a, array $b): int {
618 47
        if ($a['columns'] > $b['columns']) {
619 15
            return 1;
620 47
        } elseif ($a['columns'] < $b['columns']) {
621 15
            return -1;
622
        }
623
624 43
        return strcmp(
625 43
            isset($a['type']) ? $a['type'] : '',
626 43
            isset($b['type']) ? $b['type'] : ''
627
        );
628
    }
629
630
    /**
631
     * Get data from the database.
632
     *
633
     * @param string|Identifier $table The name of the table to get the data from.
634
     * @param array $where An array of where conditions.
635
     * @param array $options An array of additional options.
636
     * @return \PDOStatement Returns the result set.
637
     */
638
    abstract public function get($table, array $where, array $options = []): \PDOStatement;
639
640
    /**
641
     * Get a single row from the database.
642
     *
643
     * This is a convenience method that calls {@link Db::get()} and shifts off the first row.
644
     *
645
     * @param string|Identifier $table The name of the table to get the data from.
646
     * @param array $where An array of where conditions.
647
     * @param array $options An array of additional options.
648
     * @return array|object|null Returns the row or false if there is no row.
649
     */
650 20
    final public function getOne($table, array $where, array $options = []) {
651 20
        $rows = $this->get($table, $where, $options);
652 20
        $row = $rows->fetch();
653
654 20
        return $row === false ? null : $row;
655
    }
656
657
    /**
658
     * Insert a row into a table.
659
     *
660
     * @param string $table The name of the table to insert into.
661
     * @param array $row The row of data to insert.
662
     * @param array $options An array of options for the insert.
663
     *
664
     * Db::OPTION_IGNORE
665
     * : Whether or not to ignore inserts that lead to a duplicate key. *default false*
666
     * Db::OPTION_REPLACE
667
     * : Whether or not to replace duplicate keys. *default false*
668
     * Db::OPTION_UPSERT
669
     * : Whether or not to update the existing data when duplicate keys exist.
670
     *
671
     * @return mixed Returns the id of the inserted record, **true** if the table doesn't have an auto increment, or **false** otherwise.
672
     * @see Db::load()
673
     */
674
    abstract public function insert(string $table, array $row, array $options = []);
675
676
    /**
677
     * Load many rows into a table.
678
     *
679
     * @param string $table The name of the table to insert into.
680
     * @param \Traversable|array $rows A dataset to insert.
681
     * Note that all rows must contain the same columns.
682
     * The first row will be looked at for the structure of the insert and the rest of the rows will use this structure.
683
     * @param array $options An array of options for the inserts. See {@link Db::insert()} for details.
684
     * @see Db::insert()
685
     */
686
    public function load(string $table, $rows, array $options = []) {
687
        foreach ($rows as $row) {
688
            $this->insert($table, $row, $options);
689
        }
690
    }
691
692
693
    /**
694
     * Update a row or rows in a table.
695
     *
696
     * @param string $table The name of the table to update.
697
     * @param array $set The values to set.
698
     * @param array $where The where filter for the update.
699
     * @param array $options An array of options for the update.
700
     * @return int Returns the number of affected rows.
701
     */
702
    abstract public function update(string $table, array $set, array $where, array $options = []): int;
703
704
    /**
705
     * Delete rows from a table.
706
     *
707
     * @param string $table The name of the table to delete from.
708
     * @param array $where The where filter of the delete.
709
     * @param array $options An array of options.
710
     *
711
     * Db:OPTION_TRUNCATE
712
     * : Truncate the table instead of deleting rows. In this case {@link $where} must be blank.
713
     * @return int Returns the number of affected rows.
714
     */
715
    abstract public function delete(string $table, array $where, array $options = []): int;
716
717
    /**
718
     * Reset the internal table definition cache.
719
     *
720
     * @return $this
721
     */
722 14
    public function reset() {
723 14
        $this->tables = [];
724 14
        $this->tableNames = null;
725 14
        return $this;
726
    }
727
728
    /**
729
     * Build a standardized index name from an index definition.
730
     *
731
     * @param string $tableName The name of the table the index is in.
732
     * @param array $indexDef The index definition.
733
     * @return string Returns the index name.
734
     */
735 68
    protected function buildIndexName(string $tableName, array $indexDef): string {
736 68
        $indexDef += ['type' => Db::INDEX_IX, 'suffix' => ''];
737
738 68
        $type = $indexDef['type'];
739
740 68
        if ($type === Db::INDEX_PK) {
741 33
            return 'primary';
742
        }
743 58
        $px = self::val($type, [Db::INDEX_IX => 'ix_', Db::INDEX_UNIQUE => 'ux_'], 'ix_');
744 58
        $sx = $indexDef['suffix'];
745 58
        $result = $px.$tableName.'_'.($sx ?: implode('', $indexDef['columns']));
746 58
        return $result;
747
    }
748
749
    /**
750
     * Execute a query that fetches data.
751
     *
752
     * @param string $sql The query to execute.
753
     * @param array $params Input parameters for the query.
754
     * @param array $options Additional options.
755
     * @return \PDOStatement Returns the result of the query.
756
     * @throws \PDOException Throws an exception if something went wrong during the query.
757
     */
758 107
    protected function query(string $sql, array $params = [], array $options = []): \PDOStatement {
759
        $options += [
760 107
            Db::OPTION_FETCH_MODE => $this->getFetchArgs()
761
        ];
762
763 107
        $stm = $this->getPDO()->prepare($sql);
764
765
766 107
        if ($options[Db::OPTION_FETCH_MODE]) {
767 92
            $stm->setFetchMode(...(array)$options[Db::OPTION_FETCH_MODE]);
0 ignored issues
show
Bug introduced by
(array)$options[Garden\Db\Db::OPTION_FETCH_MODE] is expanded, but the parameter $mode of PDOStatement::setFetchMode() does not expect variable arguments. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

767
            $stm->setFetchMode(/** @scrutinizer ignore-type */ ...(array)$options[Db::OPTION_FETCH_MODE]);
Loading history...
768
        }
769
770 107
        $r = $stm->execute($params);
771
772
        // This is a kludge for those that don't have errors turning into exceptions.
773 107
        if ($r === false) {
774
            list($state, $code, $msg) = $stm->errorInfo();
775
            throw new \PDOException($msg, $code);
776
        }
777
778 107
        return $stm;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $stm could return the type boolean which is incompatible with the type-hinted return PDOStatement. Consider adding an additional type-check to rule them out.
Loading history...
779
    }
780
781
    /**
782
     * Query the database and return a row count.
783
     *
784
     * @param string $sql The query to execute.
785
     * @param array $params Input parameters for the query.
786
     * @param array $options Additional options.
787
     * @return int
788
     */
789 40
    protected function queryModify(string $sql, array $params = [], array $options = []): int {
790 40
        $options += [Db::OPTION_FETCH_MODE => 0];
791 40
        $stm = $this->query($sql, $params, $options);
792 40
        return $stm->rowCount();
793
    }
794
795
    /**
796
     * Query the database and return the ID of the record that was inserted.
797
     *
798
     * @param string $sql The query to execute.
799
     * @param array $params Input parameters for the query.
800
     * @param array $options Additional options.
801
     * @return mixed Returns the record ID.
802
     */
803 23
    protected function queryID(string $sql, array $params = [], array $options = []) {
0 ignored issues
show
Coding Style introduced by
This method is not in camel caps format.

This check looks for method names that are not written in camelCase.

In camelCase names are written without any punctuation, the start of each new word being marked by a capital letter. Thus the name database connection seeker becomes databaseConnectionSeeker.

Loading history...
804 23
        $options += [Db::OPTION_FETCH_MODE => 0];
805 23
        $this->query($sql, $params, $options);
806 23
        $r = $this->getPDO()->lastInsertId();
807
808 23
        return is_numeric($r) ? (int)$r : $r;
809
    }
810
811
    /**
812
     * Query the database for a database define.
813
     *
814
     * @param string $sql The query to execute.
815
     * @param array $options Additional options.
816
     */
817 44
    protected function queryDefine(string $sql, array $options = []) {
818 44
        $options += [Db::OPTION_FETCH_MODE => 0];
819 44
        $this->query($sql, [], $options);
820 44
    }
821
822
    /**
823
     * Safely get a value out of an array.
824
     *
825
     * This function will always return a value even if the array key doesn't exist.
826
     * The self::val() function is one of the biggest workhorses of Vanilla and shows up a lot throughout other code.
827
     * It's much preferable to use this function if your not sure whether or not an array key exists rather than
828
     * using @ error suppression.
829
     *
830
     * This function uses optimizations found in the [facebook libphputil library](https://github.com/facebook/libphutil).
831
     *
832
     * @param string|int $key The array key.
833
     * @param array|object $array The array to get the value from.
834
     * @param mixed $default The default value to return if the key doesn't exist.
835
     * @return mixed The item from the array or `$default` if the array key doesn't exist.
836
     * @category Array Functions
837
     */
838 98
    protected static function val($key, $array, $default = null) {
839 98
        if (is_array($array)) {
840
            // isset() is a micro-optimization - it is fast but fails for null values.
841 98
            if (isset($array[$key])) {
842 91
                return $array[$key];
843
            }
844
845
            // Comparing $default is also a micro-optimization.
846 97
            if ($default === null || array_key_exists($key, $array)) {
847 97
                return null;
848
            }
849
        } elseif (is_object($array)) {
850
            if (isset($array->$key)) {
851
                return $array->$key;
852
            }
853
854
            if ($default === null || property_exists($array, $key)) {
855
                return null;
856
            }
857
        }
858
859 4
        return $default;
860
    }
861
862
    /**
863
     * Escape an identifier.
864
     *
865
     * @param string|Literal $identifier The identifier to escape.
866
     * @return string Returns the field properly escaped.
867
     */
868 107
    public function escape($identifier): string {
869 107
        if ($identifier instanceof Literal) {
870 6
            return $identifier->getValue($this);
871
        }
872 107
        return '`'.str_replace('`', '``', $identifier).'`';
873
    }
874
875
    /**
876
     * Escape a a like string so that none of its characters work as wildcards.
877
     *
878
     * @param string $str The string to escape.
879
     * @return string Returns an escaped string.
880
     */
881 12
    protected function escapeLike(string $str): string {
882 12
        return addcslashes($str, '_%');
883
    }
884
885
    /**
886
     * Prefix a table name.
887
     *
888
     * @param string|Identifier $table The name of the table to prefix.
889
     * @param bool $escape Whether or not to escape the output.
890
     * @return string Returns a full table name.
891
     */
892 107
    protected function prefixTable($table, bool $escape = true): string {
893 107
        if ($table instanceof Identifier) {
894 15
            return $escape ? $table->escape($this) : (string)$table;
895
        } else {
896 107
            $table = $this->px.$table;
897 107
            return $escape ? $this->escape($table) : $table;
898
        }
899
    }
900
901
    /**
902
     * Strip the database prefix off a table name.
903
     *
904
     * @param string $table The name of the table to strip.
905
     * @return string Returns the table name stripped of the prefix.
906
     */
907 12
    protected function stripPrefix(string $table): string {
908 12
        $len = strlen($this->px);
909 12
        if (strcasecmp(substr($table, 0, $len), $this->px) === 0) {
910 12
            $table = substr($table, $len);
911
        }
912 12
        return $table;
913
    }
914
915
    /**
916
     * Optionally quote a where value.
917
     *
918
     * @param mixed $value The value to quote.
919
     * @param string $column The column being operated on. It must already be quoted.
920
     * @return string Returns the value, optionally quoted.
921
     * @internal param bool $quote Whether or not to quote the value.
922
     */
923 46
    public function quote($value, string $column = ''): string {
924 46
        if ($value instanceof Literal) {
925
            /* @var Literal $value */
926 25
            return $value->getValue($this, $column);
927
        } else {
928 41
            return $this->getPDO()->quote($value);
929
        }
930
    }
931
932
    /**
933
     * Gets the {@link PDO} object for this connection.
934
     *
935
     * @return \PDO
936
     */
937 107
    public function getPDO(): PDO {
0 ignored issues
show
Coding Style introduced by
This method is not in camel caps format.

This check looks for method names that are not written in camelCase.

In camelCase names are written without any punctuation, the start of each new word being marked by a capital letter. Thus the name database connection seeker becomes databaseConnectionSeeker.

Loading history...
938 107
        return $this->pdo;
939
    }
940
941
    /**
942
     * Set the connection to the database.
943
     *
944
     * @param PDO $pdo The new connection to the database.
945
     * @return $this
946
     */
947
    public function setPDO(PDO $pdo) {
0 ignored issues
show
Coding Style introduced by
This method is not in camel caps format.

This check looks for method names that are not written in camelCase.

In camelCase names are written without any punctuation, the start of each new word being marked by a capital letter. Thus the name database connection seeker becomes databaseConnectionSeeker.

Loading history...
948
        $this->pdo = $pdo;
949
        return $this;
950
    }
951
}
952