Completed
Push — master ( 72aa74...afd9e7 )
by Todd
02:20
created

Db   F

Complexity

Total Complexity 113

Size/Duplication

Total Lines 915
Duplicated Lines 0 %

Coupling/Cohesion

Components 4
Dependencies 3

Test Coverage

Coverage 79.27%

Importance

Changes 0
Metric Value
wmc 113
c 0
b 0
f 0
lcom 4
cbo 3
dl 0
loc 915
ccs 218
cts 275
cp 0.7927
rs 1.263

41 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 7 2
A driverClass() 0 10 3
createTableDb() 0 1 ?
alterTableDb() 0 1 ?
A dropTable() 0 7 1
dropTableDb() 0 1 ?
A fetchTableNames() 0 15 3
fetchTableNamesDb() 0 1 ?
B fetchTableDef() 0 22 6
fetchTableDefDb() 0 1 ?
B fetchColumnDefs() 0 15 5
fetchColumnDefsDb() 0 1 ?
F typeDef() 0 73 15
B dbType() 0 21 6
nativeDbType() 0 1 ?
C defineTable() 0 73 11
A findPrimaryKeyIndex() 0 8 3
A getPx() 0 3 1
A setPx() 0 3 1
B indexCompare() 0 12 5
get() 0 1 ?
A queryDefine() 0 4 1
C val() 0 23 9
A escapeLike() 0 3 1
A prefixTable() 0 8 4
A stripPrefix() 0 7 2
A quote() 0 8 2
A getPDO() 0 3 1
A setPDO() 0 4 1
C fixIndexes() 0 45 14
A getOne() 0 6 2
insert() 0 1 ?
update() 0 1 ?
delete() 0 1 ?
A reset() 0 5 1
A buildIndexName() 0 13 3
A queryModify() 0 5 1
A queryID() 0 7 2
A escape() 0 6 2
A load() 0 5 2
A query() 0 22 3

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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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
12
/**
13
 * Defines a standard set of methods that all database drivers must conform to.
14
 */
