Completed
Push — master ( 2041e7...c131ba )
by Ron
02:14
created

MySQL::transaction()   D

Complexity

Conditions 10
Paths 45

Size

Total Lines 44
Code Lines 37

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 44
c 0
b 0
f 0
rs 4.8196
cc 10
eloc 37
nc 45
nop 2

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
namespace Kir\MySQL\Databases;
3
4
use PDO;
5
use PDOException;
6
use RuntimeException;
7
use UnexpectedValueException;
8
use Kir\MySQL\Builder;
9
use Kir\MySQL\Builder\Exception;
10
use Kir\MySQL\Builder\QueryStatement;
11
use Kir\MySQL\Database;
12
use Kir\MySQL\Databases\MySQL\MySQLExceptionInterpreter;
13
use Kir\MySQL\QueryLogger\QueryLoggers;
14
use Kir\MySQL\Tools\AliasRegistry;
15
16
/**
17
 */
18
class MySQL implements Database {
19
	/** @var array */
20
	private static $tableFields = array();
21
	/** @var PDO */
22
	private $pdo;
23
	/** @var bool */
24
	private $outerTransaction = false;
25
	/** @var AliasRegistry */
26
	private $aliasRegistry;
27
	/** @var int */
28
	private $transactionLevel = 0;
29
	/** @var QueryLoggers */
30
	private $queryLoggers = 0;
31
	/** @var MySQLExceptionInterpreter */
32
	private $exceptionInterpreter = 0;
33
	/** @var array */
34
	private $options;
35
	
36
	/**
37
	 * @param PDO $pdo
38
	 * @param array $options
39
	 */
40
	public function __construct(PDO $pdo, array $options = []) {
41
		if($pdo->getAttribute(PDO::ATTR_ERRMODE) === PDO::ERRMODE_SILENT) {
42
			$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
43
		}
44
		$this->pdo = $pdo;
45
		$this->aliasRegistry = new AliasRegistry();
46
		$this->queryLoggers = new QueryLoggers();
47
		$this->exceptionInterpreter = new MySQLExceptionInterpreter();
48
		$defaultOptions = [
49
			'select-options' => [],
50
			'insert-options' => [],
51
			'update-options' => [],
52
			'delete-options' => [],
53
		];
54
		$this->options = array_merge($defaultOptions, $options);
55
	}
56
57
	/**
58
	 * @return QueryLoggers
59
	 */
60
	public function getQueryLoggers() {
61
		return $this->queryLoggers;
62
	}
63
64
	/**
65
	 * @return AliasRegistry
66
	 */
67
	public function getAliasRegistry() {
68
		return $this->aliasRegistry;
69
	}
70
71
	/**
72
	 * @param string $query
73
	 * @throws Exception
74
	 * @return QueryStatement
75
	 */
76
	public function query($query) {
77
		return $this->buildQueryStatement($query, function ($query) {
78
			$stmt = $this->pdo->query($query);
79
			return $stmt;
80
		});
81
	}
82
83
	/**
84
	 * @param string $query
85
	 * @throws Exception
86
	 * @return QueryStatement
87
	 */
88
	public function prepare($query) {
89
		return $this->buildQueryStatement((string) $query, function ($query) {
90
			$stmt = $this->pdo->prepare($query);
91
			return $stmt;
92
		});
93
	}
94
95
	/**
96
	 * @param string $query
97
	 * @param array $params
98
	 * @return int
99
	 */
100
	public function exec($query, array $params = array()) {
101
		return $this->exceptionHandler(function () use ($query, $params) {
102
			$stmt = $this->pdo->prepare($query);
103
			$timer = microtime(true);
104
			$stmt->execute($params);
105
			$this->queryLoggers->log($query, microtime(true) - $timer);
106
			$result = $stmt->rowCount();
107
			$stmt->closeCursor();
108
			return $result;
109
		});
110
	}
111
112
	/**
113
	 * @return string
114
	 */
115
	public function getLastInsertId() {
116
		return $this->pdo->lastInsertId();
117
	}
118
119
	/**
120
	 * @param string $table
121
	 * @return array
122
	 */
123
	public function getTableFields($table) {
124
		$table = $this->select()->aliasReplacer()->replace($table);
125
		if(array_key_exists($table, self::$tableFields)) {
126
			return self::$tableFields[$table];
127
		}
128
		$stmt = $this->pdo->query("DESCRIBE {$table}");
129
		$rows = $stmt->fetchAll(\PDO::FETCH_ASSOC);
130
		self::$tableFields[$table] = array_map(function ($row) { return $row['Field']; }, $rows);
131
		$stmt->closeCursor();
132
		return self::$tableFields[$table];
133
	}
134
135
	/**
136
	 * @param mixed $expression
137
	 * @param array $arguments
138
	 * @return string
139
	 */
140
	public function quoteExpression($expression, array $arguments = array()) {
141
		$func = function () use ($arguments) {
142
			static $idx = -1;
143
			$idx++;
144
			$index = $idx;
145
			if(array_key_exists($index, $arguments)) {
146
				$argument = $arguments[$index];
147
				$value = $this->quote($argument);
148
			} else {
149
				$value = 'NULL';
150
			}
151
			return $value;
152
		};
153
		$result = preg_replace_callback('/(\\?)/', $func, $expression);
154
		return (string) $result;
155
	}
156
157
	/**
158
	 * @param mixed $value
159
	 * @return string
160
	 */
161
	public function quote($value) {
162
		if(is_null($value)) {
163
			$result = 'NULL';
164
		} elseif($value instanceof Builder\DBExpr) {
165
			$result = $value->getExpression();
166
		} elseif($value instanceof Builder\Select) {
167
			$result = sprintf('(%s)', (string) $value);
168
		} elseif(is_array($value)) {
169
			$result = join(', ', array_map(function ($value) { return $this->quote($value); }, $value));
170
		} else {
171
			$result = $this->pdo->quote($value);
172
		}
173
		return $result;
174
	}
175
176
	/**
177
	 * @param string $field
178
	 * @return string
179
	 */
180
	public function quoteField($field) {
181
		if (is_numeric($field) || !is_string($field)) {
182
			throw new UnexpectedValueException('Field name is invalid');
183
		}
184
		if(strpos($field, '`') !== false) {
185
			return (string) $field;
186
		}
187
		$parts = explode('.', $field);
188
		return '`'.join('`.`', $parts).'`';
189
	}
190
191
	/**
192
	 * @param array $fields
193
	 * @return Builder\RunnableSelect
194
	 */
195 View Code Duplication
	public function select(array $fields = null) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
196
		$select = array_key_exists('select-factory', $this->options)
197
			? call_user_func($this->options['select-factory'], $this, $this->options['select-options'])
198
			: new Builder\RunnableSelect($this, $this->options['select-options']);
199
		if($fields !== null) {
200
			$select->fields($fields);
201
		}
202
		return $select;
203
	}
