Issues (38)

src/PDODatabase.php (4 issues)

1
<?php
2
3
declare(strict_types=1);
4
/**
5
 * Database Access Layer.
6
 * @author Doug Wright
7
 */
8
9
namespace DVDoug\DB;
10
11
use function array_keys;
12
use function array_map;
13
use function ctype_digit;
14
use function implode;
15
use function in_array;
16
use function is_int;
17
use PDO;
18
use function preg_match;
19
use RuntimeException;
20
use function strtolower;
21
22
/**
23
 * PDO-backed database connection (common parts).
24
 * @author Doug Wright
25
 */
26
abstract class PDODatabase extends PDO implements DatabaseInterface
27
{
28
    /**
29
     * Character to use when quoting identifiers.
30
     */
31
    public const IDENTIFIER_OPENQUOTE = '"';
32
33
    /**
34
     * Character to use when quoting identifiers.
35
     */
36
    public const IDENTIFIER_CLOSEQUOTE = '"';
37
38
    /**
39
     * Constructor.
40
     * @param string $aDSN
41
     * @param string $aUsername
42
     * @param string $aPassword
43
     */
44
    public function __construct($aDSN, $aUsername, $aPassword, array $aDriverOptions = null)
45
    {
46
        parent::__construct($aDSN, $aUsername, $aPassword, $aDriverOptions);
47
        $this->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
48
        $this->setAttribute(PDO::ATTR_STATEMENT_CLASS, ['\DVDoug\DB\PDOStatement']);
49
    }
50
51
    /**
52
     * Returns the ID of the last inserted row or sequence value.
53
     * @param string $aName name of the sequence object (if any) from which the ID should be returned
54
     */
55
    public function getLastInsertId($aName = null): string
56
    {
57
        return parent::lastInsertID($aName);
58
    }
59
60
    /**
61
     * Escapes/quotes a parameter for use in a query.
62
     *
63
     * @param mixed $aParam     the parameter to be quoted
64
     * @param int   $aParamType data type hint for drivers
65
     *
66
     * @throws RuntimeException
67
     * @return string           a quoted string that is theoretically safe to pass into an SQL statement
68
     */
69
    public function escape($aParam, $aParamType = DatabaseInterface::PARAM_IS_STR)
70
    {
71
        switch ($aParamType) {
72
            case self::PARAM_IS_INT:
73
                if (is_int($aParam) || ctype_digit($aParam)) {
74
                    return (int) $aParam;
75
                } else {
76
                    throw new RuntimeException("Parameter {$aParam} is not an integer");
77
                }
78
                break;
79
80
            default:
81
                return parent::quote($aParam, $aParamType);
82
        }
83
    }
84
85
    /**
86
     * Adds appropriate quotes to an identifier so it can be safely used in an SQL statement.
87
     * @param mixed $aIdentifier the parameter to be quoted
88
     */
89
    public function quoteIdentifier($aIdentifier): string
90
    {
91
        return static::IDENTIFIER_OPENQUOTE . $aIdentifier . static::IDENTIFIER_CLOSEQUOTE;
92
    }
93
94
    /**
95
     * Get MySQL table definition.
96
     * @param string $aDatabase       database/schema name
97
     * @param string $aTable          table name
98
     * @param bool   $aSkipUnusedCols whether to skip unused columns
99
     */
100
    public function getMySQLTableDef($aDatabase, $aTable, $aSkipUnusedCols = true): string
101
    {
102
        $table = strtolower($aTable);
103
104
        $columns = $this->getTableColumns($aDatabase, $aTable);
105
106
        $colDefs = [];
107
108
        foreach ($columns as $columnName => $column) {
109
            if ($aSkipUnusedCols && $column->getDistinctValueCount() <= 1) {
110
                unset($columns[$columnName]);
111
                continue;
112
            }
113
114
            $colDefs[] = $column->getMySQLColumnDef();
115
        }
116
117
        $primaryKey = $this->getPrimaryKey($aDatabase, $aTable);
118
        $indexes = $this->getIndexes($aDatabase, $aTable);
119
120
        $tableDef = "CREATE TABLE `{$table}` (" . "\n";
121
        $tableDef .= implode(',' . "\n", $colDefs);
122
123
        if ($primaryKey) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $primaryKey of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
124
            $length = 0;
125
            foreach ($primaryKey as $primaryCol) {
126
                $length += $columns[$primaryCol]->getLength();
127
            }
128
            if ($length <= 191) { //skip index if too long for MySQL
129
                $tableDef .= ',' . "\n" . "\n";
130
                $tableDef .= 'PRIMARY KEY (';
131
                $tableDef .= implode(', ' . "\n", array_map(function ($c) {return '`' . strtolower($c) . '`'; }, $primaryKey));
132
                $tableDef .= ')';
133
            }
134
        }
135
136
        if ($indexes) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $indexes of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
137
            foreach ($indexes as $indexName => $indexColumns) {
138
                $length = 0;
139
                foreach ($indexColumns as &$col) {
140
                    if (!in_array($col, array_keys($columns))) { //skip index if it includes a skipped column
141
                        continue 2;
142
                    }
143
144
                    $length += $columns[$col]->getLength();
145
                    if ($length > 191) { //skip index if too long for MySQL
146
                        continue 2;
147
                    }
148
149
                    if (preg_match('/(BLOB|TEXT)$/', $columns[$col]->getMySQLType())) {
150
                        continue 2;
151
                    }
152
                }
153
                $tableDef .= ',' . "\n";
154
                $tableDef .= 'KEY `' . strtolower($indexName) . '` (';
155
                $tableDef .= implode(', ', array_map(function ($c) {return '`' . strtolower($c) . '`'; }, $indexColumns));
156
                $tableDef .= ')';
157
            }
158
        }
