Completed
Push — master ( 50c742...25c477 )
by Ron
03:14
created

MySQL::transaction()   C

Complexity

Conditions 7
Paths 19

Size

Total Lines 32
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

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