15
abstract class Db {
16
    use Utils\FetchModeTrait;
17
18
    const QUERY_DEFINE = 'define';
19
    const QUERY_READ = 'read';
20
    const QUERY_WRITE = 'write';
21
22
    const INDEX_PK = 'primary';
23
    const INDEX_IX = 'index';
24
    const INDEX_UNIQUE = 'unique';
25
26
    const OPTION_REPLACE = 'replace';
27
    const OPTION_IGNORE = 'ignore';
28
    const OPTION_UPSERT = 'upsert';
29
    const OPTION_TRUNCATE = 'truncate';
30
    const OPTION_DROP = 'drop';
31
    const OPTION_FETCH_MODE = 'fetchMode';
32
33
    const OP_EQ = '=';
34
    const OP_GT = '>';
35
    const OP_GTE = '>=';
36
    const OP_IN = '$in';
37
    const OP_LIKE = '$like';
38
    const OP_LT = '<';
39
    const OP_LTE = '<=';
40
    const OP_NEQ = '<>';
41
42
    const OP_AND = '$and';
43
    const OP_OR = '$or';
44
45
    /**
46
     * @var string[] Maps PDO drivers to db classes.
47
     */
48
    private static $drivers = [
49
        'mysql' => MySqlDb::class,
50
        'sqlite' => SqliteDb::class
51
    ];
52
53
    /**
54
     * @var array The canonical database types.
55
     */
56
    private static $types = [
57
        // String
58
        'char' => ['type' => 'string', 'length' => true],
59
        'varchar' => ['type' => 'string', 'length' => true],
60
        'tinytext' => ['type' => 'string', 'schema' => ['maxLength' => 255]],
61
        'text' => ['type' => 'string', 'schema' => ['maxLength' =>  65535]],
62
        'mediumtext' => ['type' => 'string', 'schema' => ['maxLength' => 16777215]],
63
        'longtext' => ['type' => 'string', 'schema' => ['maxLength' => 4294967295]],
64
        'binary' => ['type' => 'string', 'length' => true],
65
        'varbinary' => ['type' => 'string', 'length' => true],
66
67
        // Boolean
68
        'bool' => ['type' => 'boolean'],
69
70
        // Integer
71
        'byte' => ['type' => 'integer', 'schema' => ['maximum' => 127, 'minimum' => -128]],
72
        'short' => ['type' => 'integer', 'schema' => ['maximum' => 32767, 'minimum' => -32768]],
73
        'int' => ['type' => 'integer', 'schema' => ['maximum' => 2147483647, 'minimum' => -2147483648]],
74
        'long' => ['type' => 'integer'],
75
76
        // Number
77
        'float' => ['type' => 'number'],
78
        'double' => ['type' => 'number'],
79
        'decimal' => ['type' => 'number', 'precision' => true],
80
        'numeric' => ['type' => 'number', 'precision' => true],
81
82
        // Date/Time
83
        'datetime' => ['type' => 'datetime'],
84
        'timestamp' => ['type' => 'datetime'],
85
86
        // Enum
87
        'enum' => ['type' => 'string', 'enum' => true],
88
89
        // Schema types
90
        'string' => 'varchar',
91
        'boolean' => 'bool',
92
        'integer' => 'int',
93
        'number' => 'float',
94
95
        // Other aliases
96
        'character' => 'char',
97
        'tinyint' => 'byte',
98
        'int8' => 'byte',
99
        'smallint' => 'short',
100
        'int16' => 'short',
101
        'int32' => 'int',
102
        'bigint' => 'long',
103
        'int64' => 'long',
104
        'real' => 'double'
105
    ];
106
107
    /**
108
     * @var string The database prefix.
109
     */
110
    private $px = '';
111
112
    /**
113
     * @var array A cached copy of the table schemas indexed by lowercase name.
114
     */
115
    private $tables = [];
116
117
    /**
118
     * @var array|null A cached copy of the table names indexed by lowercase name.
119
     */
120
    private $tableNames = null;
121
122
    /**
123
     * @var \PDO
124
     */
125
    private $pdo;
126
127
    /**
128
     * Initialize an instance of the {@link MySqlDb} class.
129
     *
130
     * @param PDO $pdo The connection to the database.
131
     * @param string $px The database prefix.
132
     */
133
    public function __construct(PDO $pdo, $px = '') {
134
        $this->pdo = $pdo;
135
        $this->px = $px;
136
137
        $fetchMode = $this->pdo->getAttribute(PDO::ATTR_DEFAULT_FETCH_MODE);
138
        $this->setFetchMode(in_array($fetchMode, [0, PDO::FETCH_BOTH], true) ? PDO::FETCH_ASSOC: $fetchMode);
139
    }
140
141
    /**
142
     * Get the name of the class that handles a database driver.
143
     *
144
     * @param string|PDO $driver The name of the driver or a database connection.
145
     * @return null|string Returns the driver classname or **null** if one isn't found.
146
     */
147
    public static function driverClass($driver) {
148
        if ($driver instanceof PDO) {
149
            $name = $driver->getAttribute(PDO::ATTR_DRIVER_NAME);
150
        } else {
151
            $name = (string)$driver;
152
        }
153
154
        $name = strtolower($name);
155
        return isset(self::$drivers[$name]) ? self::$drivers[$name] : null;
156
    }
157
158
    /**
159
     * Add a table to the database.
160
     *
161
     * @param array $tableDef The table definition.
162
     * @param array $options An array of additional options when adding the table.
163
     */
164
    abstract protected function createTableDb(array $tableDef, array $options = []);
0 ignored issues
show
Documentation introduced by
For interfaces and abstract methods it is generally a good practice to add a @return annotation even if it is just @return void or @return null, so that implementors know what to do in the overridden method.

For interface and abstract methods, it is impossible to infer the return type from the immediate code. In these cases, it is generally advisible to explicitly annotate these methods with a @return doc comment to communicate to implementors of these methods what they are expected to return.

Loading history...
165
166
    /**
167
     * Alter a table in the database.
168
     *
169
     * When altering a table you pass an array with three optional keys: add, drop, and alter.
170
     * Each value is consists of a table definition in a format that would be passed to {@link Db::setTableDef()}.
171
     *
172
     * @param array $alterDef The alter definition.
173
     * @param array $options An array of additional options when adding the table.
174
     */
175
    abstract protected function alterTableDb(array $alterDef, array $options = []);
0 ignored issues
show
Documentation introduced by
For interfaces and abstract methods it is generally a good practice to add a @return annotation even if it is just @return void or @return null, so that implementors know what to do in the overridden method.

For interface and abstract methods, it is impossible to infer the return type from the immediate code. In these cases, it is generally advisible to explicitly annotate these methods with a @return doc comment to communicate to implementors of these methods what they are expected to return.

Loading history...
176
177
    /**
178
     * Drop a table.
179
     *
180
     * @param string $table The name of the table to drop.
181
     * @param array $options An array of additional options when adding the table.
182
     */
183 6
    final public function dropTable($table, array $options = []) {
184 6
        $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...
185 6
        $this->dropTableDb($table, $options);
186
187 6
        $tableKey = strtolower($table);
188 6
        unset($this->tables[$tableKey], $this->tableNames[$tableKey]);
189 6
    }
190
191
    /**
192
     * Perform the actual table drop.
193
     *
194
     * @param string $table The name of the table to drop.
195
     * @param array $options An array of additional options when adding the table.
196
     */
197
    abstract protected function dropTableDb($table, array $options = []);
0 ignored issues
show
Documentation introduced by
For interfaces and abstract methods it is generally a good practice to add a @return annotation even if it is just @return void or @return null, so that implementors know what to do in the overridden method.

For interface and abstract methods, it is impossible to infer the return type from the immediate code. In these cases, it is generally advisible to explicitly annotate these methods with a @return doc comment to communicate to implementors of these methods what they are expected to return.

Loading history...
198
199
    /**
200
     * Get the names of all the tables in the database.
201
     *
202
     * @return string[] Returns an array of table names without prefixes.
203
     */
204
    final public function fetchTableNames() {
205
        if ($this->tableNames !== null) {
206
            return array_values($this->tableNames);
207
        }
208
209
        $names = $this->fetchTableNamesDb();
210
211
        $this->tableNames = [];
212
        foreach ($names as $name) {
213
            $name = $this->stripPrefix($name);
214
            $this->tableNames[strtolower($name)] = $name;
215
        }
216
217
        return array_values($this->tableNames);
218
    }
219
220
    /**
221
     * Fetch the table names from the underlying database layer.
222
     *
223
     * The driver should return all table names. It doesn't have to strip the prefix.
224
     *
225
     * @return string[]
226
     */
227
    abstract protected function fetchTableNamesDb();
228
229
    /**
230
     * Get a table definition.
231
     *
232
     * @param string $table The name of the table.
233
     * @return array|null Returns the table definition or null if the table does not exist.
234
     */
235 64
    final public function fetchTableDef($table) {
236 64
        $tableKey = strtolower($table);
237
238
        // First check the table cache.
239 64
        if (isset($this->tables[$tableKey])) {
240 50
            $tableDef = $this->tables[$tableKey];
241
242 50
            if (isset($tableDef['columns'], $tableDef['indexes'])) {
243 50
                return $tableDef;
244
            }
245 28
        } elseif ($this->tableNames !== null && !isset($this->tableNames[$tableKey])) {
246 22
            return null;
247
        }
248
249 8
        $tableDef = $this->fetchTableDefDb($table);
250 8
        if ($tableDef !== null) {
251 8
            $this->fixIndexes($tableDef['name'], $tableDef);
252 8
            $this->tables[$tableKey] = $tableDef;
253
        }
254
255 8
        return $tableDef;
256
    }
257
258
    /**
259
     * Fetch the table definition from the database.
260
     *
261
     * @param string $table The name of the table to get.
262
     * @return array|null Returns the table def or **null** if the table doesn't exist.
263
     */
264
    abstract protected function fetchTableDefDb($table);
265
266
267
    /**
268
     * Get the column definitions for a table.
269
     *
270
     * @param string $table The name of the table to get the columns for.
271
     * @return array|null Returns an array of column definitions.
272
     */
273 1
    final public function fetchColumnDefs($table) {
274 1
        $tableKey = strtolower($table);
275
276 1
        if (!empty($this->tables[$tableKey]['columns'])) {
277 1
            $this->tables[$tableKey]['columns'];
278
        } elseif ($this->tableNames !== null && !isset($this->tableNames[$tableKey])) {
279
            return null;
280
        }
281
282 1
        $columnDefs = $this->fetchColumnDefsDb($table);
283 1
        if ($columnDefs !== null) {
284 1
            $this->tables[$tableKey]['columns'] = $columnDefs;
285
        }
286 1
        return $columnDefs;
287
    }
288
289
    /**
290
     * Get the column definitions from the database.
291
     *
292
     * @param string $table The name of the table to fetch the columns for.
293
     * @return array|null
294
     */
295
    abstract protected function fetchColumnDefsDb($table);
296
297
    /**
298
     * Get the canonical type based on a type string.
299
     *
300
     * @param string $type A type string.
301
     * @return array|null Returns the type schema array or **null** if a type isn't found.
302
     */
303 49
    public static function typeDef($type) {
304
        // Check for the unsigned signifier.
305 49
        $unsigned = null;
306 49
        if ($type[0] === 'u') {
307 6
            $unsigned = true;
308 6
            $type = substr($type, 1);
309 47
        } elseif (preg_match('`(.+)\s+unsigned`i', $type, $m)) {
310 2
            $unsigned = true;
311 2
            $type = $m[1];
312
        }
313
314
        // Remove brackets from the type.
315 49
        $brackets = null;
316 49
        if (preg_match('`^(.*)\((.*)\)$`', $type, $m)) {
317 33
            $brackets = $m[2];
318 33
            $type = $m[1];
319
        }
320
321
        // Look for the type.
322 49
        $type = strtolower($type);
323 49
        if (isset(self::$types[$type])) {
324 49
            $row = self::$types[$type];
325 49
            $dbtype = $type;
326
327
            // Resolve an alias.
328 49
            if (is_string($row)) {
329 2
                $dbtype = $row;
330 49
                $row = self::$types[$row];
331
            }
332
        } else {
333
            return null;
334
        }
335
336
        // Now that we have a type row we can build a schema for it.
337
        $schema = [
338 49
            'type' => $row['type'],
339 49
            'dbtype' => $dbtype
340
        ];
341
342 49
        if (!empty($row['schema'])) {
343 40
            $schema += $row['schema'];
344
        }
345
346 49
        if ($row['type'] === 'integer' && $unsigned) {
347 6
            $schema['unsigned'] = true;
348
349 6
            if (!empty($schema['maximum'])) {
350 6
                $schema['maximum'] = $schema['maximum'] * 2 + 1;
351 6
                $schema['minimum'] = 0;
352
            }
353
        }
354
355 49
        if (!empty($row['length'])) {
356 26
            $schema['maxLength'] = (int)$brackets ?: 255;
357
        }
358
359 49
        if (!empty($row['precision'])) {
360 2
            $parts = array_map('trim', explode(',', $brackets));
361 2
            $schema['precision'] = (int)$parts[0];
362 2
            if (isset($parts[1])) {
363 2
                $schema['scale'] = (int)$parts[1];
364
            }
365
        }
366
367 49
        if (!empty($row['enum'])) {
368 1
            $enum = explode(',', $brackets);
369
            $schema['enum'] = array_map(function ($str) {
370 1
                return trim($str, "'\" \t\n\r\0\x0B");
371 1
            }, $enum);
372
        }
373
374 49
        return $schema;
375
    }
376
377
    /**
378
     * Get the database type string from a type definition.
379
     *
380
     * This is the opposite of {@link Db::typeDef()}.
381
     *
382
     * @param array $typeDef The type definition array.
383
     * @return string Returns a db type string.
384
     */
385 28
    protected static function dbType(array $typeDef) {
386 28
        $dbtype = $typeDef['dbtype'];
387
388 28
        if (!empty($typeDef['maxLength'])) {
389 14
            $dbtype .= "({$typeDef['maxLength']})";
390 28
        } elseif (!empty($typeDef['unsigned'])) {
391
            $dbtype = 'u'.$dbtype;
392 28
        } elseif (!empty($typeDef['precision'])) {
393
            $dbtype .= "({$typeDef['precision']}";
394
            if (!empty($typeDef['scale'])) {
395
                $dbtype .= ",{$typeDef['scale']}";
396
            }
397
            $dbtype .= ')';
398 28
        } elseif (!empty($typeDef['enum'])) {
399
            $parts = array_map(function ($str) {
400
                return "'{$str}'";
401
            }, $typeDef['enum']);
402
            $dbtype .= '('.implode(',', $parts).')';
403
        }
404 28
        return $dbtype;
405
    }
406
407
408
    /**
409
     * Get the native database type based on a type schema.
410
     *
411
     * The default implementation of this method returns the canonical db types. Individual database classes will have
412
     * to override to provide any differences.
413
     *
414
     * @param array $type The type schema.
415
     * @return string
416
     */
417
    abstract protected function nativeDbType(array $type);
418
419
    /**
420
     * Set a table definition to the database.
421
     *
422
     * @param array $tableDef The table definition.
423
     * @param array $options An array of additional options when adding the table.
424
     */
425 64
    final public function defineTable(array $tableDef, array $options = []) {
426 64
        $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...
427
428 64
        $tableName = $tableDef['name'];
429 64
        $tableKey = strtolower($tableName);
430 64
        $tableDef['name'] = $tableName;
431 64
        $curTable = $this->fetchTableDef($tableName);
432
433 64
        $this->fixIndexes($tableName, $tableDef, $curTable);
434
435 64
        if (!$curTable) {
436 28
            $this->createTableDb($tableDef, $options);
437 28
            $this->tables[$tableKey] = $tableDef;
438 28
            $this->tableNames[$tableKey] = $tableDef['name'];
439 28
            return;
440
        }
441
        // This is the alter statement.
442 45
        $alterDef = ['name' => $tableName];
443
444
        // Figure out the columns that have changed.
445 45
        $curColumns = (array)$curTable['columns'];
446 45
        $newColumns = (array)$tableDef['columns'];
447
448 45
        $alterDef['add']['columns'] = array_diff_key($newColumns, $curColumns);
449 45
        $alterDef['alter']['columns'] = array_uintersect_assoc($newColumns, $curColumns, function ($new, $curr) {
450 45
            $search = ['dbtype', 'allowNull', 'default'];
451 45
            foreach ($search as $key) {
452 45
                if (self::val($key, $curr) !== self::val($key, $new)) {
453
                    // Return 0 if the values are different, not the same.
454 45
                    return 0;
455
                }
456
            }
457 45
            return 1;
458 45
        });
459
460
        // Figure out the indexes that have changed.
461 45
        $curIndexes = (array)self::val('indexes', $curTable, []);
462 45
        $newIndexes = (array)self::val('indexes', $tableDef, []);
463
464 45
        $alterDef['add']['indexes'] = array_udiff($newIndexes, $curIndexes, [$this, 'indexCompare']);
465
466 45
        $dropIndexes = array_udiff($curIndexes, $newIndexes, [$this, 'indexCompare']);
467 45
        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...
468 2
            $alterDef['drop']['columns'] = array_diff_key($curColumns, $newColumns);
469 2
            $alterDef['drop']['indexes'] = $dropIndexes;
470
        } else {
471 43
            $alterDef['drop']['columns'] = [];
472 43
            $alterDef['drop']['indexes'] = [];
473
474
            // If the primary key has changed then the old one needs to be dropped.
475 43
            if ($pk = $this->findPrimaryKeyIndex($dropIndexes)) {
476 4
                $alterDef['drop']['indexes'][] = $pk;
477
            }
478
        }
479
480
        // Check to see if any alterations at all need to be made.
481 45
        if (empty($alterDef['add']['columns']) && empty($alterDef['add']['indexes']) &&
482 45
            empty($alterDef['drop']['columns']) && empty($alterDef['drop']['indexes']) &&
483 45
            empty($alterDef['alter']['columns'])
484
        ) {
485 37
            return;
486
        }
487
488 8
        $alterDef['def'] = $tableDef;
489
490
        // Alter the table.
491 8
        $this->alterTableDb($alterDef, $options);
492
493
        // Update the cached schema.
494 8
        $tableDef['name'] = $tableName;
495 8
        $this->tables[$tableKey] = $tableDef;
496 8
        $this->tableNames[$tableKey] = $tableName;
497 8
    }
