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 (#1038)
by Dan
11:47
created

Database   F

Complexity

Total Complexity 60

Size/Duplication

Total Lines 282
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 103
dl 0
loc 282
rs 3.6
c 0
b 0
f 0
wmc 60

33 Methods

Rating   Name   Duplication   Size   Complexity  
A error() 0 2 1
A getField() 0 2 1
A escape() 0 15 6
A getFloat() 0 2 1
A getBoolean() 0 7 3
A __construct() 0 7 2
A getMicrotime() 0 4 1
A getChangedRows() 0 2 1
A getRow() 0 2 1
A escapeNumber() 0 7 2
A getInstance() 0 2 1
A escapeBoolean() 0 6 2
A escapeString() 0 5 4
A escapeMicrotime() 0 3 1
A lockTable() 0 2 1
A nextRecord() 0 8 3
A getDbBytes() 0 4 1
A close() 0 12 2
A unlock() 0 2 1
A switchDatabases() 0 3 1
A switchDatabaseToLive() 0 2 1
A escapeArray() 0 7 2
A getInt() 0 2 1
A query() 0 2 1
A escapeBinary() 0 2 1
A escapeObject() 0 8 4
A hasField() 0 2 1
A getObject() 0 9 4
A mysqliFactory() 0 14 3
A getNumRows() 0 2 1
A reconnectMysql() 0 4 1
A requireRecord() 0 3 3
A getInsertID() 0 2 1

How to fix   Complexity   

Complex Class

Complex classes like Database often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Database, and based on these observations, apply Extract Interface, too.

1
<?php declare(strict_types=1);
2
3
namespace Smr;
4
5
use mysqli;
6
use RuntimeException;
7
use Smr\Container\DiContainer;
8
use Smr\DatabaseProperties;
9
10
class Database {
11
	private mysqli $dbConn;
12
	private DatabaseProperties $dbProperties;
13
	private string $selectedDbName;
14
	/**
15
	 * @var bool | mysqli_result
0 ignored issues
show
Bug introduced by
The type Smr\mysqli_result was not found. Did you mean mysqli_result? If so, make sure to prefix the type with \.
Loading history...
16
	 */
17
	private $dbResult = null;
18
	private ?array $dbRecord = null;
19
20
	public static function getInstance(): self {
21
		return DiContainer::make(self::class);
22
	}
23
24
	/**
25
	 * Reconnects to the MySQL database, and replaces the managed mysqli instance
26
	 * in the dependency injection container for future retrievals.
27
	 * @throws \DI\DependencyException
28
	 * @throws \DI\NotFoundException
29
	 */
30
	private static function reconnectMysql(): mysqli {
31
		$newMysqli = DiContainer::make(mysqli::class);
32
		DiContainer::getContainer()->set(mysqli::class, $newMysqli);
33
		return $newMysqli;
34
	}
35
36
	public static function mysqliFactory(DatabaseProperties $dbProperties): mysqli {
37
		if (!mysqli_report(MYSQLI_REPORT_ERROR|MYSQLI_REPORT_STRICT)) {
38
			throw new RuntimeException('Failed to enable mysqli error reporting');
39
		}
40
		$mysql = new mysqli(
41
			$dbProperties->getHost(),
42
			$dbProperties->getUser(),
43
			$dbProperties->getPassword(),
44
			$dbProperties->getDatabaseName());
45
		$charset = $mysql->character_set_name();
46
		if ($charset != 'utf8') {
47
			throw new RuntimeException('Unexpected charset: ' . $charset);
48
		}
49
		return $mysql;
50
	}
51
52
	/**
53
	 * Database constructor.
54
	 * Not intended to be constructed by hand. If you need an instance of Database,
55
	 * use Database::getInstance();
56
	 * @param ?mysqli $dbConn The mysqli instance (null if reconnect needed)
57
	 * @param DatabaseProperties $dbProperties The properties object that was used to construct the mysqli instance
58
	 */
59
	public function __construct(?mysqli $dbConn, DatabaseProperties $dbProperties) {
60
		if (is_null($dbConn)) {
61
			$dbConn = self::reconnectMysql();
62
		}
63
		$this->dbConn = $dbConn;
64
		$this->dbProperties = $dbProperties;
65
		$this->selectedDbName = $dbProperties->getDatabaseName();
66
	}
67
68
	/**
69
	 * This method will switch the connection to the specified database.
70
	 * Useful for switching back and forth between historical, and live databases.
71
	 *
72
	 * @param string $databaseName The name of the database to switch to
73
	 */
74
	public function switchDatabases(string $databaseName) {
75
		$this->dbConn->select_db($databaseName);
76
		$this->selectedDbName = $databaseName;
77
	}
78
79
	/**
80
	 * Switch back to the configured live database
81
	 */
82
	public function switchDatabaseToLive() {
83
		$this->switchDatabases($this->dbProperties->getDatabaseName());
84
	}
85
86
	/**
87
	 * Returns the size of the selected database in bytes.
88
	 */
89
	public function getDbBytes() {
90
		$query = 'SELECT SUM(data_length + index_length) as db_bytes FROM information_schema.tables WHERE table_schema=' . $this->escapeString($this->selectedDbName);
91
		$result = $this->dbConn->query($query);
92
		return (int)$result->fetch_assoc()['db_bytes'];
93
	}
94
95
	/**
96
	 * This should not be needed except perhaps by persistent connections
97
	 *
98
	 * Closes the connection to the MySQL database. After closing this connection,
99
	 * this instance is no longer valid, and will subsequently throw exceptions when
100
	 * attempting to perform database operations.
101
	 *
102
	 * You must call Database::getInstance() again to retrieve a valid instance that
103
	 * is reconnected to the database.
104
	 *
105
	 * @return bool Whether the underlying connection was closed by this call.
106
	 */
107
	public function close() : bool {
108
		if (!isset($this->dbConn)) {
109
			// Connection is already closed; nothing to do.
110
			return false;
111
		}
112
		$this->dbConn->close();
113
		unset($this->dbConn);
114
		// Set the mysqli instance in the dependency injection container to
115
		// null so that the Database constructor will reconnect the next time
116
		// it is called.
117
		DiContainer::getContainer()->set(mysqli::class, null);
118
		return true;
119
	}
120
121
	public function query($query) {
122
		$this->dbResult = $this->dbConn->query($query);
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->dbConn->query($query) can also be of type mysqli_result. However, the property $dbResult is declared as type Smr\mysqli_result|boolean. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
123
	}
124
125
	/**
126
	 * Use to populate this instance with the next record of the active query.
127
	 */
128
	public function nextRecord(): bool {
129
		if (!$this->dbResult) {
130
			$this->error('No resource to get record from.');
131
		}
132
		if ($this->dbRecord = $this->dbResult->fetch_assoc()) {
133
			return true;
134
		}
135
		return false;
136
	}
137
138
	/**
139
	 * Use instead of nextRecord when exactly one record is expected from the
140
	 * active query.
141
	 */
142
	public function requireRecord(): void {
143
		if (!$this->nextRecord() || $this->getNumRows() != 1) {
144
			$this->error('One record required, but found ' . $this->getNumRows());
145
		}
146
	}
147
148
	public function hasField($name) {
149
		return isset($this->dbRecord[$name]);
150
	}
151
152
	public function getField($name) {
153
		return $this->dbRecord[$name];
154
	}
155
156
	public function getBoolean(string $name) : bool {
157
		if ($this->dbRecord[$name] === 'TRUE') {
158
			return true;
159
		} elseif ($this->dbRecord[$name] === 'FALSE') {
160
			return false;
161
		}
162
		$this->error('Field is not a boolean: ' . $name);
163
	}
164
165
	public function getInt($name) {
166
		return (int)$this->dbRecord[$name];
167
	}
168
169
	public function getFloat($name) {
170
		return (float)$this->dbRecord[$name];
171
	}
172
173
	public function getMicrotime(string $name) : string {
174
		// All digits of precision are stored in a MySQL bigint
175
		$data = $this->dbRecord[$name];
176
		return sprintf('%f', $data / 1E6);
177
	}
178
179
	public function getObject($name, $compressed = false, $nullable = false) {
180
		$object = $this->getField($name);
181
		if ($nullable === true && $object === null) {
182
			return null;
183
		}
184
		if ($compressed === true) {
185
			$object = gzuncompress($object);
186
		}
187
		return unserialize($object);
188
	}
189
190
	public function getRow() {
191
		return $this->dbRecord;
192
	}
193
194
	public function lockTable($table) {
195
		$this->dbConn->query('LOCK TABLES ' . $table . ' WRITE');
196
	}
197
198
	public function unlock() {
199
		$this->dbConn->query('UNLOCK TABLES');
200
	}
201
202
	public function getNumRows() {
203
		return $this->dbResult->num_rows;
0 ignored issues
show
Bug introduced by
The property num_rows does not exist on boolean.
Loading history...
204
	}
205
206
	public function getChangedRows() {
207
		return $this->dbConn->affected_rows;
208
	}
209
210
	public function getInsertID() {
211
		return $this->dbConn->insert_id;
212
	}
213
214
	protected function error($err) {
215
		throw new RuntimeException($err);
216
	}
217
218
	public function escape($escape) {
219
		if (is_bool($escape)) {
220
			return $this->escapeBoolean($escape);
221
		}
222
		if (is_numeric($escape)) {
223
			return $this->escapeNumber($escape);
224
		}
225
		if (is_string($escape)) {
226
			return $this->escapeString($escape);
227
		}
228
		if (is_array($escape)) {
229
			return $this->escapeArray($escape);
230
		}
231
		if (is_object($escape)) {
232
			return $this->escapeObject($escape);
233
		}
234
	}
235
236
	public function escapeString(?string $string, bool $nullable = false) : string {
237
		if ($nullable === true && ($string === null || $string === '')) {
238
			return 'NULL';
239
		}
240
		return '\'' . $this->dbConn->real_escape_string($string) . '\'';
241
	}
242
243
	public function escapeBinary($binary) {
244
		return '0x' . bin2hex($binary);
245
	}
246
247
	/**
248
	 * Warning: If escaping a nested array, use escapeIndividually=true,
249
	 * but beware that the escaped array is flattened!
250
	 */
251
	public function escapeArray(array $array, string $delimiter = ',', bool $escapeIndividually = true) : string {
252
		if ($escapeIndividually) {
253
			$string = join($delimiter, array_map(function($item) { return $this->escape($item); }, $array));
254
		} else {
255
			$string = $this->escape(join($delimiter, $array));
256
		}
257
		return $string;
258
	}
259
260
	public function escapeNumber($num) {
261
		// Numbers need not be quoted in MySQL queries, so if we know $num is
262
		// numeric, we can simply return its value (no quoting or escaping).
263
		if (is_numeric($num)) {
264
			return $num;
265
		} else {
266
			$this->error('Not a number! (' . $num . ')');
267
		}
268
	}
269
270
	public function escapeMicrotime(float $microtime) : string {
271
		// Retain all digits of precision for storing in a MySQL bigint
272
		return sprintf('%d', $microtime * 1E6);
273
	}
274
275
	public function escapeBoolean(bool $bool) : string {
276
		// We store booleans as an enum
277
		if ($bool) {
278
			return '\'TRUE\'';
279
		} else {
280
			return '\'FALSE\'';
281
		}
282
	}
283
284
	public function escapeObject($object, bool $compress = false, bool $nullable = false) : string {
285
		if ($nullable === true && $object === null) {
286
			return 'NULL';
287
		}
288
		if ($compress === true) {
289
			return $this->escapeBinary(gzcompress(serialize($object)));
290
		}
291
		return $this->escapeString(serialize($object));
292
	}
293
}
294