Completed
Push — master ( cf8dce...10f953 )
by Todd
04:47
created

Db::fetchColumnDefs()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 15
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 5.2

Importance

Changes 0
Metric Value
dl 0
loc 15
ccs 8
cts 10
cp 0.8
rs 8.8571
c 0
b 0
f 0
cc 5
eloc 10
nc 5
nop 1
crap 5.2
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 = []);
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...
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 = []);
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...
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 8
    final public function dropTable(string $table, array $options = []) {
186 8
        $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 8
        $this->dropTableDb($table, $options);
188
189 8
        $tableKey = strtolower($table);
190 8
        unset($this->tables[$tableKey], $this->tableNames[$tableKey]);
191 8
    }
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 = []);
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...
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 68
    final public function fetchTableDef(string $table) {
238 68
        $tableKey = strtolower($table);
239
240
        // First check the table cache.
241 68
        if (isset($this->tables[$tableKey])) {
242 53
            $tableDef = $this->tables[$tableKey];
243
244 53
            if (isset($tableDef['columns'], $tableDef['indexes'])) {
245 53
                return $tableDef;
246
            }
247 32
        } elseif ($this->tableNames !== null && !isset($this->tableNames[$tableKey])) {
248 24
            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 53
    public static function typeDef(string $type) {
306
        // Check for the unsigned signifier.
307 53
        $unsigned = null;
308 53
        if ($type[0] === 'u') {
309 6
            $unsigned = true;
310 6
            $type = substr($type, 1);
311 51
        } 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 53
        $brackets = null;
318 53
        if (preg_match('`^(.*)\((.*)\)$`', $type, $m)) {
319 37
            $brackets = $m[2];
320 37
            $type = $m[1];
321
        }
322
323
        // Look for the type.
324 53
        $type = strtolower($type);
325 53
        if (isset(self::$types[$type])) {
326 53
            $row = self::$types[$type];
327 53
            $dbtype = $type;
328
329
            // Resolve an alias.
330 53
            if (is_string($row)) {
331 2
                $dbtype = $row;
332 53
                $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 53
            'type' => $row['type'],
341 53
            'dbtype' => $dbtype
342
        ];
343
344 53
        if (!empty($row['schema'])) {
345 40
            $schema += $row['schema'];
346
        }
347
348 53
        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 53
        if (!empty($row['length'])) {
358 28
            $schema['maxLength'] = (int)$brackets ?: 255;
359
        }
360
361 53
        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 53
        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 53
        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 32
    protected static function dbType(array $typeDef) {
388 32
        $dbtype = $typeDef['dbtype'];
389
390 32
        if (!empty($typeDef['maxLength'])) {
391 17
            $dbtype .= "({$typeDef['maxLength']})";
392 29
        } elseif (!empty($typeDef['unsigned'])) {
393
            $dbtype = 'u'.$dbtype;
394 29
        } elseif (!empty($typeDef['precision'])) {
395
            $dbtype .= "({$typeDef['precision']}";
396
            if (!empty($typeDef['scale'])) {
397
                $dbtype .= ",{$typeDef['scale']}";
398
            }
399
            $dbtype .= ')';
400 29
        } 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 32
        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 68
    final public function defineTable(array $tableDef, array $options = []) {
428 68
        $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 68
        $tableName = $tableDef['name'];
431 68
        $tableKey = strtolower($tableName);
432 68
        $tableDef['name'] = $tableName;
433 68
        $curTable = $this->fetchTableDef($tableName);
434
435 68
        $this->fixIndexes($tableName, $tableDef, $curTable);
436
437 68
        if (!$curTable) {
438 32
            $this->createTableDb($tableDef, $options);
439 32
            $this->tables[$tableKey] = $tableDef;
440 32
            $this->tableNames[$tableKey] = $tableDef['name'];
441 32
            return;
442
        }
443
        // This is the alter statement.
444 49
        $alterDef = ['name' => $tableName];
445
446
        // Figure out the columns that have changed.
447 49
        $curColumns = (array)$curTable['columns'];
448 49
        $newColumns = (array)$tableDef['columns'];
449
450 49
        $alterDef['add']['columns'] = array_diff_key($newColumns, $curColumns);
451 49
        $alterDef['alter']['columns'] = array_uintersect_assoc($newColumns, $curColumns, function ($new, $curr) {
452 49
            $search = ['dbtype', 'allowNull', 'default'];
453 49
            foreach ($search as $key) {
454 49
                if (self::val($key, $curr) !== self::val($key, $new)) {
455
                    // Return 0 if the values are different, not the same.
456 49
                    return 0;
457
                }
458
            }
459
460
            // Enum checking.
461 49
            if (isset($curr['enum']) xor isset($new['enum'])) {
462
                return 0;
463 49
            } elseif (isset($curr['enum']) && isset($new['enum'])
464
                && (
465 2
                    count($curr['enum']) !== count($new['enum'])
466 49
                    || !empty(array_diff($curr['enum'], $new['enum']))
467
                )
468
            ) {
469 2
                return 0;
470
            }
471
472 47
            return 1;
473 49
        });
474
475
        // Figure out the indexes that have changed.
476 49
        $curIndexes = (array)self::val('indexes', $curTable, []);
477 49
        $newIndexes = (array)self::val('indexes', $tableDef, []);
478
479 49
        $alterDef['add']['indexes'] = array_udiff($newIndexes, $curIndexes, [$this, 'indexCompare']);
480
481 49
        $dropIndexes = array_udiff($curIndexes, $newIndexes, [$this, 'indexCompare']);
482 49
        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 47
            $alterDef['drop']['columns'] = [];
487 47
            $alterDef['drop']['indexes'] = [];
488
489
            // If the primary key has changed then the old one needs to be dropped.
490 47
            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 49
        if (empty($alterDef['add']['columns']) && empty($alterDef['add']['indexes']) &&
497 49
            empty($alterDef['drop']['columns']) && empty($alterDef['drop']['indexes']) &&
498 49
            empty($alterDef['alter']['columns'])
499
        ) {
500 37
            return;
501
        }
502
503 12
        $alterDef['def'] = $tableDef;
504
505
        // Alter the table.
506 12
        $this->alterTableDb($alterDef, $options);
507
508
        // Update the cached schema.
509 12
        $tableDef['name'] = $tableName;
510 12
        $this->tables[$tableKey] = $tableDef;
511
512 12
        if ($this->tableNames === null) {
513 2
            $this->fetchTableNames();
514
        }
515
516 12
        $this->tableNames[$tableKey] = $tableName;
517 12
    }
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 55
    protected function findPrimaryKeyIndex(array $indexes) {
526 55
        foreach ($indexes as $index) {
527 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...
528 13
                return $index;
529
            }
530
        }
531 45
        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 68
    private function fixIndexes(string $tableName, array &$tableDef, $curTableDef = null) {
544 68
        $tableDef += ['indexes' => []];
545
546
        // Loop through the columns and add the primary key index.
547 68
        $primaryColumns = [];
548 68
        foreach ($tableDef['columns'] as $cname => $cdef) {
549 68
            if (!empty($cdef['primary'])) {
550 68
                $primaryColumns[] = $cname;
551
            }
552
        }
553
554
        // Massage the primary key index.
555 68
        $primaryFound = false;
556 68
        foreach ($tableDef['indexes'] as &$indexDef) {
557 64
            $indexDef += ['name' => $this->buildIndexName($tableName, $indexDef), 'type' => null];
558
559 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...
560 28
                $primaryFound = true;
561
562 28
                if (empty($primaryColumns)) {
563 8
                    foreach ($indexDef['columns'] as $cname) {
564 8
                        $tableDef['columns'][$cname]['primary'] = true;
565
                    }
566 22
                } elseif (array_diff($primaryColumns, $indexDef['columns'])) {
567 28
                    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 64
                        break;
576
                    }
577
                }
578
            }
579
        }
580
581 68
        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 68
    }
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 45
    private function indexCompare(array $a, array $b): int {
616 45
        if ($a['columns'] > $b['columns']) {
617 15
            return 1;
618 45
        } elseif ($a['columns'] < $b['columns']) {
619 15
            return -1;
620
        }
621
622 41
        return strcmp(
623 41
            isset($a['type']) ? $a['type'] : '',
624 41
            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 64
    protected function buildIndexName(string $tableName, array $indexDef): string {
734 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...
735
736 64
        $type = $indexDef['type'];
737
738 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...
739 29
            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 91
    protected function query(string $sql, array $params = [], array $options = []): \PDOStatement {
757
        $options += [
758 91
            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 91
        $stm = $this->getPDO()->prepare($sql);
762
763
764 91
        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...
766
        }
767
768 91
        $r = $stm->execute($params);
769
770
        // This is a kludge for those that don't have errors turning into exceptions.
771 91
        if ($r === false) {
772
            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...
773
            throw new \PDOException($msg, $code);
774
        }
775
776 91
        return $stm;
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 38
    protected function queryModify(string $sql, array $params = [], array $options = []): int {
788 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...
789 38
        $stm = $this->query($sql, $params, $options);
790 38
        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 19
    protected function queryID(string $sql, array $params = [], array $options = []) {
802 19
        $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 19
        $this->query($sql, $params, $options);
804 19
        $r = $this->getPDO()->lastInsertId();
805
806 19
        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 32
    protected function queryDefine(string $sql, array $options = []) {
816 32
        $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 32
        $this->query($sql, [], $options);
818 32
    }
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 86
    protected static function val($key, $array, $default = null) {
837 86
        if (is_array($array)) {
838
            // isset() is a micro-optimization - it is fast but fails for null values.
839 86
            if (isset($array[$key])) {
840 79
                return $array[$key];
841
            }
842
843
            // Comparing $default is also a micro-optimization.
844 86
            if ($default === null || array_key_exists($key, $array)) {
845 86
                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 95
    public function escape($identifier): string {
867 95
        if ($identifier instanceof Literal) {
868 2
            return $identifier->getValue($this);
869
        }
870 95
        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 95
    protected function prefixTable($table, bool $escape = true): string {
891 95
        if ($table instanceof Identifier) {
892 8
            return $escape ? $table->escape($this) : (string)$table;
893
        } else {
894 95
            $table = $this->px.$table;
895 95
            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 43
    public function quote($value, string $column = ''): string {
922 43
        if ($value instanceof Literal) {
923
            /* @var Literal $value */
924 25
            return $value->getValue($this, $column);
925
        } else {
926 38
            return $this->getPDO()->quote($value);
927
        }
928
    }
929
930
    /**
931
     * Gets the {@link PDO} object for this connection.
932
     *
933
     * @return \PDO
934
     */
935 95
    public function getPDO(): PDO {
936 95
        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) {
946
        $this->pdo = $pdo;
947
        return $this;
948
    }
949
}
950