498
499
    /**
500
     * Find the primary key in an array of indexes.
501
     *
502
     * @param array $indexes The indexes to search.
503
     * @return array|null Returns the primary key or **null** if there isn't one.
504
     */
505 51
    protected function findPrimaryKeyIndex(array $indexes) {
506 51
        foreach ($indexes as $index) {
507 13
            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...
508 13
                return $index;
509
            }
510
        }
511 41
        return null;
512
    }
513
514
    /**
515
     * Move the primary key index into the correct place for database drivers.
516
     *
517
     * @param string $tableName The name of the table.
518
     * @param array &$tableDef The table definition.
519
     * @param array|null $curTableDef The current database table def used to resolve conflicts in some names.
520
     * @throws \Exception Throws an exception when there is a mismatch between the primary index and the primary key
521
     * defined on the columns themselves.
522
     */
523 64
    private function fixIndexes($tableName, array &$tableDef, $curTableDef = null) {
524 64
        $tableDef += ['indexes' => []];
525
526
        // Loop through the columns and add the primary key index.
527 64
        $primaryColumns = [];
528 64
        foreach ($tableDef['columns'] as $cname => $cdef) {
529 64
            if (!empty($cdef['primary'])) {
530 64
                $primaryColumns[] = $cname;
531
            }
532
        }
533
534
        // Massage the primary key index.
535 64
        $primaryFound = false;
536 64
        foreach ($tableDef['indexes'] as &$indexDef) {
537 64
            $indexDef += ['name' => $this->buildIndexName($tableName, $indexDef), 'type' => null];
538
539 64
            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...
540 28
                $primaryFound = true;
541
542 28
                if (empty($primaryColumns)) {
543 8
                    foreach ($indexDef['columns'] as $cname) {
544 8
                        $tableDef['columns'][$cname]['primary'] = true;
545
                    }
546 22
                } elseif (array_diff($primaryColumns, $indexDef['columns'])) {
547 28
                    throw new \Exception("There is a mismatch in the primary key index and primary key columns.", 500);
548
                }
549 58
            } elseif (isset($curTableDef['indexes'])) {
550 41
                foreach ($curTableDef['indexes'] as $curIndexDef) {
551 41
                    if ($this->indexCompare($indexDef, $curIndexDef) === 0) {
552 41
                        if (!empty($curIndexDef['name'])) {
553 41
                            $indexDef['name'] = $curIndexDef['name'];
554
                        }
555 64
                        break;
556
                    }
557
                }
558
            }
559
        }
