Completed
Push — master ( 798675...5c43e0 )
by Chris
02:40
created

MySql::copyArray()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
c 1
b 1
f 0
dl 0
loc 9
rs 9.6666
cc 2
eloc 5
nc 2
nop 1
1
<?php
2
namespace Darya\Database\Connection;
3
4
use mysqli as php_mysqli;
5
use mysqli_result;
6
use mysqli_stmt;
7
use ReflectionClass;
8
use Darya\Database\AbstractConnection;
9
use Darya\Database\Error;
10
use Darya\Database\Result;
11
use Darya\Database\Query;
12
use Darya\Database\Query\Translator;
13
14
/**
15
 * Darya's MySQL database interface. Uses mysqli.
16
 * 
17
 * @author Chris Andrew <[email protected]>
18
 */
19
class MySql extends AbstractConnection {
20
	
21
	/**
22
	 * Copy a flat array. Aids copying fetched results without the mysqlnd
23
	 * extension installed without retaining references to array elements.
24
	 * 
25
	 * Who knew references could be so awkward to get rid of?
26
	 * 
27
	 * @param array $array
28
	 * @return array
29
	 */
30
	protected static function copyArray($array) {
31
		$copy = array();
32
		
33
		foreach ($array as $key => $value) {
34
			$copy[$key] = $value;
35
		}
36
		
37
		return $copy;
38
	}
39
	
40
	/**
41
	 * Fetch result data from the given MySQLi statement.
42
	 * 
43
	 * Expects the statement to have been executed.
44
	 * 
45
	 * Attempts to use mysqli_stmt::get_result() and mysqli_result::fetch_all(),
46
	 * but falls back to fetching from the statement directly if get_result()
47
	 * isn't found (mysqlnd isn't installed).
48
	 * 
49
	 * @param mysqli_stmt $statement
50
	 * @return array array($data, $fields, $count)
51
	 */
52
	protected function fetchResult(mysqli_stmt $statement) {
53
		if (!method_exists($statement, 'get_result')) {
54
			return $this->fetchResultWithoutNativeDriver($statement);
55
		}
56
		
57
		$result = $statement->get_result();
58
		
59
		if (is_object($result) && $result instanceof mysqli_result) {
60
			return array(
61
				$result->fetch_all(MYSQL_ASSOC),
62
				$result->fetch_fields(),
63
				$result->num_rows
64
			);
65
		}
66
		
67
		return array(array(), array(), null);
68
	}
69
	
70
	/**
71
	 * Method for fetching the same information in a way that doesn't require
72
	 * mysqlnd to be installed.
73
	 * 
74
	 * Fetches directly from the statement with variable binding instead.
75
	 * 
76
	 * @param mysqli_stmt $statement
77
	 * @return array
78
	 */
79
	protected function fetchResultWithoutNativeDriver(mysqli_stmt $statement) {
80
		$statement->store_result();
81
		
82
		$data = array();
83
		$metadata = $statement->result_metadata();
84
		
85
		$row = array();
86
		$count = 0;
87
		$fields = array();
88
		$arguments = array();
89
		
90
		if ($metadata) {
91
			while ($field = $metadata->fetch_field()) {
92
				$fields[] = (array) $field;
93
				$arguments[] = &$row[$field->name];
94
			}
95
			
96
			call_user_func_array(array($statement, 'bind_result'), $arguments);
97
			
98
			while ($statement->fetch()) {
99
				$data[] = static::copyArray($row);
100
				$count++;
101
			}
102
			
103
		}
104
		
105
		return array($data, $fields, $count);
106
	}
107
	
108
	/**
109
	 * Retrieve the type of a variable for binding mysqli parameters.
110
	 * 
111
	 * @param mixed $parameter
112
	 * @return string
113
	 */
114
	protected function prepareType($parameter) {
115
		if (is_int($parameter)) {
116
			return 'i';
117
		}
118
		
119
		if (is_float($parameter)) {
120
			return 'd';
121
		}
122
		
123
		return 's';
124
	}
125
	
126
	/**
127
	 * Prepares an array of values as an array of references to those values.
128
	 * 
129
	 * Required for PHP 5.3+ to prevent warnings when dynamically invoking
130
	 * mysqli_stmt::bind_param().
131
	 * 
132
	 * @param array $parameters
133
	 * @return array
134
	 */
135
	protected function prepareReferences(array $parameters) {
136
		$references = array();
137
		
138
		foreach ($parameters as $key => $value) {
139
			$references[$key] = &$parameters[$key];
140
		}
141
		
142
		return $references;
143
	}
144
	
145
	/**
146
	 * Prepare the given query and parameters as a mysqli statement.
147
	 * 
148
	 * @param string $query
149
	 * @param array  $parameters [optional]
150
	 * @return \mysqli_stmt
151
	 */
152
	protected function prepareStatement($query, $parameters = array()) {
153
		$statement = $this->connection->stmt_init();
154
		
155
		if (!$statement->prepare($query)) {
156
			return $statement;
157
		}
158
		
159
		if (empty($parameters)) {
160
			return $statement;
161
		}
162
		
163
		$types = '';
164
		
165
		foreach ((array) $parameters as $parameter) {
166
			$types .= $this->prepareType($parameter);
167
		}
168
		
169
		array_unshift($parameters, $types);
170
		
171
		call_user_func_array(
172
			array($statement, 'bind_param'),
173
			$this->prepareReferences($parameters)
174
		);
175
		
176
		return $statement;
177
	}
178
	
179
	/**
180
	 * Prepare a result array using the given mysqli statement.
181
	 * 
182
	 * @param mysqli_stmt $statement
183
	 * @return array
184
	 */
185
	protected function prepareStatementResult(mysqli_stmt $statement) {
186
		list($data, $fields, $count) = $this->fetchResult($statement);
187
		
188
		$result = array(
189
			'data'      => $data,
190
			'fields'    => $fields,
191
			'affected'  => $statement->affected_rows,
192
			'num_rows'  => $count,
193
			'insert_id' => $statement->insert_id
194
		);
195
		
196
		$statement->free_result();
197
		
198
		return $result;
199
	}
200
	
201
	/**
202
	 * Initiate the connection.
203
	 * 
204
	 * @return bool
205
	 */
206
	public function connect() {
207
		if ($this->connected()) {
208
			return true;
209
		}
210
		
211
		$this->connection = new php_mysqli(
212
			$this->details['host'],
213
			$this->details['user'],
214
			$this->details['pass'],
215
			$this->details['name'],
216
			$this->details['port']
217
		);
218
		
219
		if ($this->connection->connect_errno) {
220
			return false;
221
		}
222
		
223
		return $this->connected = true;
224
	}
225
	
226
	/**
227
	 * Determine whether the connection is currently active.
228
	 * 
229
	 * @return bool
230
	 */
231
	public function connected() {
232
		return $this->connected && !$this->connection->connect_errno;
233
	}
234
	
235
	/**
236
	 * Close the connection.
237
	 */
238
	public function disconnect() {
239
		$this->connection->close();
240
		$this->connected = false;
241
	}
242
	
243
	/**
244
	 * Retrieve the query translator.
245
	 * 
246
	 * @return Translator
247
	 */
248
	public function translator() {
249
		if (!$this->translator) {
250
			$this->translator = new Translator\MySql;
251
		}
252
		
253
		return $this->translator;
254
	}
255
	
256
	/**
257
	 * Query the database with the given query and optional parameters.
258
	 * 
259
	 * TODO: Simplify this.
260
	 * 
261
	 * @param Query|string $query
262
	 * @param array        $parameters [optional]
263
	 * @return Result
264
	 */
265
	public function query($query, array $parameters = array()) {
266
		if (!$query instanceof Query) {
267
			$query = new Query((string) $query, $parameters);
268
		}
269
		
270
		$this->connect();
271
		
272 View Code Duplication
		if (!$this->connected()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
273
			$this->lastResult = new Result($query, array(), array(), $this->error());
274
			
275
			$this->event('mysql.query', array($this->lastResult));
276
			
277
			return $this->lastResult;
278
		}
279
		
280
		$this->lastResult = null;
281
		
282
		$this->event('mysql.prequery', array($query));
283
		
284
		$statement = $this->prepareStatement($query->string, $query->parameters);
285
		
286
		if ($statement->errno) {
287
			$error = new Error($statement->errno, $statement->error);
288
			$this->lastResult = new Result($query, array(), array(), $error);
289
			
290
			$this->event('mysql.query', array($this->lastResult));
291
			
292
			return $this->lastResult;
293
		}
294
		
295
		$statement->execute();
296
		
297
		$error = $this->error();
298
		
299
		if ($error) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
300
			$this->lastResult = new Result($query, array(), array(), $error);
301
			
302
			$this->event('mysql.query', array($this->lastResult));
303
			
304
			return $this->lastResult;
305
		}
306
		
307
		$result = $this->prepareStatementResult($statement);
308
		
309
		$statement->close();
310
		
311
		$info = array(
312
			'count'     => $result['num_rows'],
313
			'fields'    => $result['fields'],
314
			'affected'  => $result['affected'],
315
			'insert_id' => $result['insert_id']
316
		);
317
		
318
		$this->lastResult = new Result($query, $result['data'], $info, $error);
319
		
320
		$this->event('mysql.query', array($this->lastResult));
321
		
322
		return $this->lastResult;
323
	}
