Scrutinizer GitHub App not installed

We could not synchronize checks via GitHub's checks API since Scrutinizer's GitHub App is not installed for this repository.

Install GitHub App

Failed Conditions
Push — master ( c670fd...b18f1a )
by Dan
20s queued 16s
created

Database::error()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 2
rs 10
c 0
b 0
f 0
1
<?php declare(strict_types=1);
2
3
namespace Smr;
4
5
use mysqli;
6
use mysqli_result;
7
use RuntimeException;
8
use Smr\Container\DiContainer;
9
use Smr\DatabaseProperties;
10
11
class Database {
12
	private mysqli $dbConn;
13
	private DatabaseProperties $dbProperties;
14
	private string $selectedDbName;
15
	private bool|mysqli_result $dbResult;
16
	private ?array $dbRecord;
17
18
	public static function getInstance(): self {
19
		return DiContainer::make(self::class);
20
	}
21
22
	/**
23
	 * Reconnects to the MySQL database, and replaces the managed mysqli instance
24
	 * in the dependency injection container for future retrievals.
25
	 * @throws \DI\DependencyException
26
	 * @throws \DI\NotFoundException
27
	 */
28
	private static function reconnectMysql(): mysqli {
29
		$newMysqli = DiContainer::make(mysqli::class);
30
		DiContainer::getContainer()->set(mysqli::class, $newMysqli);
31
		return $newMysqli;
32
	}
33
34
	public static function mysqliFactory(DatabaseProperties $dbProperties): mysqli {
35
		if (!mysqli_report(MYSQLI_REPORT_ERROR|MYSQLI_REPORT_STRICT)) {
36
			throw new RuntimeException('Failed to enable mysqli error reporting');
37
		}
38
		$mysql = new mysqli(
39
			$dbProperties->getHost(),
40
			$dbProperties->getUser(),
41
			$dbProperties->getPassword(),
42
			$dbProperties->getDatabaseName());
43
		$charset = $mysql->character_set_name();
44
		if ($charset != 'utf8') {
45
			throw new RuntimeException('Unexpected charset: ' . $charset);
46
		}
47
		return $mysql;
48
	}
49
50
	/**
51
	 * Database constructor.
52
	 * Not intended to be constructed by hand. If you need an instance of Database,
53
	 * use Database::getInstance();
54
	 * @param ?mysqli $dbConn The mysqli instance (null if reconnect needed)
55
	 * @param DatabaseProperties $dbProperties The properties object that was used to construct the mysqli instance
56
	 */
57
	public function __construct(?mysqli $dbConn, DatabaseProperties $dbProperties) {
58
		if (is_null($dbConn)) {
59
			$dbConn = self::reconnectMysql();
60
		}
61
		$this->dbConn = $dbConn;
62
		$this->dbProperties = $dbProperties;
63
		$this->selectedDbName = $dbProperties->getDatabaseName();
64
	}
65
66
	/**
67
	 * This method will switch the connection to the specified database.
68
	 * Useful for switching back and forth between historical, and live databases.
69
	 *
70
	 * @param string $databaseName The name of the database to switch to
71
	 */
72
	public function switchDatabases(string $databaseName) {
73
		$this->dbConn->select_db($databaseName);
74
		$this->selectedDbName = $databaseName;
75
	}
76
77
	/**
78
	 * Switch back to the configured live database
79
	 */
80
	public function switchDatabaseToLive() {
81
		$this->switchDatabases($this->dbProperties->getDatabaseName());
82
	}
83
84
	/**
85
	 * Returns the size of the selected database in bytes.
86
	 */
87
	public function getDbBytes() {
88
		$query = 'SELECT SUM(data_length + index_length) as db_bytes FROM information_schema.tables WHERE table_schema=' . $this->escapeString($this->selectedDbName);
89
		$result = $this->dbConn->query($query);
90
		return (int)$result->fetch_assoc()['db_bytes'];
91
	}
92
93
	/**
94
	 * This should not be needed except perhaps by persistent connections
95
	 *
96
	 * Closes the connection to the MySQL database. After closing this connection,
97
	 * this instance is no longer valid, and will subsequently throw exceptions when
98
	 * attempting to perform database operations.
99
	 *
100
	 * You must call Database::getInstance() again to retrieve a valid instance that
101
	 * is reconnected to the database.
102
	 *
103
	 * @return bool Whether the underlying connection was closed by this call.
104
	 */
105
	public function close() : bool {
106
		if (!isset($this->dbConn)) {
107
			// Connection is already closed; nothing to do.
108
			return false;
109
		}
110
		$this->dbConn->close();
111
		unset($this->dbConn);
112
		// Set the mysqli instance in the dependency injection container to
113
		// null so that the Database constructor will reconnect the next time
114
		// it is called.
115
		DiContainer::getContainer()->set(mysqli::class, null);
116
		return true;
117
	}
118
119
	public function query($query) {
120
		$this->dbResult = $this->dbConn->query($query);
121
	}
122
123
	/**
124
	 * Use to populate this instance with the next record of the active query.
125
	 */
126
	public function nextRecord(): bool {
127
		if (empty($this->dbResult)) {
128
			$this->error('No resource to get record from.');
129
		}
130
		if ($this->dbRecord = $this->dbResult->fetch_assoc()) {
131
			return true;
132
		}
133
		return false;
134
	}
135
136
	/**
137
	 * Use instead of nextRecord when exactly one record is expected from the
138
	 * active query.
139
	 */
140
	public function requireRecord(): void {
141
		if (!$this->nextRecord() || $this->getNumRows() != 1) {
142
			$this->error('One record required, but found ' . $this->getNumRows());
143
		}
144
	}
145
146
	public function hasField($name) {
147
		return isset($this->dbRecord[$name]);
148
	}
149
150
	public function getField($name) {
151
		return $this->dbRecord[$name];
152
	}
153
154
	public function getBoolean(string $name) : bool {
155
		if ($this->dbRecord[$name] === 'TRUE') {
156
			return true;
157
		} elseif ($this->dbRecord[$name] === 'FALSE') {
158
			return false;
159
		}
160
		$this->error('Field is not a boolean: ' . $name);
161
	}
162
163
	public function getInt($name) {
164
		return (int)$this->dbRecord[$name];
165
	}
166
167
	public function getFloat($name) {
168
		return (float)$this->dbRecord[$name];
169
	}
170
171
	public function getMicrotime(string $name) : string {
172
		// All digits of precision are stored in a MySQL bigint
173
		$data = $this->dbRecord[$name];
174
		return sprintf('%f', $data / 1E6);
175
	}
176
177
	public function getObject($name, $compressed = false, $nullable = false) {
178
		$object = $this->getField($name);
179
		if ($nullable === true && $object === null) {
180
			return null;
181
		}
182
		if ($compressed === true) {
183
			$object = gzuncompress($object);
184
		}
185
		return unserialize($object);
186
	}
187
188
	public function getRow() {
189
		return $this->dbRecord;
190
	}
191
192
	public function lockTable($table) {
193
		$this->dbConn->query('LOCK TABLES ' . $table . ' WRITE');
194
	}
195
196
	public function unlock() {
197
		$this->dbConn->query('UNLOCK TABLES');
198
	}
199
200
	public function getNumRows() {
201
		return $this->dbResult->num_rows;
202
	}
203
204
	public function getChangedRows() {
205
		return $this->dbConn->affected_rows;
206
	}
207
208
	public function getInsertID() {
209
		return $this->dbConn->insert_id;
210
	}
211
212
	protected function error($err) {
213
		throw new RuntimeException($err);
214
	}
215
216
	public function escape($escape) {
217
		if (is_bool($escape)) {
218
			return $this->escapeBoolean($escape);
219
		}
220
		if (is_numeric($escape)) {
221
			return $this->escapeNumber($escape);
222
		}
223
		if (is_string($escape)) {
224
			return $this->escapeString($escape);
225
		}
226
		if (is_array($escape)) {
227
			return $this->escapeArray($escape);
228
		}
229
		if (is_object($escape)) {
230
			return $this->escapeObject($escape);
231
		}
232
	}
233
234
	public function escapeString(?string $string, bool $nullable = false) : string {
235
		if ($nullable === true && ($string === null || $string === '')) {
236
			return 'NULL';
237
		}
238
		return '\'' . $this->dbConn->real_escape_string($string) . '\'';
239
	}
240
241
	public function escapeBinary($binary) {
242
		return '0x' . bin2hex($binary);
243
	}
244
245
	/**
246
	 * Warning: If escaping a nested array, use escapeIndividually=true,
247
	 * but beware that the escaped array is flattened!
248
	 */
249
	public function escapeArray(array $array, string $delimiter = ',', bool $escapeIndividually = true) : string {
250
		if ($escapeIndividually) {
251
			$string = join($delimiter, array_map(function($item) { return $this->escape($item); }, $array));
252
		} else {
253
			$string = $this->escape(join($delimiter, $array));
254
		}
255
		return $string;
256
	}
257
258
	public function escapeNumber($num) {
259
		// Numbers need not be quoted in MySQL queries, so if we know $num is
260
		// numeric, we can simply return its value (no quoting or escaping).
261
		if (is_numeric($num)) {
262
			return $num;
263
		} else {
264
			$this->error('Not a number! (' . $num . ')');
265
		}
266
	}
267
268
	public function escapeMicrotime(float $microtime) : string {
269
		// Retain all digits of precision for storing in a MySQL bigint
270
		return sprintf('%d', $microtime * 1E6);
271
	}
272
273
	public function escapeBoolean(bool $bool) : string {
274
		// We store booleans as an enum
275
		if ($bool) {
276
			return '\'TRUE\'';
277
		} else {
278
			return '\'FALSE\'';
279
		}
280
	}
281
282
	public function escapeObject($object, bool $compress = false, bool $nullable = false) : string {
283
		if ($nullable === true && $object === null) {
284
			return 'NULL';
285
		}
286
		if ($compress === true) {
287
			return $this->escapeBinary(gzcompress(serialize($object)));
288
		}
289
		return $this->escapeString(serialize($object));
290
	}
291
}
292