Completed
Push — master ( c131ba...eae540 )
by Ron
03:34
created

MySQL::getVirtualTables()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 6
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 4
nc 2
nop 0
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
use Kir\MySQL\Tools\VirtualTables;
16
17
/**
18
 */
19
class MySQL implements Database {
20
	/** @var array */
21
	private static $tableFields = array();
22
	/** @var PDO */
23
	private $pdo;
24
	/** @var bool */
25
	private $outerTransaction = false;
26
	/** @var AliasRegistry */
27
	private $aliasRegistry;
28
	/** @var int */
29
	private $transactionLevel = 0;
30
	/** @var QueryLoggers */
31
	private $queryLoggers = 0;
32
	/** @var VirtualTables */
33
	private $virtualTables = null;
34
	/** @var MySQLExceptionInterpreter */
35
	private $exceptionInterpreter = 0;
36
	/** @var array */
37
	private $options;
38
	
39
	/**
40
	 * @param PDO $pdo
41
	 * @param array $options
42
	 */
43
	public function __construct(PDO $pdo, array $options = []) {
44
		if($pdo->getAttribute(PDO::ATTR_ERRMODE) === PDO::ERRMODE_SILENT) {
45
			$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
46
		}
47
		$this->pdo = $pdo;
48
		$this->aliasRegistry = new AliasRegistry();
49
		$this->queryLoggers = new QueryLoggers();
50
		$this->exceptionInterpreter = new MySQLExceptionInterpreter();
51
		$defaultOptions = [
52
			'select-options' => [],
53
			'insert-options' => [],
54
			'update-options' => [],
55
			'delete-options' => [],
56
		];
57
		$this->options = array_merge($defaultOptions, $options);
58
	}
59
60
	/**
61
	 * @return QueryLoggers
62
	 */
63
	public function getQueryLoggers() {
64
		return $this->queryLoggers;
65
	}
66
67
	/**
68
	 * @return AliasRegistry
69
	 */
70
	public function getAliasRegistry() {
71
		return $this->aliasRegistry;
72
	}
73
74
	/**
75
	 * @return VirtualTables
76
	 */
77
	public function getVirtualTables() {
78
		if($this->virtualTables === null) {
79
			$this->virtualTables = new VirtualTables();
80
		}
81
		return $this->virtualTables;
82
	}
83
84
	/**
85
	 * @param string $query
86
	 * @throws Exception
87
	 * @return QueryStatement
88
	 */
89
	public function query($query) {
90
		return $this->buildQueryStatement($query, function ($query) {
91
			$stmt = $this->pdo->query($query);
92
			return $stmt;
93
		});
94
	}
95
96
	/**
97
	 * @param string $query
98
	 * @throws Exception
99
	 * @return QueryStatement
100
	 */
101
	public function prepare($query) {
102
		return $this->buildQueryStatement((string) $query, function ($query) {
103
			$stmt = $this->pdo->prepare($query);
104
			return $stmt;
105
		});
106
	}
107
108
	/**
109
	 * @param string $query
110
	 * @param array $params
111
	 * @return int
112
	 */
113
	public function exec($query, array $params = array()) {
114
		return $this->exceptionHandler(function () use ($query, $params) {
115
			$stmt = $this->pdo->prepare($query);
116
			$timer = microtime(true);
117
			$stmt->execute($params);
118
			$this->queryLoggers->log($query, microtime(true) - $timer);
119
			$result = $stmt->rowCount();
120
			$stmt->closeCursor();
121
			return $result;
122
		});
123
	}
124
125
	/**
126
	 * @return string
127
	 */
128
	public function getLastInsertId() {
129
		return $this->pdo->lastInsertId();
130
	}
131
132
	/**
133
	 * @param string $table
134
	 * @return array
135
	 */
136
	public function getTableFields($table) {
137
		$table = $this->select()->aliasReplacer()->replace($table);
138
		if(array_key_exists($table, self::$tableFields)) {
139
			return self::$tableFields[$table];
140
		}
141
		$stmt = $this->pdo->query("DESCRIBE {$table}");
142
		$rows = $stmt->fetchAll(\PDO::FETCH_ASSOC);
143
		self::$tableFields[$table] = array_map(function ($row) { return $row['Field']; }, $rows);
144
		$stmt->closeCursor();
145
		return self::$tableFields[$table];
146
	}
147
148
	/**
149
	 * @param mixed $expression
150
	 * @param array $arguments
151
	 * @return string
152
	 */
153
	public function quoteExpression($expression, array $arguments = array()) {
154
		$func = function () use ($arguments) {
155
			static $idx = -1;
156
			$idx++;
157
			$index = $idx;
158
			if(array_key_exists($index, $arguments)) {
159
				$argument = $arguments[$index];
160
				$value = $this->quote($argument);
161
			} else {
162
				$value = 'NULL';
163
			}
164
			return $value;
165
		};
166
		$result = preg_replace_callback('/(\\?)/', $func, $expression);
167
		return (string) $result;
168
	}
169
170
	/**
171
	 * @param mixed $value
172
	 * @return string
173
	 */
174
	public function quote($value) {
175
		if(is_null($value)) {
176
			$result = 'NULL';
177
		} elseif($value instanceof Builder\DBExpr) {
178
			$result = $value->getExpression();
179
		} elseif($value instanceof Builder\Select) {
180
			$result = sprintf('(%s)', (string) $value);
181
		} elseif(is_array($value)) {
182
			$result = join(', ', array_map(function ($value) { return $this->quote($value); }, $value));
183
		} else {
184
			$result = $this->pdo->quote($value);
185
		}
186
		return $result;
187
	}
188
189
	/**
190
	 * @param string $field
191
	 * @return string
192
	 */
193
	public function quoteField($field) {
194
		if (is_numeric($field) || !is_string($field)) {
195
			throw new UnexpectedValueException('Field name is invalid');
196
		}
197
		if(strpos($field, '`') !== false) {
198
			return (string) $field;
199
		}
200
		$parts = explode('.', $field);
201
		return '`'.join('`.`', $parts).'`';
202
	}
203
204
	/**
205
	 * @param array $fields
206
	 * @return Builder\RunnableSelect
207
	 */
208 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...
209
		$select = array_key_exists('select-factory', $this->options)
210
			? call_user_func($this->options['select-factory'], $this, $this->options['select-options'])
211
			: new Builder\RunnableSelect($this, $this->options['select-options']);
212
		if($fields !== null) {
213
			$select->fields($fields);
214
		}
215
		return $select;
216
	}