204
205
	/**
206
	 * @param array $fields
207
	 * @return Builder\RunnableInsert
208
	 */
209 View Code Duplication
	public function insert(array $fields = null) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
210
		$insert = array_key_exists('insert-factory', $this->options)
211
			? call_user_func($this->options['insert-factory'], $this, $this->options['insert-options'])
212
			: new Builder\RunnableInsert($this, $this->options['insert-options']);
213
		if($fields !== null) {
214
			$insert->addAll($fields);
215
		}
216
		return $insert;
217
	}
218
219
	/**
220
	 * @param array $fields
221
	 * @return Builder\RunnableUpdate
222
	 */
223 View Code Duplication
	public function update(array $fields = null) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
224
		$update = array_key_exists('update-factory', $this->options)
225
			? call_user_func($this->options['update-factory'], $this, $this->options['update-options'])
226
			: new Builder\RunnableUpdate($this, $this->options['update-options']);
227
		if($fields !== null) {
228
			$update->setAll($fields);
229
		}
230
		return $update;
231
	}
232
233
	/**
234
	 * @return Builder\RunnableDelete
235
	 */
236
	public function delete() {
237
		return array_key_exists('delete-factory', $this->options)
238
			? call_user_func($this->options['delete-factory'], $this, $this->options['delete-options'])
239
			: new Builder\RunnableDelete($this, $this->options['delete-options']);
240
	}
241
242
	/**
243
	 * @return $this
244
	 */
245
	public function transactionStart() {
246
		if((int) $this->transactionLevel === 0) {
247
			if($this->pdo->inTransaction()) {
248
				$this->outerTransaction = true;
249
			} else {
250
				$this->pdo->beginTransaction();
251
			}
252
		}
253
		$this->transactionLevel++;
254
		return $this;
255
	}
256
257
	/**
258
	 * @return $this
259
	 * @throws \Exception
260
	 */
261
	public function transactionCommit() {
262
		return $this->transactionEnd(function () {
263
			$this->pdo->commit();
264
		});
265
	}
266
267
	/**
268
	 * @return $this
269
	 * @throws \Exception
270
	 */
271
	public function transactionRollback() {
272
		return $this->transactionEnd(function () {
273
			$this->pdo->rollBack();
274
		});
275
	}
276
277
	/**
278
	 * @param callable|null $callback
279
	 * @return mixed
280
	 * @throws \Exception
281
	 * @throws \Error
282
	 * @throws null
283
	 */