159
        $tableDef .= ') ENGINE=InnoDB ROW_FORMAT=COMPRESSED';
160
161
        return $tableDef;
162
    }
163
164
    /**
165
     * Get Oracle table definition.
166
     * @param string $aDatabase       database/schema name
167
     * @param string $aTable          table name
168
     * @param bool   $aSkipUnusedCols whether to skip unused columns
169
     */
170
    public function getOracleTableDef($aDatabase, $aTable, $aSkipUnusedCols = true): string
171
    {
172
        $table = strtolower($aTable);
173
174
        $columns = $this->getTableColumns($aDatabase, $aTable);
175
176
        $colDefs = [];
177
178
        foreach ($columns as $columnName => $column) {
179
            if ($aSkipUnusedCols && $column->getDistinctValueCount() <= 1) {
180
                unset($columns[$columnName]);
181
                continue;
182
            }
183
184
            $colDefs[] = $column->getOracleColumnDef();
185
        }
186
187
        $primaryKey = $this->getPrimaryKey($aDatabase, $aTable);
188
        $indexes = $this->getIndexes($aDatabase, $aTable);
189
190
        $tableDef = "CREATE TABLE `{$table}` (" . "\n";
191
        $tableDef .= implode(',' . "\n", $colDefs);
192
193
        if ($primaryKey) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $primaryKey of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
194
            $tableDef .= ',' . "\n" . "\n";
195
            $tableDef .= 'PRIMARY KEY (';
196
            $tableDef .= implode(', ' . "\n", array_map(function ($c) {return '"' . strtolower($c) . '"'; }, $primaryKey));
197
            $tableDef .= ')';
198
        }
199
200
        if ($indexes) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $indexes of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
201
            foreach ($indexes as $indexName => $indexColumns) {
202
                foreach ($indexColumns as &$col) {
203
                    if (!in_array($col, array_keys($columns))) { //skip index if it includes a skipped column
204
                        continue 2;
205
                    }
206
                }
207
                $tableDef .= ',' . "\n";
208
                $tableDef .= 'KEY `' . strtolower($indexName) . '` (';
209
                $tableDef .= implode(', ', array_map(function ($c) {return '"' . strtolower($c) . '"'; }, $indexColumns));
210
                $tableDef .= ')';
211
            }
212
        }
213
        $tableDef .= ')';
214
215
        return $tableDef;
216
    }
217
}
218