217
218
	/**
219
	 * @param array $fields
220
	 * @return Builder\RunnableInsert
221
	 */
222 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...
223
		$insert = array_key_exists('insert-factory', $this->options)
224
			? call_user_func($this->options['insert-factory'], $this, $this->options['insert-options'])
225
			: new Builder\RunnableInsert($this, $this->options['insert-options']);
226
		if($fields !== null) {
227
			$insert->addAll($fields);
228
		}
229
		return $insert;
230
	}
231
232
	/**
233
	 * @param array $fields
234
	 * @return Builder\RunnableUpdate
235
	 */
236 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...
237
		$update = array_key_exists('update-factory', $this->options)
238
			? call_user_func($this->options['update-factory'], $this, $this->options['update-options'])
239
			: new Builder\RunnableUpdate($this, $this->options['update-options']);
240
		if($fields !== null) {
241
			$update->setAll($fields);
242
		}
243
		return $update;
244
	}
245
246
	/**
247
	 * @return Builder\RunnableDelete
248
	 */
249
	public function delete() {
250
		return array_key_exists('delete-factory', $this->options)
251
			? call_user_func($this->options['delete-factory'], $this, $this->options['delete-options'])
252
			: new Builder\RunnableDelete($this, $this->options['delete-options']);
253
	}
254
255
	/**
256
	 * @return $this
257
	 */
258
	public function transactionStart() {
259
		if((int) $this->transactionLevel === 0) {
260
			if($this->pdo->inTransaction()) {
261
				$this->outerTransaction = true;
262
			} else {
263
				$this->pdo->beginTransaction();
264
			}
265
		}
266
		$this->transactionLevel++;
267
		return $this;
268
	}
269
270
	/**
271
	 * @return $this
272
	 * @throws \Exception
273
	 */
274
	public function transactionCommit() {
275
		return $this->transactionEnd(function () {
276
			$this->pdo->commit();
277
		});
278
	}
279
280
	/**
281
	 * @return $this
282
	 * @throws \Exception
283
	 */
284
	public function transactionRollback() {
285
		return $this->transactionEnd(function () {
286
			$this->pdo->rollBack();
287
		});
288
	}
289
290
	/**
291
	 * @param callable|null $callback
292
	 * @return mixed
293
	 * @throws \Exception
294
	 * @throws \Error
295
	 */