560
561 64
        if (!$primaryFound && !empty($primaryColumns)) {
562 4
            $tableDef['indexes'][] = [
563 4
                'columns' => $primaryColumns,
564
                '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...
565
            ];
566
        }
567 64
    }
568
569
    /**
570
     * Get the database prefix.
571
     *
572
     * @return string Returns the current db prefix.
573
     */
574
    public function getPx() {
575
        return $this->px;
576
    }
577
578
    /**
579
     * Set the database prefix.
580
     *
581
     * @param string $px The new database prefix.
582
     */
583
    public function setPx($px) {
584
        $this->px = $px;
585
    }
586
587
    /**
588
     * Compare two index definitions to see if they have the same columns and same type.
589
     *
590
     * @param array $a The first index.
591
     * @param array $b The second index.
592
     * @return int Returns an integer less than, equal to, or greater than zero if {@link $a} is
593
     * considered to be respectively less than, equal to, or greater than {@link $b}.
594
     */
595 45
    private function indexCompare(array $a, array $b) {
596 45
        if ($a['columns'] > $b['columns']) {
597 15
            return 1;
598 45
        } elseif ($a['columns'] < $b['columns']) {
599 15
            return -1;
600
        }
601
602 41
        return strcmp(
603 41
            isset($a['type']) ? $a['type'] : '',
604 41
            isset($b['type']) ? $b['type'] : ''
605
        );
606
    }
