1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
|
<?php
/**
* webtrees: online genealogy
* Copyright (C) 2017 webtrees development team
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Fisharebest\Webtrees;
use Fisharebest\Webtrees\Schema\MigrationInterface;
use PDO;
use PDOException;
/**
* Extend PHP's native PDO class.
*/
class Database {
/** @var Database Implement the singleton pattern */
private static $instance;
/** @var PDO Native PHP database driver */
private static $pdo;
/** @var array Keep a log of all the SQL statements that we execute */
private static $log;
/** @var Statement[] Cache of prepared statements */
private static $prepared = [];
/**
* Prevent instantiation via new Database
*/
private function __construct() {
self::$log = [];
}
/**
* Begin a transaction.
*
* @return bool
*/
public static function beginTransaction() {
return self::$pdo->beginTransaction();
}
/**
* Commit this transaction.
*
* @return bool
*/
public static function commit() {
return self::$pdo->commit();
}
/**
* Disconnect from the server, so we can connect to another one
*/
public static function disconnect() {
self::$pdo = null;
}
/**
* Implement the singleton pattern, using a static accessor.
*
* @param string $DBHOST
* @param string $DBPORT
* @param string $DBNAME
* @param string $DBUSER
* @param string $DBPASS
*
* @throws \Exception
*/
public static function createInstance($DBHOST, $DBPORT, $DBNAME, $DBUSER, $DBPASS) {
if (self::$pdo instanceof PDO) {
throw new \Exception('Database::createInstance() can only be called once.');
}
// Create the underlying PDO object
self::$pdo = new PDO(
(substr($DBHOST, 0, 1) === '/' ?
"mysql:unix_socket={$DBHOST};dbname={$DBNAME}" :
"mysql:host={$DBHOST};dbname={$DBNAME};port={$DBPORT}"
),
$DBUSER, $DBPASS,
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_OBJ,
PDO::ATTR_CASE => PDO::CASE_LOWER,
PDO::ATTR_AUTOCOMMIT => true,
]
);
self::$pdo->exec("SET NAMES UTF8");
self::$pdo->prepare("SET time_zone = :time_zone")->execute(['time_zone' => date('P')]);
self::$instance = new self;
}
/**
* We don't access $instance directly, only via query(), exec() and prepare()
*
* @throws \Exception
*
* @return Database
*/
public static function getInstance() {
if (self::$pdo instanceof PDO) {
return self::$instance;
} else {
throw new \Exception('createInstance() must be called before getInstance().');
}
}
/**
* Are we currently connected to a database?
*
* @return bool
*/
public static function isConnected() {
return self::$pdo instanceof PDO;
}
/**
* Log the details of a query, for debugging and analysis.
*
* @param string $query
* @param int $rows
* @param float $microseconds
* @param string[] $bind_variables
*/
public static function logQuery($query, $rows, $microseconds, $bind_variables) {
// Trace
$trace = debug_backtrace();
array_shift($trace);
array_shift($trace);
foreach ($trace as $n => $frame) {
if (isset($frame['file']) && isset($frame['line'])) {
$trace[$n] = basename($frame['file']) . ':' . $frame['line'] . ' ' . $frame['function'];
} else {
unset($trace[$n]);
}
}
$stack = '<abbr title="' . Filter::escapeHtml(implode(' / ', $trace)) . '">' . (count(self::$log) + 1) . '</abbr>';
// Bind variables
foreach ($bind_variables as $key => $value) {
if (is_null($value)) {
$value = 'NULL';
} elseif (!is_integer($value)) {
$value = '\'' . $value . '\'';
}
if (is_integer($key)) {
$query = preg_replace('/\?/', $value, $query, 1);
} else {
$query = str_replace(':' . $key, $value, $query);
}
}
$milliseconds = sprintf('%.3f', $microseconds * 1000);
self::$log[] = '<tr><td>' .$stack . '</td><td>' . $query . '</td><td>' . $rows . '</td><td>' . $milliseconds . '</td></tr>';
}
/**
* Determine the number of queries executed, for the page statistics.
*
* @return int
*/
public static function getQueryCount() {
return count(self::$log);
}
/**
* Convert the query log into an HTML table.
*
* @return string
*/
public static function getQueryLog() {
$html = '<table border="1" style="table-layout: fixed; width: 960px;word-wrap: break-word;"><col span="3"><col align="char"><thead><tr><th>#</th><th style="width: 800px;">Query</th><th>Rows</th><th>Time (ms)</th></tr></thead><tbody>' . implode('', self::$log) . '</tbody></table>';
self::$log = [];
return $html;
}
/**
* Determine the most recently created value of an AUTO_INCREMENT field.
*
* @return string
*/
public static function lastInsertId() {
return self::$pdo->lastInsertId();
}
/**
* Quote a string for embedding in a MySQL statement.
*
* The native quote() function does not convert PHP nulls to DB nulls
*
* @param string $string
*
* @return string
*
* @deprecated We should use bind-variables instead.
*/
public static function quote($string) {
if (is_null($string)) {
return 'NULL';
} else {
return self::$pdo->quote($string, PDO::PARAM_STR);
}
}
/**
* Execute an SQL statement, and log the result.
*
* @param string $sql The SQL statement to execute
*
* @return int The number of rows affected by this SQL query
*/
public static function exec($sql) {
$sql = str_replace('##', WT_TBLPREFIX, $sql);
$start = microtime(true);
$rows = self::$pdo->exec($sql);
$end = microtime(true);
self::logQuery($sql, $rows, $end - $start, []);
return $rows;
}
/**
* Prepare an SQL statement for execution.
*
* @param $sql
*
* @throws \Exception
*
* @return Statement
*/
public static function prepare($sql) {
if (!self::$pdo instanceof PDO) {
throw new \Exception('No Connection Established');
}
$sql = str_replace('##', WT_TBLPREFIX, $sql);
$hash = md5($sql);
if (!array_key_exists($hash, self::$prepared)) {
self::$prepared[$hash] = new Statement(self::$pdo->prepare($sql));
}
return self::$prepared[$hash];
}
/**
* Roll back this transaction.
*
* @return bool
*/
public static function rollBack() {
return self::$pdo->rollBack();
}
/**
* Run a series of scripts to bring the database schema up to date.
*
* @param string $namespace Where to find our MigrationXXX classes
* @param string $schema_name Where to find our MigrationXXX classes
* @param int $target_version updade/downgrade to this version
*
* @throws PDOException
*
* @return bool Were any updates applied
*/
public static function updateSchema($namespace, $schema_name, $target_version) {
try {
$current_version = (int) Site::getPreference($schema_name);
} catch (PDOException $e) {
// During initial installation, the site_preference table won’t exist.
$current_version = 0;
}
$updates_applied = false;
try {
// Update the schema, one version at a time.
while ($current_version < $target_version) {
$class = $namespace . '\\Migration' . $current_version;
/** @var MigrationInterface $migration */
$migration = new $class;
$migration->upgrade();
$current_version++;
Site::setPreference($schema_name, (string) $current_version);
$updates_applied = true;
}
} catch (PDOException $ex) {
// The schema update scripts should never fail. If they do, there is no clean recovery.
FlashMessages::addMessage($ex->getMessage(), 'danger');
header('Location: site-unavailable.php');
throw $ex;
}
return $updates_applied;
}
}
|