296
	public function dryRun($callback = null) {
297
		$result = null;
298
		if(!$this->pdo->inTransaction()) {
299
			$this->transactionStart();
300
			try {
301
				$result = call_user_func($callback, $this);
302
				$this->transactionRollback();
303
			} catch (\Exception $e) {
304
				$this->transactionRollback();
305
				throw $e;
306
			} 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...
307
				$this->transactionRollback();
308
				throw $e;
309
			}
310
		} else {
311
			$uniqueId = $this->genUniqueId();
312
			$this->exec("SAVEPOINT {$uniqueId}");
313
			try {
314
				$result = call_user_func($callback, $this);
315
				$this->exec("ROLLBACK TO {$uniqueId}");
316
			} catch (\Exception $e) {
317
				$this->exec("ROLLBACK TO {$uniqueId}");
318
				throw $e;
319
			} 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...
320
				$this->exec("ROLLBACK TO {$uniqueId}");
321
				throw $e;
322
			}
323
		}
324
		return $result;
325
	}
326
	
327
	/**
328
	 * @param int|callable $tries
329
	 * @param callable|null $callback
330
	 * @return mixed
331
	 * @throws \Exception
332
	 * @throws \Error
333
	 */
334
	public function transaction($tries = 1, $callback = null) {
335
		if(is_callable($tries)) {
336
			$callback = $tries;
337
			$tries = 1;
338
		} elseif(!is_callable($callback)) {
339
			throw new RuntimeException('$callback must be a callable');
340
		}
341
		$result = null;
342
		$exception = null;
343
		for(; $tries--;) {
344
			if(!$this->pdo->inTransaction()) {
345
				$this->transactionStart();
346
				try {
347
					$result = call_user_func($callback, $this);
348
					$exception = null;
349
					$this->transactionCommit();
350
				} catch (\Exception $e) {
351
					$this->transactionRollback();
352
					$exception = $e;
353
				} 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...
354
					$this->transactionRollback();
355
					$exception = $e;
356
				}
357
			} else {
358
				$uniqueId = $this->genUniqueId();
359
				$this->exec("SAVEPOINT {$uniqueId}");
360
				try {
361
					$result = call_user_func($callback, $this);
362
					$exception = null;
363
					$this->exec("RELEASE SAVEPOINT {$uniqueId}");
364
				} catch (\Exception $e) {
365
					$this->exec("ROLLBACK TO {$uniqueId}");
366
					$exception = $e;
367
				} 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...
368
					$this->exec("ROLLBACK TO {$uniqueId}");
369
					$exception = $e;
370
				}
371
			}
372
		}
373
		if($exception !== null) {
374
			throw $exception;
375
		}
376
		return $result;
377
	}
378
379
	/**
380
	 * @param callable $fn
381
	 * @return $this
382
	 * @throws RuntimeException
383
	 */
384
	private function transactionEnd($fn) {
385
		$this->transactionLevel--;
386
		if($this->transactionLevel < 0) {
387
			throw new RuntimeException("Transaction-Nesting-Problem: Trying to invoke commit on a already closed transaction");
388
		}
389
		if((int) $this->transactionLevel === 0) {
390
			if($this->outerTransaction) {
391
				$this->outerTransaction = false;
392
			} else {
393
				call_user_func($fn);
394
			}
395
		}
396
		return $this;
397
	}
398
399
	/**
400
	 * @param string $query
401
	 * @param callable $fn
402
	 * @return QueryStatement
403
	 * @throws RuntimeException
404
	 */
405
	private function buildQueryStatement($query, $fn) {
406
		$stmt = call_user_func($fn, $query);
407
		if(!$stmt) {
408
			throw new RuntimeException("Could not execute statement:\n{$query}");
409
		}
410
		$stmtWrapper = new QueryStatement($stmt, $query, $this->exceptionInterpreter, $this->queryLoggers);
411
		return $stmtWrapper;
412
	}
413
414
	/**
415
	 * @param callable $fn
416
	 * @return mixed
417
	 */
418
	private function exceptionHandler($fn) {
419
		try {
420
			return call_user_func($fn);
421
		} catch (PDOException $e) {
422
			$this->exceptionInterpreter->throwMoreConcreteException($e);
423
		}
424
		return null;
425
	}
426
427
	/**
428
	 * @return string
429
	 */
430
	private function genUniqueId() {
431
		// Generate a unique id from a former random-uuid-generator
432
		return sprintf('ID%04x%04x%04x%04x%04x%04x%04x%04x',
433
			mt_rand(0, 0xffff),
434
			mt_rand(0, 0xffff),
435
			mt_rand(0, 0xffff),
436
			mt_rand(0, 0x0fff) | 0x4000,
437
			mt_rand(0, 0x3fff) | 0x8000,
438
			mt_rand(0, 0xffff),
439
			mt_rand(0, 0xffff),
440
			mt_rand(0, 0xffff)
441
		);
442
	}
443
}
444