324
	
325
	/**
326
	 * Escape the given string for a MySQL query.
327
	 * 
328
	 * @param string $string
329
	 * @return string
330
	 */
331
	public function escape($string) {
332
		$this->connect();
333
		
334
		return $this->connection->real_escape_string($string);
335
	}
336
	
337
	/**
338
	 * Retrieve error information regarding the last query or connection
339
	 * attempt.
340
	 * 
341
	 * Returns null if there is no error.
342
	 * 
343
	 * @return Error
344
	 */
345
	public function error() {
346
		$connectionError = $this->connectionError();
347
		
348
		if ($connectionError) {
349
			return $connectionError;
350
		}
351
		
352
		if ($this->lastResult && $this->lastResult->error) {
353
			return $this->lastResult->error;
354
		}
355
		
356
		return null;
357
	}
358
	
359
	/**
360
	 * Retrieve error information from the mysqli connection object.
361
	 * 
362
	 * Checks for general errors first, then connection errors.
363
	 * 
364
	 * Returns null if there is no error.
365
	 * 
366
	 * @return Error
367
	 */
368
	protected function connectionError() {
369
		if (!$this->connection) {
370
			return null;
371
		}
372
		
373
		if ($this->connection->errno) {
374
			return new Error($this->connection->errno, $this->connection->error);
375
		}
376
		
377
		if ($this->connection->connect_errno) {
378
			return new Error($this->connection->connect_errno, $this->connection->connect_error);
379
		}
380
		
381
		return null;
382
	}
383
	
384
}
385