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 ( f174b5...646f17 )
by Dan
21s queued 18s
created

Database::write()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 3
c 0
b 0
f 0
nc 2
nop 1
dl 0
loc 4
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