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 — live ( 05ca5f...631a24 )
by Dan
05:49
created

Database::reconnect()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 5
c 0
b 0
f 0
nc 2
nop 0
dl 0
loc 7
rs 10
1
<?php declare(strict_types=1);
2
3
namespace Smr;
4
5
use Exception;
6
use mysqli;
7
use RuntimeException;
8
use Smr\Container\DiContainer;
9
10
/**
11
 * Wraps an active connection to the database.
12
 * Primarily provides query, escaping, and locking methods.
13
 */
14
class Database {
15
16
	/**
17
	 * Returns the instance of this class from the DI container.
18
	 * If one does not exist yet, it will be created.
19
	 * This is the intended way to construct this class.
20
	 */
21
	public static function getInstance(): self {
22
		return DiContainer::get(self::class);
23
	}
24
25
	/**
26
	 * This should not be needed except perhaps by persistent services
27
	 * (such as Dicord/IRC clients) to prevent connection timeouts between
28
	 * callbacks.
29
	 *
30
	 * Closes the underlying database connection and resets the state of the
31
	 * DI container so that a new Database and mysqli instance will be made
32
	 * the next time Database::getInstance() is called. Existing instances of
33
	 * this class will no longer be valid, and will throw when attempting to
34
	 * perform database operations.
35
	 *
36
	 * This function is safe to use even if the DI container or the Database
37
	 * instances have not been initialized yet.
38
	 */
39
	public static function resetInstance(): void {
40
		if (DiContainer::initialized(mysqli::class)) {
41
			$container = DiContainer::getContainer();
42
			if (DiContainer::initialized(self::class)) {
43
				self::getInstance()->dbConn->close();
44
				$container->reset(self::class);
45
			}
46
			$container->reset(mysqli::class);
47
		}
48
	}
49
50
	/**
51
	 * Used by the DI container to construct a mysqli instance.
52
	 * Not intended to be used outside the DI context.
53
	 */
54
	public static function mysqliFactory(DatabaseProperties $dbProperties): mysqli {
55
		if (!mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT)) {
56
			throw new RuntimeException('Failed to enable mysqli error reporting');
57
		}
58
		$mysql = new mysqli(
59
			$dbProperties->getHost(),
60
			$dbProperties->getUser(),
61
			$dbProperties->getPassword(),
62
			$dbProperties->getDatabaseName()
63
		);
64
		$charset = $mysql->character_set_name();
65
		if ($charset != 'utf8') {
66
			throw new RuntimeException('Unexpected charset: ' . $charset);
67
		}
68
		return $mysql;
69
	}
70
71
	/**
72
	 * Not intended to be constructed by hand. If you need an instance of Database,
73
	 * use Database::getInstance();
74
	 *
75
	 * @param \mysqli $dbConn The mysqli instance
76
	 * @param string $dbName The name of the database that was used to construct the mysqli instance
77
	 */
78
	public function __construct(
79
		private readonly mysqli $dbConn,
80
		private readonly string $dbName,
81
	) {}
82
83
	/**
84
	 * This method will switch the connection to the specified database.
85
	 * Useful for switching back and forth between historical, and live databases.
86
	 *
87
	 * @param string $databaseName The name of the database to switch to
88
	 */
89
	public function switchDatabases(string $databaseName): void {
90
		$this->dbConn->select_db($databaseName);
91
	}
92
93
	/**
94
	 * Switch back to the configured live database
95
	 */
96
	public function switchDatabaseToLive(): void {
97
		$this->switchDatabases($this->dbName);
98
	}
99
100
	/**
101
	 * Returns the size of the current database in bytes.
102
	 */
103
	public function getDbBytes(): int {
104
		$query = 'SELECT SUM(data_length + index_length) as db_bytes FROM information_schema.tables WHERE table_schema=(SELECT database())';
105
		return $this->read($query)->record()->getInt('db_bytes');
106
	}
107
108
	/**
109
	 * Perform a write-only query on the database.
110
	 * Used for UPDATE, DELETE, REPLACE and INSERT queries, for example.
111
	 */
112
	public function write(string $query): void {
113
		$result = $this->dbConn->query($query);
114
		if ($result !== true) {
115
			throw new RuntimeException('Wrong query type');
116
		}
117
	}
