Passed
Push — master ( 9e2434...486cd6 )
by Ron
01:59
created

MySQL   B

Complexity

Total Complexity 48

Size/Duplication

Total Lines 338
Duplicated Lines 0 %

Importance

Changes 30
Bugs 2 Features 2
Metric Value
wmc 48
eloc 135
c 30
b 2
f 2
dl 0
loc 338
rs 8.5599

24 Methods

Rating   Name   Duplication   Size   Complexity  
A quoteExpression() 0 2 1
A transactionRollback() 0 3 1
A quote() 0 2 1
A update() 0 8 3
A transactionEnd() 0 13 4
A getQueryLoggers() 0 2 1
A getTableFields() 0 13 4
A transactionCommit() 0 3 1
A getLastInsertId() 0 2 1
A exec() 0 9 1
A dryRun() 0 15 2
A quoteField() 0 2 1
A insert() 0 8 3
A query() 0 3 1
A transactionStart() 0 10 3
A __construct() 0 15 2
A buildQueryStatement() 0 6 2
A getVirtualTables() 0 5 2
A getAliasRegistry() 0 2 1
A select() 0 8 3
A exceptionHandler() 0 7 2
A prepare() 0 3 1
A delete() 0 4 2
A transaction() 0 23 5

How to fix   Complexity   

Complex Class

Complex classes like MySQL 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 MySQL, and based on these observations, apply Extract Interface, too.

1
<?php
2
namespace Kir\MySQL\Databases;
3
4
use Kir\MySQL\Builder;
5
use Kir\MySQL\Builder\DBExpr;
6
use Kir\MySQL\Builder\QueryStatement;
7
use Kir\MySQL\Builder\Select;
8
use Kir\MySQL\Database;
9
use Kir\MySQL\Databases\MySQL\MySQLExceptionInterpreter;
10
use Kir\MySQL\Databases\MySQL\MySQLExpressionQuoter;
11
use Kir\MySQL\Databases\MySQL\MySQLFieldQuoter;
12
use Kir\MySQL\Databases\MySQL\MySQLRunnableSelect;
13
use Kir\MySQL\Databases\MySQL\MySQLUUIDGenerator;
14
use Kir\MySQL\Databases\MySQL\MySQLValueQuoter;
15
use Kir\MySQL\QueryLogger\QueryLoggers;
16
use Kir\MySQL\Tools\AliasRegistry;
17
use Kir\MySQL\Tools\VirtualTables;
18
use PDO;
19
use PDOException;
20
use RuntimeException;
21
use Throwable;
22
use phpDocumentor\Reflection\Types\Scalar;
23
24
/**
25
 */
