Completed
Push — master ( e292fe...29291c )
by Ron
04:26
created

MySQL::dryRun()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 15
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 15
rs 9.4285
cc 3
eloc 12
nc 4
nop 1
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
		$this->transactionStart();
269
		try {
270
			$result = call_user_func($callback, $this);
271
		} catch (\Exception $e) {
272
			$exception = $e;
273
		}
274
		$this->transactionRollback();
275
		if($exception !== null) {
276
			throw $exception;
277
		}
278
		return $result;
279
	}
280
281
	/**
282
	 * @param int|callable $tries
283
	 * @param callable|null $callback
284
	 * @return mixed
285
	 * @throws \Exception
286
	 * @throws null
287
	 */
288
	public function transaction($tries = 1, $callback = null) {
289
		if(is_callable($tries)) {
290
			$callback = $tries;
291
			$tries = 1;
292
		} elseif(!is_callable($callback)) {
293
			throw new \Exception('$callback must be a callable');
294
		}
295
		$e = null;
296
		for(; $tries--;) {
297
			try {
298
				$this->transactionStart();
299
				$result = call_user_func($callback, $this);
300
				$this->transactionCommit();
301
				return $result;
302
			} catch (\Exception $e) {
303
				$this->transactionRollback();
304
			}
305
		}
306
		throw $e;
307
	}
308
309
	/**
310
	 * @param callable $fn
311
	 * @return $this
312
	 * @throws \Exception
313
	 */
314
	private function transactionEnd($fn) {
315
		$this->transactionLevel--;
316
		if($this->transactionLevel < 0) {
317
			throw new \Exception("Transaction-Nesting-Problem: Trying to invoke commit on a already closed transaction");
318
		}
319
		if((int) $this->transactionLevel === 0) {
320
			if($this->outerTransaction) {
321
				$this->outerTransaction = false;
322
			} else {
323
				call_user_func($fn);
324
			}
325
		}
326
		return $this;
327
	}
328
329
	/**
330
	 * @param string $query
331
	 * @param callable $fn
332
	 * @return QueryStatement
333
	 * @throws Exception
334
	 */
335
	private function buildQueryStatement($query, $fn) {
336
		$stmt = call_user_func($fn, $query);
337
		if(!$stmt) {
338
			throw new Exception("Could not execute statement:\n{$query}");
339
		}
340
		$stmtWrapper = new QueryStatement($stmt, $query, $this->exceptionInterpreter, $this->queryLoggers);
341
		return $stmtWrapper;
342
	}
343
344
	/**
345
	 * @param callable $fn
346
	 * @return mixed
347
	 */
348
	private function exceptionHandler($fn) {
349
		try {
350
			return call_user_func($fn);
351
		} catch (PDOException $e) {
352
			$this->exceptionInterpreter->throwMoreConcreteException($e);
353
		}
354
	}
355
}
356