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
Pull Request — master (#1094)
by Dan
04:47
created

Database::read()   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
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 2
rs 10
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
/**
12
 * Wraps an active connection to the database.
13
 * Primarily provides query, escaping, and locking methods.
14
 */
15
class Database {
16
17
	/**
18
	 * Returns the instance of this class from the DI container.
19
	 * If one does not exist yet, it will be created.
20
	 * This is the intended way to construct this class.
21
	 */
22
	public static function getInstance() : self {
23
		return DiContainer::get(self::class);
24
	}
25
26
	/**
27
	 * Used by the DI container to construct a mysqli instance.
28
	 * Not intended to be used outside the DI context.
29
	 */
30
	public static function mysqliFactory(DatabaseProperties $dbProperties) : mysqli {
31
		if (!mysqli_report(MYSQLI_REPORT_ERROR|MYSQLI_REPORT_STRICT)) {
32
			throw new RuntimeException('Failed to enable mysqli error reporting');
33
		}
34
		$mysql = new mysqli(
35
			$dbProperties->getHost(),
36
			$dbProperties->getUser(),
37
			$dbProperties->getPassword(),
38
			$dbProperties->getDatabaseName());
39
		$charset = $mysql->character_set_name();
40
		if ($charset != 'utf8') {
41
			throw new RuntimeException('Unexpected charset: ' . $charset);
42
		}
43
		return $mysql;
44
	}
45
46
	/**
47
	 * Database constructor.
48
	 * Not intended to be constructed by hand. If you need an instance of Database,
49
	 * use Database::getInstance();
50
	 * @param mysqli $dbConn The mysqli instance
51
	 * @param DatabaseProperties $dbProperties The properties object that was used to construct the mysqli instance
52
	 */
53
	public function __construct(
54
		private mysqli $dbConn,
55
		private DatabaseProperties $dbProperties,
56
	) {}
57
58
	/**
59
	 * This method will switch the connection to the specified database.
60
	 * Useful for switching back and forth between historical, and live databases.
61
	 *
62
	 * @param string $databaseName The name of the database to switch to
63
	 */
64
	public function switchDatabases(string $databaseName) : void {
65
		$this->dbConn->select_db($databaseName);
66
	}
67
68
	/**
69
	 * Switch back to the configured live database
70
	 */
71
	public function switchDatabaseToLive() : void {
72
		$this->switchDatabases($this->dbProperties->getDatabaseName());
73
	}
74
75
	/**
76
	 * Returns the size of the live database in bytes.
77
	 */
78
	public function getDbBytes() : int {
79
		$query = 'SELECT SUM(data_length + index_length) as db_bytes FROM information_schema.tables WHERE table_schema=' . $this->escapeString($this->dbProperties->getDatabaseName());
80
		return $this->read($query)->record()->getInt('db_bytes');
81
	}
82
83
	/**
84
	 * This should not be needed except perhaps by persistent connections
85
	 *
86
	 * Closes the connection to the MySQL database. After closing this connection,
87
	 * this instance is no longer valid, and will subsequently throw exceptions when
88
	 * attempting to perform database operations.
89
	 *
90
	 * Once the connection is closed, you must call Database::reconnect() before
91
	 * any further database queries can be made.
92
	 *
93
	 * @return bool Whether the underlying connection was closed by this call.
94
	 */
95
	public function close() : bool {
96
		if (!isset($this->dbConn)) {
97
			// Connection is already closed; nothing to do.
98
			return false;
99
		}
100
		$this->dbConn->close();
101
		unset($this->dbConn);
102
		// Set the mysqli instance in the dependency injection container to
103
		// null so that we don't accidentally try to use it.
104
		DiContainer::getContainer()->set(mysqli::class, null);
105
		return true;
106
	}
107
108
	/**
109
	 * Reconnects to the MySQL database, and replaces the managed mysqli instance
110
	 * in the dependency injection container for future retrievals.
111
	 * @throws \DI\DependencyException
112
	 * @throws \DI\NotFoundException
113
	 */
114
	public function reconnect() : void {
115
		if (isset($this->dbConn)) {
116
			return; // No reconnect needed
117
		}
118
		$newMysqli = DiContainer::make(mysqli::class);
119
		DiContainer::getContainer()->set(mysqli::class, $newMysqli);
120
		$this->dbConn = $newMysqli;
121
	}
122
123
	/**
124
	 * Perform a write-only query on the database.
125
	 * Used for UPDATE, DELETE, REPLACE and INSERT queries, for example.
126
	 */
127
	public function write(string $query) : void {
128
		$result = $this->dbConn->query($query);
129
		if ($result !== true) {
130
			throw new RuntimeException('Wrong query type');
131
		}
132
	}
133
134
	/**
135
	 * Perform a read-only query on the database.
136
	 * Used for SELECT queries, for example.
137
	 */
138
	public function read(string $query) : DatabaseResult {
139
		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

139
		return new DatabaseResult(/** @scrutinizer ignore-type */ $this->dbConn->query($query));
Loading history...
140
	}
141
142
	public function lockTable(string $table) : void {
143
		$this->write('LOCK TABLES ' . $table . ' WRITE');
144
	}
145
146
	public function unlock() : void {
147
		$this->write('UNLOCK TABLES');
148
	}
149
150
	public function getChangedRows() : int {
151
		return $this->dbConn->affected_rows;
152
	}
153
154
	public function getInsertID() : int {
155
		return $this->dbConn->insert_id;
156
	}
157
158
	public function escape($escape) {
159
		if (is_bool($escape)) {
160
			return $this->escapeBoolean($escape);
161
		}
162
		if (is_numeric($escape)) {
163
			return $this->escapeNumber($escape);
164
		}
165
		if (is_string($escape)) {
166
			return $this->escapeString($escape);
167
		}
168
		if (is_array($escape)) {
169
			return $this->escapeArray($escape);
170
		}
171
		if (is_object($escape)) {
172
			return $this->escapeObject($escape);
173
		}
174
	}
175
176
	public function escapeString(?string $string, bool $nullable = false) : string {
177
		if ($nullable === true && ($string === null || $string === '')) {
178
			return 'NULL';
179
		}
180
		return '\'' . $this->dbConn->real_escape_string($string) . '\'';
181
	}
182
183
	public function escapeBinary($binary) {
184
		return '0x' . bin2hex($binary);
185
	}
186
187
	/**
188
	 * Warning: If escaping a nested array, use escapeIndividually=true,
189
	 * but beware that the escaped array is flattened!
190
	 */
191
	public function escapeArray(array $array, string $delimiter = ',', bool $escapeIndividually = true) : string {
192
		if ($escapeIndividually) {
193
			$string = join($delimiter, array_map(function($item) { return $this->escape($item); }, $array));
194
		} else {
195
			$string = $this->escape(join($delimiter, $array));
196
		}
197
		return $string;
198
	}
199
200
	public function escapeNumber(mixed $num) : mixed {
201
		// Numbers need not be quoted in MySQL queries, so if we know $num is
202
		// numeric, we can simply return its value (no quoting or escaping).
203
		if (!is_numeric($num)) {
204
			throw new RuntimeException('Not a number: ' . $num);
205
		}
206
		return $num;
207
	}
208
209
	public function escapeMicrotime(float $microtime) : string {
210
		// Retain all digits of precision for storing in a MySQL bigint
211
		return sprintf('%d', $microtime * 1E6);
212
	}
213
214
	public function escapeBoolean(bool $bool) : string {
215
		// We store booleans as an enum
216
		if ($bool) {
217
			return '\'TRUE\'';
218
		} else {
219
			return '\'FALSE\'';
220
		}
221
	}
222
223
	public function escapeObject(mixed $object, bool $compress = false, bool $nullable = false) : string {
224
		if ($nullable === true && $object === null) {
225
			return 'NULL';
226
		}
227
		if ($compress === true) {
228
			return $this->escapeBinary(gzcompress(serialize($object)));
229
		}
230
		return $this->escapeString(serialize($object));
231
	}
232
}
233