607
608
    /**
609
     * Get data from the database.
610
     *
611
     * @param string|Identifier $table The name of the table to get the data from.
612
     * @param array $where An array of where conditions.
613
     * @param array $options An array of additional options.
614
     * @return \PDOStatement Returns the result set.
615
     */
616
    abstract public function get($table, array $where, array $options = []);
617
618
    /**
619
     * Get a single row from the database.
620
     *
621
     * This is a convenience method that calls {@link Db::get()} and shifts off the first row.
622
     *
623
     * @param string|Identifier $table The name of the table to get the data from.
624
     * @param array $where An array of where conditions.
625
     * @param array $options An array of additional options.
626
     * @return array|object|null Returns the row or false if there is no row.
627
     */
628 16
    final public function getOne($table, array $where, array $options = []) {
629 16
        $rows = $this->get($table, $where, $options);
630 16
        $row = $rows->fetch();
631
632 16
        return $row === false ? null : $row;
633
    }
634
635
    /**
636
     * Insert a row into a table.
637
     *
638
     * @param string $table The name of the table to insert into.
639
     * @param array $row The row of data to insert.
640
     * @param array $options An array of options for the insert.
641
     *
642
     * Db::OPTION_IGNORE
643
     * : Whether or not to ignore inserts that lead to a duplicate key. *default false*
644
     * Db::OPTION_REPLACE
645
     * : Whether or not to replace duplicate keys. *default false*
646
     * Db::OPTION_UPSERT
647
     * : Whether or not to update the existing data when duplicate keys exist.
648
     *
649
     * @return mixed Returns the id of the inserted record, **true** if the table doesn't have an auto increment, or **false** otherwise.
650
     * @see Db::load()
651
     */