26
class MySQL implements Database {
27
	/** @var array<string, array<int, string>> */
28
	private $tableFields = [];
29
	/** @var PDO */
30
	private $pdo;
31
	/** @var bool */
32
	private $outerTransaction = false;
33
	/** @var AliasRegistry */
34
	private $aliasRegistry;
35
	/** @var int */
36
	private $transactionLevel = 0;
37
	/** @var QueryLoggers */
38
	private $queryLoggers;
39
	/** @var VirtualTables */
40
	private $virtualTables;
41
	/** @var MySQLExceptionInterpreter */
42
	private $exceptionInterpreter;
43
	/** @var array<string, mixed> */
44
	private $options;
45
46
	/**
47
	 * @param PDO $pdo
48
	 * @param array<string, mixed> $options
49
	 */
50
	public function __construct(PDO $pdo, array $options = []) {
51
		if($pdo->getAttribute(PDO::ATTR_ERRMODE) === PDO::ERRMODE_SILENT) {
52
			$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
53
		}
54
		$this->pdo = $pdo;
55
		$this->aliasRegistry = new AliasRegistry();
56
		$this->queryLoggers = new QueryLoggers();
57
		$this->exceptionInterpreter = new MySQLExceptionInterpreter();
58
		$defaultOptions = [
59
			'select-options' => [],
60
			'insert-options' => [],
61
			'update-options' => [],
62
			'delete-options' => [],
63
		];
64
		$this->options = array_merge($defaultOptions, $options);
65
	}
66
67
	/**
68
	 * @return QueryLoggers
69
	 */
70
	public function getQueryLoggers(): QueryLoggers {
71
		return $this->queryLoggers;
72
	}
73
74
	/**
75
	 * @return AliasRegistry
76
	 */
77
	public function getAliasRegistry(): AliasRegistry {
78
		return $this->aliasRegistry;
79
	}
80
81
	/**
82
	 * @return VirtualTables
83
	 */
84
	public function getVirtualTables(): VirtualTables {
85
		if($this->virtualTables === null) {
86
			$this->virtualTables = new VirtualTables();
87
		}
88
		return $this->virtualTables;
89
	}
90
91
	/**
92
	 * @param string $query
93
	 * @return QueryStatement
94
	 */
95
	public function query(string $query) {
96
		return $this->buildQueryStatement($query, function ($query) {
97
			return $this->pdo->query($query);
98
		});
99
	}
100
101
	/**
102
	 * @param string $query
103
	 * @return QueryStatement
104
	 */
105
	public function prepare(string $query) {
106
		return $this->buildQueryStatement($query, function ($query) {
107
			return $this->pdo->prepare($query);
108
		});
109
	}
110
111
	/**
112
	 * @param string $query
113
	 * @param array<string, mixed> $params
114
	 * @return int
115
	 */
116
	public function exec(string $query, array $params = []): int {
117
		return $this->exceptionHandler(function () use ($query, $params) {
118
			$stmt = $this->pdo->prepare($query);
119
			$timer = microtime(true);
120
			$stmt->execute($params);
121
			$this->queryLoggers->log($query, microtime(true) - $timer);
122
			$result = $stmt->rowCount();
123
			$stmt->closeCursor();
124
			return $result;
125
		});
126
	}
127
128
	/**
129
	 * @param string|null $name
130
	 * @return string
131
	 */
132
	public function getLastInsertId(?string $name = null): string {
133
		return $this->pdo->lastInsertId();
134
	}
135
136
	/**
137
	 * @param string $table
138
	 * @return array<int, string>
139
	 */
140
	public function getTableFields(string $table): array {
141
		$table = $this->select()->aliasReplacer()->replace($table);
142
		if(array_key_exists($table, $this->tableFields)) {
143
			return $this->tableFields[$table];
144
		}
145
		$stmt = $this->pdo->query("DESCRIBE {$table}");
146
		if($stmt === false) {
147
			throw new RuntimeException('Invalid return type');
148
		}
149
		$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
150
		$this->tableFields[$table] = array_map(static function ($row) { return $row['Field']; }, $rows ?: []);
151
		$stmt->closeCursor();
152
		return $this->tableFields[$table];
153
	}
154
155
	/**
156
	 * @param string $expression
157
	 * @param array<int, null|scalar|array<int, string>|DBExpr|Select> $arguments
158
	 * @return string
159
	 */
160
	public function quoteExpression(string $expression, array $arguments = []): string {
161
		return MySQLExpressionQuoter::quoteExpression($this->pdo, $expression, $arguments);
162
	}
163
164
	/**
165
	 * @param null|scalar|array<int, string>|DBExpr|Select $value
166
	 * @return string
167
	 */
168
	public function quote($value): string {
169
		return MySQLValueQuoter::quote($this->pdo, $value);
170
	}
171
172
	/**
173
	 * @param string $field
174
	 * @return string
175
	 */
176
	public function quoteField(string $field): string {
177
		return MySQLFieldQuoter::quoteField($field);
178
	}
179
180
	/**
181
	 * @param array<string|int, string>|null $fields
182
	 * @return MySQLRunnableSelect
183
	 */
184
	public function select(array $fields = null) {
185
		$select = array_key_exists('select-factory', $this->options)
186
			? call_user_func($this->options['select-factory'], $this, $this->options['select-options'])
187
			: new MySQL\MySQLRunnableSelect($this, $this->options['select-options']);
188
		if($fields !== null) {
189
			$select->fields($fields);
190
		}
191
		return $select;
192
	}
193
194
	/**
195
	 * @param null|array<string|int, string> $fields
196
	 * @return Builder\RunnableInsert
197
	 */
198
	public function insert(array $fields = null) {
199
		$insert = array_key_exists('insert-factory', $this->options)
200
			? call_user_func($this->options['insert-factory'], $this, $this->options['insert-options'])
201
			: new Builder\RunnableInsert($this, $this->options['insert-options']);
202
		if($fields !== null) {
203
			$insert->addAll($fields);
204
		}
205
		return $insert;
206
	}
207
208
	/**
209
	 * @param array<string|int, string>|null $fields
210
	 * @return Builder\RunnableUpdate
211
	 */
212
	public function update(array $fields = null) {
213
		$update = array_key_exists('update-factory', $this->options)
214
			? call_user_func($this->options['update-factory'], $this, $this->options['update-options'])
215
			: new Builder\RunnableUpdate($this, $this->options['update-options']);
216
		if($fields !== null) {
217
			$update->setAll($fields);
218
		}
219
		return $update;
220
	}
221
222
	/**
223
	 * @return Builder\RunnableDelete
224
	 */
225
	public function delete() {
226
		return array_key_exists('delete-factory', $this->options)
227
			? call_user_func($this->options['delete-factory'], $this, $this->options['delete-options'])
228
			: new Builder\RunnableDelete($this, $this->options['delete-options']);
229
	}
230
231
	/**
232
	 * @return $this
233
	 */
234
	public function transactionStart() {
235
		if($this->transactionLevel === 0) {
236
			if($this->pdo->inTransaction()) {
237
				$this->outerTransaction = true;
238
			} else {
239
				$this->pdo->beginTransaction();
240
			}
241
		}
242
		$this->transactionLevel++;
243
		return $this;
244
	}
245
246
	/**
247
	 * @return $this
248
	 */
249
	public function transactionCommit() {
250
		return $this->transactionEnd(function () {
251
			$this->pdo->commit();
252
		});
253
	}
254
255
	/**
256
	 * @return $this
257
	 */
258
	public function transactionRollback() {
259
		return $this->transactionEnd(function () {
260
			$this->pdo->rollBack();
261
		});
262
	}
263
264
	/**
265
	 * @template T
266
	 * @param callable(MySQL): T $callback
267
	 * @return T
268
	 */
269
	public function dryRun(callable $callback) {
270
		if(!$this->pdo->inTransaction()) {
271
			$this->transactionStart();
272
			try {
273
				return $callback($this);
274
			} finally {
275
				$this->transactionRollback();
276
			}
277
		} else {
278
			$uniqueId = MySQLUUIDGenerator::genUUIDv4();
279
			$this->exec("SAVEPOINT {$uniqueId}");
280
			try {
281
				return $callback($this);
282
			} finally {
283
				$this->exec("ROLLBACK TO {$uniqueId}");
284
			}
285
		}
286
	}
287
288
	/**
289
	 * @template T
290
	 * @param callable(MySQL): T $callback
291
	 * @return T
292
	 * @throws Throwable
293
	 */
294
	public function transaction(callable $callback) {
295
		if(!$this->pdo->inTransaction()) {
296
			$this->transactionStart();
297
			try {
298
				$result = $callback($this);
299
				$this->transactionCommit();
300
				return $result;
301
			} catch (Throwable $e) {
302
				if($this->pdo->inTransaction()) {
303
					$this->transactionRollback();
304
				}
305
				throw $e;
306
			}
307
		}
308
		$uniqueId = MySQLUUIDGenerator::genUUIDv4();
309
		$this->exec("SAVEPOINT {$uniqueId}");
310
		try {
311
			$result = $callback($this);
312
			$this->exec("RELEASE SAVEPOINT {$uniqueId}");
313
			return $result;
314
		} catch (Throwable $e) {
315
			$this->exec("ROLLBACK TO {$uniqueId}");
316
			throw $e;
317
		}
318
	}
319
320
	/**
321
	 * @param callable(): void $fn
322
	 * @return $this
323
	 */
324
	private function transactionEnd(callable $fn): self {
325
		$this->transactionLevel--;
326
		if($this->transactionLevel < 0) {
327
			throw new RuntimeException("Transaction-Nesting-Problem: Trying to invoke commit on a already closed transaction");
328
		}
329
		if($this->transactionLevel < 1) {
330
			if($this->outerTransaction) {
331
				$this->outerTransaction = false;
332
			} else {
333
				$fn();
334
			}
335
		}
336
		return $this;
337
	}
338
339
340
	/**
341
	 * @param string $query
342
	 * @param callable $fn
343
	 * @return QueryStatement
344
	 */
345
	private function buildQueryStatement(string $query, callable $fn): QueryStatement {
346
		$stmt = $fn($query);
347
		if(!$stmt) {
348
			throw new RuntimeException("Could not execute statement:\n{$query}");
349
		}
350
		return new QueryStatement($stmt, $query, $this->exceptionInterpreter, $this->queryLoggers);
351
	}
352
353
	/**
354
	 * @param callable $fn
355
	 * @return mixed
356
	 */
357
	private function exceptionHandler(callable $fn) {
358
		try {
359
			return $fn();
360
		} catch (PDOException $e) {
361
			$this->exceptionInterpreter->throwMoreConcreteException($e);
362
		}
363
		return null;
364
	}
365
}
366