284
	public function dryRun($callback = null) {
285
		$result = null;
286
		$exception = null;
0 ignored issues
show
Unused Code introduced by
$exception is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
287
		if(!$this->pdo->inTransaction()) {
288
			$this->transactionStart();
289
			try {
290
				$result = call_user_func($callback, $this);
291
				$this->transactionRollback();
292
			} catch (\Exception $e) {
293
				$this->transactionRollback();
294
				throw $e;
295
			} catch (\Error $e) {
0 ignored issues
show
Bug introduced by
The class Error does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
296
				$this->transactionRollback();
297
				throw $e;
298
			}
299
		} else {
300
			$uniqueId = $this->genUniqueId();
301
			$this->exec("SAVEPOINT {$uniqueId}");
302
			try {
303
				$result = call_user_func($callback, $this);
304
				$this->exec("ROLLBACK TO {$uniqueId}");
305
			} catch (\Exception $e) {
306
				$this->exec("ROLLBACK TO {$uniqueId}");
307
				throw $e;
308
			} catch (\Error $e) {
0 ignored issues
show
Bug introduced by
The class Error does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
309
				$this->exec("ROLLBACK TO {$uniqueId}");
310
				throw $e;
311
			}
312
		}
313
		return $result;
314
	}
315
	
316
	/**
317
	 * @param int|callable $tries
318
	 * @param callable|null $callback
319
	 * @return mixed
320
	 * @throws \Exception
321
	 * @throws \Error
322
	 */
323
	public function transaction($tries = 1, $callback = null) {
324
		if(is_callable($tries)) {
325
			$callback = $tries;
326
			$tries = 1;
327
		} elseif(!is_callable($callback)) {
328
			throw new RuntimeException('$callback must be a callable');
329
		}
330
		$result = null;
331
		$exception = null;
332
		for(; $tries--;) {
333
			if(!$this->pdo->inTransaction()) {
334
				$this->transactionStart();
335
				try {
336
					$result = call_user_func($callback, $this);
337
					$exception = null;
338
					$this->transactionCommit();
339
				} catch (\Exception $e) {
340
					$this->transactionRollback();
341
					$exception = $e;
342
				} catch (\Error $e) {
0 ignored issues
show
Bug introduced by
The class Error does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
343
					$this->transactionRollback();
344
					$exception = $e;
345
				}
346
			} else {
347
				$uniqueId = $this->genUniqueId();
348
				$this->exec("SAVEPOINT {$uniqueId}");
349
				try {
350
					$result = call_user_func($callback, $this);
351
					$exception = null;
352
					$this->exec("RELEASE SAVEPOINT {$uniqueId}");
353
				} catch (\Exception $e) {
354
					$this->exec("ROLLBACK TO {$uniqueId}");
355
					$exception = $e;
356
				} catch (\Error $e) {
0 ignored issues
show
Bug introduced by
The class Error does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
357
					$this->exec("ROLLBACK TO {$uniqueId}");
358
					$exception = $e;
359
				}
360
			}
361
		}
362
		if($exception !== null) {
363
			throw $exception;
364
		}
365
		return $result;
366
	}
367
368
	/**
369
	 * @param callable $fn
370
	 * @return $this
371
	 * @throws RuntimeException
372
	 */
373
	private function transactionEnd($fn) {
374
		$this->transactionLevel--;
375
		if($this->transactionLevel < 0) {
376
			throw new RuntimeException("Transaction-Nesting-Problem: Trying to invoke commit on a already closed transaction");
377
		}
378
		if((int) $this->transactionLevel === 0) {
379
			if($this->outerTransaction) {
380
				$this->outerTransaction = false;
381
			} else {
382
				call_user_func($fn);
383
			}
384
		}
385
		return $this;
386
	}
387
388
	/**
389
	 * @param string $query
390
	 * @param callable $fn
391
	 * @return QueryStatement
392
	 * @throws RuntimeException
393
	 */
394
	private function buildQueryStatement($query, $fn) {
395
		$stmt = call_user_func($fn, $query);
396
		if(!$stmt) {
397
			throw new RuntimeException("Could not execute statement:\n{$query}");
398
		}
399
		$stmtWrapper = new QueryStatement($stmt, $query, $this->exceptionInterpreter, $this->queryLoggers);
400
		return $stmtWrapper;
401
	}
402
403
	/**
404
	 * @param callable $fn
405
	 * @return mixed
406
	 */
407
	private function exceptionHandler($fn) {
408
		try {
409
			return call_user_func($fn);
410
		} catch (PDOException $e) {
411
			$this->exceptionInterpreter->throwMoreConcreteException($e);
412
		}
413
		return null;
414
	}
415
416
	/**
417
	 * @return string
418
	 */
419
	private function genUniqueId() {
420
		// Generate a unique id from a former random-uuid-generator
421
		return sprintf('ID%04x%04x%04x%04x%04x%04x%04x%04x',
422
			mt_rand(0, 0xffff),
423
			mt_rand(0, 0xffff),
424
			mt_rand(0, 0xffff),
425
			mt_rand(0, 0x0fff) | 0x4000,
426
			mt_rand(0, 0x3fff) | 0x8000,
427
			mt_rand(0, 0xffff),
428
			mt_rand(0, 0xffff),
429
			mt_rand(0, 0xffff)
430
		);
431
	}
432
}
433