652
    abstract public function insert($table, array $row, array $options = []);
653
654
    /**
655
     * Load many rows into a table.
656
     *
657
     * @param string $table The name of the table to insert into.
658
     * @param \Traversable|array $rows A dataset to insert.
659
     * Note that all rows must contain the same columns.
660
     * The first row will be looked at for the structure of the insert and the rest of the rows will use this structure.
661
     * @param array $options An array of options for the inserts. See {@link Db::insert()} for details.
662
     * @see Db::insert()
663
     */
664
    public function load($table, $rows, array $options = []) {
665
        foreach ($rows as $row) {
666
            $this->insert($table, $row, $options);
667
        }
668
    }
669
670
671
    /**
672
     * Update a row or rows in a table.
673
     *
674
     * @param string $table The name of the table to update.
675
     * @param array $set The values to set.
676
     * @param array $where The where filter for the update.
677
     * @param array $options An array of options for the update.
678
     * @return int Returns the number of affected rows.
679
     */
680
    abstract public function update($table, array $set, array $where, array $options = []);
681
682
    /**
683
     * Delete rows from a table.
684
     *
685
     * @param string $table The name of the table to delete from.
686
     * @param array $where The where filter of the delete.
687
     * @param array $options An array of options.
688
     *
689
     * Db:OPTION_TRUNCATE
690
     * : Truncate the table instead of deleting rows. In this case {@link $where} must be blank.
691
     * @return int Returns the number of affected rows.
692
     */