118
119
	/**
120
	 * Perform a read-only query on the database.
121
	 * Used for SELECT queries, for example.
122
	 */
123
	public function read(string $query): DatabaseResult {
124
		return new DatabaseResult($this->dbConn->query($query));
0 ignored issues
show
Bug introduced by
It seems like $this->dbConn->query($query) can also be of type true; however, parameter $dbResult of Smr\DatabaseResult::__construct() does only seem to accept mysqli_result, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

124
		return new DatabaseResult(/** @scrutinizer ignore-type */ $this->dbConn->query($query));
Loading history...
125
	}
126
127
	/**
128
	 * INSERT a row into $table.
129
	 *
130
	 * @param string $table
131
	 * @param array<string, mixed> $fields
132
	 * @return int Insert ID of auto-incrementing column, if applicable
133
	 */
134
	public function insert(string $table, array $fields): int {
135
		$query = 'INSERT INTO ' . $table . ' (' . implode(', ', array_keys($fields))
136
			. ') VALUES (' . implode(', ', array_values($fields)) . ')';
137
		$this->write($query);
138
		return $this->getInsertID();
139
	}
140
141
	/**
142
	 * REPLACE a row into $table.
143
	 *
144
	 * @param string $table
145
	 * @param array<string, mixed> $fields
146
	 * @return int Insert ID of auto-incrementing column, if applicable
147
	 */
148
	public function replace(string $table, array $fields): int {
149
		$query = 'REPLACE INTO ' . $table . ' (' . implode(', ', array_keys($fields))
150
			. ') VALUES (' . implode(', ', array_values($fields)) . ')';
151
		$this->write($query);
152
		return $this->getInsertID();
153
	}
154
155
	public function lockTable(string $table): void {
156
		$this->write('LOCK TABLES ' . $table . ' WRITE');
157
	}
158
159
	public function unlock(): void {
160
		$this->write('UNLOCK TABLES');
161
	}
162
163
	public function getChangedRows(): int {
164
		return $this->dbConn->affected_rows;
165
	}
166
167
	public function getInsertID(): int {
168
		return $this->dbConn->insert_id;
169
	}
170
171
	public function escape(mixed $escape): mixed {
172
		return match (true) {
173
			is_bool($escape) => $this->escapeBoolean($escape),
174
			is_numeric($escape) => $this->escapeNumber($escape),
175
			is_string($escape) => $this->escapeString($escape),
176
			is_array($escape) => $this->escapeArray($escape),
177
			is_object($escape) => $this->escapeObject($escape),
178
			default => throw new Exception('Unhandled value: ' . $escape)
179
		};
180
	}
181
182
	public function escapeString(?string $string, bool $nullable = false): string {
183
		if ($nullable === true && ($string === null || $string === '')) {
184
			return 'NULL';
185
		}
186
		return '\'' . $this->dbConn->real_escape_string($string) . '\'';
187
	}
188
189
	public function escapeBinary(string $binary): string {
190
		return '0x' . bin2hex($binary);
191
	}
192
193
	/**
194
	 * Warning: If escaping a nested array, beware that the escaped array is
195
	 * flattened!
196
	 *
197
	 * @param array<mixed> $array
198
	 */
199
	public function escapeArray(array $array): string {
200
		return implode(',', array_map(fn($item) => $this->escape($item), $array));
201
	}
202
203
	public function escapeNumber(mixed $num): mixed {
204
		// Numbers need not be quoted in MySQL queries, so if we know $num is
205
		// numeric, we can simply return its value (no quoting or escaping).
206
		if (!is_numeric($num)) {
207
			throw new RuntimeException('Not a number: ' . $num);
208
		}
209
		return $num;
210
	}
211
212
	public function escapeBoolean(bool $bool): string {
213
		// We store booleans as an enum
214
		return $bool ? '\'TRUE\'' : '\'FALSE\'';
215
	}
216
217
	public function escapeObject(mixed $object, bool $compress = false, bool $nullable = false): string {
218
		if ($nullable === true && $object === null) {
219
			return 'NULL';
220
		}
221
		if ($compress === true) {
222
			return $this->escapeBinary(gzcompress(serialize($object)));
223
		}
224
		return $this->escapeString(serialize($object));
225
	}
226
227
}
228