Test Failed
Pull Request — master (#12)
by Todd
02:13
created

Db::dbType()   A

Complexity

Conditions 6
Paths 6

Size

Total Lines 20
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 6.9157

Importance

Changes 0
Metric Value
cc 6
eloc 16
nc 6
nop 1
dl 0
loc 20
ccs 12
cts 17
cp 0.7059
crap 6.9157
rs 9.1111
c 0
b 0
f 0
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 9
    final public function dropTable(string $table, array $options = []) {
186 9
        $options += [Db::OPTION_IGNORE => false];
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
187 9
        $this->dropTableDb($table, $options);
188
189 9
        $tableKey = strtolower($table);
190 9
        unset($this->tables[$tableKey], $this->tableNames[$tableKey]);
191 9
    }
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 2
    final public function fetchTableNames() {
207 2
        if ($this->tableNames !== null) {
208 2
            return array_values($this->tableNames);
209
        }
210
211 2
        $names = $this->fetchTableNamesDb();
212
213 2
        $this->tableNames = [];
214 2
        foreach ($names as $name) {
215 2
            $name = $this->stripPrefix($name);
216 2
            $this->tableNames[strtolower($name)] = $name;
217
        }
218
219 2
        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 74
    final public function fetchTableDef(string $table) {
238 74
        $tableKey = strtolower($table);
239
240
        // First check the table cache.
241 74
        if (isset($this->tables[$tableKey])) {
242 55
            $tableDef = $this->tables[$tableKey];
243
244 55
            if (isset($tableDef['columns'], $tableDef['indexes'])) {
245 55
                return $tableDef;
246
            }
247 38
        } elseif ($this->tableNames !== null && !isset($this->tableNames[$tableKey])) {
248 30
            return null;
249
        }
250
251 12
        $tableDef = $this->fetchTableDefDb($table);
252 12
        if ($tableDef !== null) {
253 10
            $this->fixIndexes($tableDef['name'], $tableDef);
254 10
            $this->tables[$tableKey] = $tableDef;
255
        }
256
257 12
        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 1
    final public function fetchColumnDefs(string $table) {
276 1
        $tableKey = strtolower($table);
277
278 1
        if (!empty($this->tables[$tableKey]['columns'])) {
279 1
            $this->tables[$tableKey]['columns'];
280
        } elseif ($this->tableNames !== null && !isset($this->tableNames[$tableKey])) {
281
            return null;
282
        }
283
284 1
        $columnDefs = $this->fetchColumnDefsDb($table);
285 1
        if ($columnDefs !== null) {
286 1
            $this->tables[$tableKey]['columns'] = $columnDefs;
287
        }
288 1
        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 59
    public static function typeDef(string $type) {
306
        // Check for the unsigned signifier.
307 59
        $unsigned = null;
308 59
        if ($type[0] === 'u') {
309 6
            $unsigned = true;
310 6
            $type = substr($type, 1);
311 57
        } 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 59
        $brackets = null;
318 59
        if (preg_match('`^(.*)\((.*)\)$`', $type, $m)) {
319 37
            $brackets = $m[2];
320 37
            $type = $m[1];
321
        }
322
323
        // Look for the type.
324 59
        $type = strtolower($type);
325 59
        if (isset(self::$types[$type])) {
326 59
            $row = self::$types[$type];
327 59
            $dbtype = $type;
328
329
            // Resolve an alias.
330 59
            if (is_string($row)) {
331 2
                $dbtype = $row;
332 59
                $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 59
            'type' => $row['type'],
341 59
            'dbtype' => $dbtype
342
        ];
343
344 59
        if (!empty($row['schema'])) {
345 44
            $schema += $row['schema'];
346
        }
347
348 59
        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 59
        if (!empty($row['length'])) {
358 28
            $schema['maxLength'] = (int)$brackets ?: 255;
359
        }
360
361 59
        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 59
        if (!empty($row['enum'])) {
370 3
            $enum = explode(',', $brackets);
371 3
            $schema['enum'] = array_map(function ($str) {
372 3
                return trim($str, "'\" \t\n\r\0\x0B");
373 3
            }, $enum);
374
        }
375
376 59
        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 38
    protected static function dbType(array $typeDef) {
388 38
        $dbtype = $typeDef['dbtype'];
389
390 38
        if (!empty($typeDef['maxLength'])) {
391 18
            $dbtype .= "({$typeDef['maxLength']})";
392 35
        } elseif (!empty($typeDef['unsigned'])) {
393
            $dbtype = 'u'.$dbtype;
394 35
        } elseif (!empty($typeDef['precision'])) {
395
            $dbtype .= "({$typeDef['precision']}";
396
            if (!empty($typeDef['scale'])) {
397
                $dbtype .= ",{$typeDef['scale']}";
398
            }
399
            $dbtype .= ')';
400 35
        } elseif (!empty($typeDef['enum'])) {
401 1
            $parts = array_map(function ($str) {
402 1
                return "'{$str}'";
403 1
            }, $typeDef['enum']);
404 1
            $dbtype .= '('.implode(',', $parts).')';
405
        }
406 38
        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
     */
427 74
    final public function defineTable(array $tableDef, array $options = []) {
428 74
        $options += [Db::OPTION_DROP => false];
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
429
430 74
        $tableName = $tableDef['name'];
431 74
        $tableKey = strtolower($tableName);
432 74
        $tableDef['name'] = $tableName;
433 74
        $curTable = $this->fetchTableDef($tableName);
434
435 74
        $this->fixIndexes($tableName, $tableDef, $curTable);
436
437 74
        if (!$curTable) {
438 38
            $this->createTableDb($tableDef, $options);
439 38
            $this->tables[$tableKey] = $tableDef;
440 38
            $this->tableNames[$tableKey] = $tableDef['name'];
441 38
            return;
442
        }
443
        // This is the alter statement.
444 51
        $alterDef = ['name' => $tableName];
445
446
        // Figure out the columns that have changed.
447 51
        $curColumns = (array)$curTable['columns'];
448 51
        $newColumns = (array)$tableDef['columns'];
449
450 51
        $alterDef['add']['columns'] = array_diff_key($newColumns, $curColumns);
451 51
        $alterDef['alter']['columns'] = array_uintersect_assoc($newColumns, $curColumns, function ($new, $curr) {
452 51
            $search = ['dbtype', 'allowNull', 'default'];
453 51
            foreach ($search as $key) {
454 51
                if (self::val($key, $curr) !== self::val($key, $new)) {
455
                    // Return 0 if the values are different, not the same.
456 51
                    return 0;
457
                }
458
            }
459
460
            // Enum checking.
461 51
            if (isset($curr['enum']) xor isset($new['enum'])) {
462
                return 0;
463 51
            } elseif (isset($curr['enum']) && isset($new['enum'])
464
                && (
465 2
                    count($curr['enum']) !== count($new['enum'])
466 51
                    || !empty(array_diff($curr['enum'], $new['enum']))
467
                )
468
            ) {
469 2
                return 0;
470
            }
471
472 49
            return 1;
473 51
        });
474
475
        // Figure out the indexes that have changed.
476 51
        $curIndexes = (array)self::val('indexes', $curTable, []);
477 51
        $newIndexes = (array)self::val('indexes', $tableDef, []);
478
479 51
        $alterDef['add']['indexes'] = array_udiff($newIndexes, $curIndexes, [$this, 'indexCompare']);
480
481 51
        $dropIndexes = array_udiff($curIndexes, $newIndexes, [$this, 'indexCompare']);
482 51
        if ($options[Db::OPTION_DROP]) {
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
483 2
            $alterDef['drop']['columns'] = array_diff_key($curColumns, $newColumns);
484 2
            $alterDef['drop']['indexes'] = $dropIndexes;
485
        } else {
486 49
            $alterDef['drop']['columns'] = [];
487 49
            $alterDef['drop']['indexes'] = [];
488
489
            // If the primary key has changed then the old one needs to be dropped.
490 49
            if ($pk = $this->findPrimaryKeyIndex($dropIndexes)) {
491 4
                $alterDef['drop']['indexes'][] = $pk;
492
            }
493
        }
494
495
        // Check to see if any alterations at all need to be made.
496 51
        if (empty($alterDef['add']['columns']) && empty($alterDef['add']['indexes']) &&
497 51
            empty($alterDef['drop']['columns']) && empty($alterDef['drop']['indexes']) &&
498 51
            empty($alterDef['alter']['columns'])
499
        ) {
500 37
            return;
501
        }
502
503 14
        $alterDef['def'] = $tableDef;
504
505
        // Alter the table.
506 14
        $this->alterTableDb($alterDef, $options);
507
508
        // Update the cached schema.
509 14
        $tableDef['name'] = $tableName;
510 14
        $this->tables[$tableKey] = $tableDef;
511
512 14
        if ($this->tableNames === null) {
513 2
            $this->fetchTableNames();
514
        }
515
516 14
        $this->tableNames[$tableKey] = $tableName;
517 14
    }
518
519
    /**
520
     * Find the primary key in an array of indexes.
521
     *
522
     * @param array $indexes The indexes to search.
523
     * @return array|null Returns the primary key or **null** if there isn't one.
524
     */
525 59
    protected function findPrimaryKeyIndex(array $indexes) {
526 59
        foreach ($indexes as $index) {
527 15
            if ($index['type'] === Db::INDEX_PK) {
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
528 15
                return $index;
529
            }
530
        }
531 48
        return null;
532
    }
533
534
    /**
535
     * Move the primary key index into the correct place for database drivers.
536
     *
537
     * @param string $tableName The name of the table.
538
     * @param array &$tableDef The table definition.
539
     * @param array|null $curTableDef The current database table def used to resolve conflicts in some names.
540
     * @throws \Exception Throws an exception when there is a mismatch between the primary index and the primary key
541
     * defined on the columns themselves.
542
     */
543 74
    private function fixIndexes(string $tableName, array &$tableDef, $curTableDef = null) {
544 74
        $tableDef += ['indexes' => []];
545
546
        // Loop through the columns and add the primary key index.
547 74
        $primaryColumns = [];
548 74
        foreach ($tableDef['columns'] as $cname => $cdef) {
549 74
            if (!empty($cdef['primary'])) {
550 74
                $primaryColumns[] = $cname;
551
            }
552
        }
553
554
        // Massage the primary key index.
555 74
        $primaryFound = false;
556 74
        foreach ($tableDef['indexes'] as &$indexDef) {
557 68
            $indexDef += ['name' => $this->buildIndexName($tableName, $indexDef), 'type' => null];
558
559 68
            if ($indexDef['type'] === Db::INDEX_PK) {
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
560 32
                $primaryFound = true;
561
562 32
                if (empty($primaryColumns)) {
563 10
                    foreach ($indexDef['columns'] as $cname) {
564 10
                        $tableDef['columns'][$cname]['primary'] = true;
565
                    }
566 24
                } elseif (array_diff($primaryColumns, $indexDef['columns'])) {
567 32
                    throw new \Exception("There is a mismatch in the primary key index and primary key columns.", 500);
568
                }
569 58
            } elseif (isset($curTableDef['indexes'])) {
570 41
                foreach ($curTableDef['indexes'] as $curIndexDef) {
571 41
                    if ($this->indexCompare($indexDef, $curIndexDef) === 0) {
572 41
                        if (!empty($curIndexDef['name'])) {
573 41
                            $indexDef['name'] = $curIndexDef['name'];
574
                        }
575 68
                        break;
576
                    }
577
                }
578
            }
579
        }
580
581 74
        if (!$primaryFound && !empty($primaryColumns)) {
582 4
            $tableDef['indexes'][] = [
583 4
                'columns' => $primaryColumns,
584
                'type' => Db::INDEX_PK
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
585
            ];
586
        }
587 74
    }
588
589
    /**
590
     * Get the database prefix.
591
     *
592
     * @return string Returns the current db prefix.
593
     */
594 2
    public function getPx(): string {
595 2
        return $this->px;
596
    }
597
598
    /**
599
     * Set the database prefix.
600
     *
601
     * @param string $px The new database prefix.
602
     */
603
    public function setPx(string $px) {
604
        $this->px = $px;
605
    }
606
607
    /**
608
     * Compare two index definitions to see if they have the same columns and same type.
609
     *
610
     * @param array $a The first index.
611
     * @param array $b The second index.
612
     * @return int Returns an integer less than, equal to, or greater than zero if {@link $a} is
613
     * considered to be respectively less than, equal to, or greater than {@link $b}.
614
     */
615 47
    private function indexCompare(array $a, array $b): int {
616 47
        if ($a['columns'] > $b['columns']) {
617 15
            return 1;
618 47
        } elseif ($a['columns'] < $b['columns']) {
619 15
            return -1;
620
        }
621
622 43
        return strcmp(
623 43
            isset($a['type']) ? $a['type'] : '',
624 43
            isset($b['type']) ? $b['type'] : ''
625
        );
626
    }
627
628
    /**
629
     * Get data from the database.
630
     *
631
     * @param string|Identifier $table The name of the table to get the data from.
632
     * @param array $where An array of where conditions.
633
     * @param array $options An array of additional options.
634
     * @return \PDOStatement Returns the result set.
635
     */
636
    abstract public function get($table, array $where, array $options = []): \PDOStatement;
637
638
    /**
639
     * Get a single row from the database.
640
     *
641
     * This is a convenience method that calls {@link Db::get()} and shifts off the first row.
642
     *
643
     * @param string|Identifier $table The name of the table to get the data from.
644
     * @param array $where An array of where conditions.
645
     * @param array $options An array of additional options.
646
     * @return array|object|null Returns the row or false if there is no row.
647
     */
648 16
    final public function getOne($table, array $where, array $options = []) {
649 16
        $rows = $this->get($table, $where, $options);
650 16
        $row = $rows->fetch();
651
652 16
        return $row === false ? null : $row;
653
    }
654
655
    /**
656
     * Insert a row into a table.
657
     *
658
     * @param string $table The name of the table to insert into.
659
     * @param array $row The row of data to insert.
660
     * @param array $options An array of options for the insert.
661
     *
662
     * Db::OPTION_IGNORE
663
     * : Whether or not to ignore inserts that lead to a duplicate key. *default false*
664
     * Db::OPTION_REPLACE
665
     * : Whether or not to replace duplicate keys. *default false*
666
     * Db::OPTION_UPSERT
667
     * : Whether or not to update the existing data when duplicate keys exist.
668
     *
669
     * @return mixed Returns the id of the inserted record, **true** if the table doesn't have an auto increment, or **false** otherwise.
670
     * @see Db::load()
671
     */
672
    abstract public function insert(string $table, array $row, array $options = []);
673
674
    /**
675
     * Load many rows into a table.
676
     *
677
     * @param string $table The name of the table to insert into.
678
     * @param \Traversable|array $rows A dataset to insert.
679
     * Note that all rows must contain the same columns.
680
     * The first row will be looked at for the structure of the insert and the rest of the rows will use this structure.
681
     * @param array $options An array of options for the inserts. See {@link Db::insert()} for details.
682
     * @see Db::insert()
683
     */
684
    public function load(string $table, $rows, array $options = []) {
685
        foreach ($rows as $row) {
686
            $this->insert($table, $row, $options);
687
        }
688
    }
689
690
691
    /**
692
     * Update a row or rows in a table.
693
     *
694
     * @param string $table The name of the table to update.
695
     * @param array $set The values to set.
696
     * @param array $where The where filter for the update.
697
     * @param array $options An array of options for the update.
698
     * @return int Returns the number of affected rows.
699
     */
700
    abstract public function update(string $table, array $set, array $where, array $options = []): int;
701
702
    /**
703
     * Delete rows from a table.
704
     *
705
     * @param string $table The name of the table to delete from.
706
     * @param array $where The where filter of the delete.
707
     * @param array $options An array of options.
708
     *
709
     * Db:OPTION_TRUNCATE
710
     * : Truncate the table instead of deleting rows. In this case {@link $where} must be blank.
711
     * @return int Returns the number of affected rows.
712
     */
713
    abstract public function delete(string $table, array $where, array $options = []): int;
714
715
    /**
716
     * Reset the internal table definition cache.
717
     *
718
     * @return $this
719
     */
720 10
    public function reset() {
721 10
        $this->tables = [];
722 10
        $this->tableNames = null;
723 10
        return $this;
724
    }
725
726
    /**
727
     * Build a standardized index name from an index definition.
728
     *
729
     * @param string $tableName The name of the table the index is in.
730
     * @param array $indexDef The index definition.
731
     * @return string Returns the index name.
732
     */
733 68
    protected function buildIndexName(string $tableName, array $indexDef): string {
734 68
        $indexDef += ['type' => Db::INDEX_IX, 'suffix' => ''];
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
735
736 68
        $type = $indexDef['type'];
737
738 68
        if ($type === Db::INDEX_PK) {
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
739 33
            return 'primary';
740
        }
741 58
        $px = self::val($type, [Db::INDEX_IX => 'ix_', Db::INDEX_UNIQUE => 'ux_'], 'ix_');
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
742 58
        $sx = $indexDef['suffix'];
743 58
        $result = $px.$tableName.'_'.($sx ?: implode('', $indexDef['columns']));
744 58
        return $result;
745
    }
746
747
    /**
748
     * Execute a query that fetches data.
749
     *
750
     * @param string $sql The query to execute.
751
     * @param array $params Input parameters for the query.
752
     * @param array $options Additional options.
753
     * @return \PDOStatement Returns the result of the query.
754
     * @throws \PDOException Throws an exception if something went wrong during the query.
755
     */
756 97
    protected function query(string $sql, array $params = [], array $options = []): \PDOStatement {
757
        $options += [
758 97
            Db::OPTION_FETCH_MODE => $this->getFetchArgs()
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
759
        ];
760
761 97
        $stm = $this->getPDO()->prepare($sql);
762
763
764 97
        if ($options[Db::OPTION_FETCH_MODE]) {
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
765 84
            $stm->setFetchMode(...(array)$options[Db::OPTION_FETCH_MODE]);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
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

765
            $stm->setFetchMode(/** @scrutinizer ignore-type */ ...(array)$options[Db::OPTION_FETCH_MODE]);
Loading history...
766
        }
767
768 97
        $r = $stm->execute($params);
769
770
        // This is a kludge for those that don't have errors turning into exceptions.
771 97
        if ($r === false) {
772
            list($state, $code, $msg) = $stm->errorInfo();
773
            throw new \PDOException($msg, $code);
774
        }
775
776 97
        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...
777
    }
778
779
    /**
780
     * Query the database and return a row count.
781
     *
782
     * @param string $sql The query to execute.
783
     * @param array $params Input parameters for the query.
784
     * @param array $options Additional options.
785
     * @return int
786
     */
787 40
    protected function queryModify(string $sql, array $params = [], array $options = []): int {
788 40
        $options += [Db::OPTION_FETCH_MODE => 0];
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
789 40
        $stm = $this->query($sql, $params, $options);
790 40
        return $stm->rowCount();
791
    }
792
793
    /**
794
     * Query the database and return the ID of the record that was inserted.
795
     *
796
     * @param string $sql The query to execute.
797
     * @param array $params Input parameters for the query.
798
     * @param array $options Additional options.
799
     * @return mixed Returns the record ID.
800
     */
801 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...
802 23
        $options += [Db::OPTION_FETCH_MODE => 0];
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
803 23
        $this->query($sql, $params, $options);
804 23
        $r = $this->getPDO()->lastInsertId();
805
806 23
        return is_numeric($r) ? (int)$r : $r;
807
    }
808
809
    /**
810
     * Query the database for a database define.
811
     *
812
     * @param string $sql The query to execute.
813
     * @param array $options Additional options.
814
     */
815 38
    protected function queryDefine(string $sql, array $options = []) {
816 38
        $options += [Db::OPTION_FETCH_MODE => 0];
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

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