Completed
Push — master ( 9cfab5...3e7612 )
by Ron
02:59
created

MySQL::exceptionHandler()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 7
rs 9.4285
cc 2
eloc 5
nc 2
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
		$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\Select) {
155
			$result = sprintf('(%s)', (string) $value);
156
		} elseif(is_array($value)) {
157
			$result = join(', ', array_map(function ($value) { return $this->quote($value); }, $value));
158
		} else {
159
			$result = $this->pdo->quote($value);
160
		}
161
		return $result;
162
	}
163
164
	/**
165
	 * @param string $field
166
	 * @return string
167
	 */
168
	public function quoteField($field) {
169
		if (is_numeric($field) || !is_string($field)) {
170
			throw new UnexpectedValueException('Field name is invalid');
171
		}
172
		if(strpos($field, '`') !== false) {
173
			return (string) $field;
174
		}
175
		$parts = explode('.', $field);
176
		return '`'.join('`.`', $parts).'`';
177
	}
178
179
	/**
180
	 * @param array $fields
181
	 * @return RunnableSelect
182
	 */
183
	public function select(array $fields = null) {
184
		$select = new RunnableSelect($this);
185
		if($fields !== null) {
186
			$select->fields($fields);
187
		}
188
		return $select;
189
	}
190
191
	/**
192
	 * @param array $fields
193
	 * @return Builder\RunnableInsert
194
	 */
195
	public function insert(array $fields = null) {
196
		$insert = new Builder\RunnableInsert($this);
197
		if($fields !== null) {
198
			$insert->addAll($fields);
199
		}
200
		return $insert;
201
	}
202
203
	/**
204
	 * @param array $fields
205
	 * @return Builder\RunnableUpdate
206
	 */
207
	public function update(array $fields = null) {
208
		$update = new Builder\RunnableUpdate($this);
209
		if($fields !== null) {
210
			$update->setAll($fields);
211
		}
212
		return $update;
213
	}
214
215
	/**
216
	 * @return Builder\RunnableDelete
217
	 */
218
	public function delete() {
219
		return new Builder\RunnableDelete($this);
220
	}
221
222
	/**
223
	 * @return $this
224
	 */
225
	public function transactionStart() {
226
		if((int) $this->transactionLevel === 0) {
227
			if($this->pdo->inTransaction()) {
228
				$this->outerTransaction = true;
229
			} else {
230
				$this->pdo->beginTransaction();
231
			}
232
		}
233
		$this->transactionLevel++;
234
		return $this;
235
	}
236
237
	/**
238
	 * @return $this
239
	 * @throws \Exception
240
	 */
241
	public function transactionCommit() {
242
		return $this->transactionEnd(function () {
243
			$this->pdo->commit();
244
		});
245
	}
246
247
	/**
248
	 * @return $this
249
	 * @throws \Exception
250
	 */
251
	public function transactionRollback() {
252
		return $this->transactionEnd(function () {
253
			$this->pdo->rollBack();
254
		});
255
	}
256
257
	/**
258
	 * @param int|callable $tries
259
	 * @param callable|null $callback
260
	 * @return mixed
261
	 * @throws \Exception
262
	 * @throws null
263
	 */
264
	public function transaction($tries = 1, $callback = null) {
265
		if(is_callable($tries)) {
266
			$callback = $tries;
267
			$tries = 1;
268
		} elseif(!is_callable($callback)) {
269
			throw new \Exception('$callback must be a callable');
270
		}
271
		$e = null;
272
		for(; $tries--;) {
273
			try {
274
				$this->transactionStart();
275
				$result = call_user_func($callback, $this);
276
				$this->transactionCommit();
277
				return $result;
278
			} catch (\Exception $e) {
279
				$this->transactionRollback();
280
			}
281
		}
282
		throw $e;
283
	}
284
285
	/**
286
	 * @param callable $fn
287
	 * @return $this
288
	 * @throws \Exception
289
	 */
290
	private function transactionEnd($fn) {
291
		$this->transactionLevel--;
292
		if($this->transactionLevel < 0) {
293
			throw new \Exception("Transaction-Nesting-Problem: Trying to invoke commit on a already closed transaction");
294
		}
295
		if((int) $this->transactionLevel === 0) {
296
			if($this->outerTransaction) {
297
				$this->outerTransaction = false;
298
			} else {
299
				call_user_func($fn);
300
			}
301
		}
302
		return $this;
303
	}
304
305
	/**
306
	 * @param string $query
307
	 * @param callable $fn
308
	 * @return QueryStatement
309
	 * @throws Exception
310
	 */
311
	private function buildQueryStatement($query, $fn) {
312
		$stmt = call_user_func($fn, $query);
313
		if(!$stmt) {
314
			throw new Exception("Could not execute statement:\n{$query}");
315
		}
316
		$stmtWrapper = new QueryStatement($stmt, $query, $this->exceptionInterpreter, $this->queryLoggers);
317
		return $stmtWrapper;
318
	}
319
320
	/**
321
	 * @param callable $fn
322
	 * @return mixed
323
	 */
324
	private function exceptionHandler($fn) {
325
		try {
326
			return call_user_func($fn);
327
		} catch (PDOException $e) {
328
			$this->exceptionInterpreter->throwMoreConcreteException($e);
329
		}
330
	}
331
}
332