693
    abstract public function delete($table, array $where, array $options = []);
694
695
    /**
696
     * Reset the internal table definition cache.
697
     *
698
     * @return $this
699
     */
700 8
    public function reset() {
701 8
        $this->tables = [];
702 8
        $this->tableNames = null;
703 8
        return $this;
704
    }
705
706
    /**
707
     * Build a standardized index name from an index definition.
708
     *
709
     * @param string $tableName The name of the table the index is in.
710
     * @param array $indexDef The index definition.
711
     * @return string Returns the index name.
712
     */
713 64
    protected function buildIndexName($tableName, array $indexDef) {
714 64
        $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...
715
716 64
        $type = $indexDef['type'];
717
718 64
        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...
719 29
            return 'primary';
720
        }
721 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...
722 58
        $sx = $indexDef['suffix'];
723 58
        $result = $px.$tableName.'_'.($sx ?: implode('', $indexDef['columns']));
724 58
        return $result;
725
    }
726
727
    /**
728
     * Execute a query that fetches data.
729
     *
730
     * @param string $sql The query to execute.
731
     * @param array $params Input parameters for the query.
732
     * @param array $options Additional options.
733
     * @return \PDOStatement Returns the result of the query.
734
     * @throws \PDOException Throws an exception if something went wrong during the query.
735
     */
736 87
    protected function query($sql, array $params = [], array $options = []) {
737
        $options += [
738 87
            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...
739
        ];
740
741 87
        $stm = $this->getPDO()->prepare($sql);
742
743
744 87
        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...
745 80
            $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...
746
        }
747
748 87
        $r = $stm->execute($params);
749
750
        // This is a kludge for those that don't have errors turning into exceptions.
751 87
        if ($r === false) {
752
            list($state, $code, $msg) = $stm->errorInfo();
0 ignored issues
show
Unused Code introduced by
The assignment to $state is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
753
            throw new \PDOException($msg, $code);
754
        }
755
756 87
        return $stm;
757
    }
758
759
    /**
760
     * Query the database and return a row count.
761
     *
762
     * @param string $sql The query to execute.
763
     * @param array $params Input parameters for the query.
764
     * @param array $options Additional options.
765
     * @return int
766
     */
767 38
    protected function queryModify($sql, array $params = [], array $options = []) {
768 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...
769 38
        $stm = $this->query($sql, $params, $options);
770 38
        return $stm->rowCount();
771
    }
772
773
    /**
774
     * Query the database and return the ID of the record that was inserted.
775
     *
776
     * @param string $sql The query to execute.
777
     * @param array $params Input parameters for the query.
778
     * @param array $options Additional options.
779
     * @return mixed Returns the record ID.
780
     */
781 17
    protected function queryID($sql, array $params = [], array $options = []) {
782 17
        $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...
783 17
        $this->query($sql, $params, $options);
784 17
        $r = $this->getPDO()->lastInsertId();
785
786 17
        return is_numeric($r) ? (int)$r : $r;
787
    }
788
789
    /**
790
     * Query the database for a database define.
791
     *
792
     * @param string $sql The query to execute.
793
     * @param array $options Additional options.
794
     */
795 28
    protected function queryDefine($sql, array $options = []) {
796 28
        $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...
797 28
        $this->query($sql, [], $options);
798 28
    }
799
800
    /**
801
     * Safely get a value out of an array.
802
     *
803
     * This function will always return a value even if the array key doesn't exist.
804
     * The self::val() function is one of the biggest workhorses of Vanilla and shows up a lot throughout other code.
805
     * It's much preferable to use this function if your not sure whether or not an array key exists rather than
806
     * using @ error suppression.
807
     *
808
     * This function uses optimizations found in the [facebook libphputil library](https://github.com/facebook/libphutil).
809
     *
810
     * @param string|int $key The array key.
811
     * @param array|object $array The array to get the value from.
812
     * @param mixed $default The default value to return if the key doesn't exist.
813
     * @return mixed The item from the array or `$default` if the array key doesn't exist.
814
     * @category Array Functions
815
     */
816 82
    protected static function val($key, $array, $default = null) {
817 82
        if (is_array($array)) {
818
            // isset() is a micro-optimization - it is fast but fails for null values.
819 82
            if (isset($array[$key])) {
820 75
                return $array[$key];
821
            }
822
823
            // Comparing $default is also a micro-optimization.
824 82
            if ($default === null || array_key_exists($key, $array)) {
825 82
                return null;
826
            }
827
        } elseif (is_object($array)) {
828
            if (isset($array->$key)) {
829
                return $array->$key;
830
            }
831
832
            if ($default === null || property_exists($array, $key)) {
833
                return null;
834
            }
835
        }
836
837 4
        return $default;
838
    }
839
840
    /**
841
     * Escape an identifier.
842
     *
843
     * @param string|Literal $identifier The identifier to escape.
844
     * @return string Returns the field properly escaped.
845
     */
846 91
    public function escape($identifier) {
847 91
        if ($identifier instanceof Literal) {
848 2
            return $identifier->getValue($this);
849
        }
850 91
        return '`'.str_replace('`', '``', $identifier).'`';
851
    }
852
853
    /**
854
     * Escape a a like string so that none of its characters work as wildcards.
855
     *
856
     * @param string $str The string to escape.
857
     * @return string Returns an escaped string.
858
     */
859
    protected function escapeLike($str) {
860
        return addcslashes($str, '_%');
861
    }
862
863
    /**
864
     * Prefix a table name.
865
     *
866
     * @param string|Identifier $table The name of the table to prefix.
867
     * @param bool $escape Whether or not to escape the output.
868
     * @return string Returns a full table name.
869
     */
870 91
    protected function prefixTable($table, $escape = true) {
871 91
        if ($table instanceof Identifier) {
872 5
            return $escape ? $table->escape($this) : (string)$table;
873
        } else {
874 91
            $table = $this->px.$table;
875 91
            return $escape ? $this->escape($table) : $table;
876
        }
877
    }
878
879
    /**
880
     * Strip the database prefix off a table name.
881
     *
882
     * @param string $table The name of the table to strip.
883
     * @return string Returns the table name stripped of the prefix.
884
     */
885
    protected function stripPrefix($table) {
886
        $len = strlen($this->px);
887
        if (strcasecmp(substr($table, 0, $len), $this->px) === 0) {
888
            $table = substr($table, $len);
889
        }
890
        return $table;
891
    }
892
893
    /**
894
     * Optionally quote a where value.
895
     *
896
     * @param mixed $value The value to quote.
897
     * @param string $column The column being operated on. It must already be quoted.
898
     * @return string Returns the value, optionally quoted.
899
     * @internal param bool $quote Whether or not to quote the value.
900
     */
901 41
    public function quote($value, $column = '') {
902 41
        if ($value instanceof Literal) {
903
            /* @var Literal $value */
904 25
            return $value->getValue($this, $column);
905
        } else {
906 36
            return $this->getPDO()->quote($value);
907
        }
908
    }
909
910
    /**
911
     * Gets the {@link PDO} object for this connection.
912
     *
913
     * @return \PDO
914
     */
915 91
    public function getPDO() {
916 91
        return $this->pdo;
917
    }
918
919
    /**
920
     * Set the connection to the database.
921
     *
922
     * @param PDO $pdo The new connection to the database.
923
     * @return $this
924
     */
925
    public function setPDO(PDO $pdo) {
926
        $this->pdo = $pdo;
927
        return $this;
928
    }
929
}
930