Completed
Push — master ( 13c57a...4aa817 )
by smiley
02:16
created

MySQLiDriver::getTypes()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 13
rs 9.2
cc 4
eloc 8
nc 4
nop 1
1
<?php
2
/**
3
 * Class MySQLiDriver
4
 *
5
 * @filesource   MySQLiDriver.php
6
 * @created      04.11.2015
7
 * @package      chillerlan\Database\Drivers\MySQLi
8
 * @author       Smiley <[email protected]>
9
 * @copyright    2015 Smiley
10
 * @license      MIT
11
 */
12
13
namespace chillerlan\Database\Drivers\MySQLi;
14
15
use chillerlan\Database\DBException;
16
use chillerlan\Database\Drivers\DBBaseDriver;
17
use chillerlan\Database\Drivers\DBDriverInterface;
18
use mysqli;
19
use mysqli_result;
20
use mysqli_sql_exception;
21
use mysqli_stmt;
22
use ReflectionClass;
23
use ReflectionMethod;
24
use stdClass;
25
26
/**
27
 *
28
 */
29
class MySQLiDriver extends DBBaseDriver implements DBDriverInterface{
30
31
	/**
32
	 * Holds the database resource object
33
	 *
34
	 * @var mysqli
35
	 */
36
	protected $db;
37
38
	/**
39
	 * Establishes a database connection and returns the connection object
40
	 *
41
	 * @return mysqli the database resource object
42
	 * @throws DBException
43
	 */
44
	public function connect(){
45
46
		if($this->db instanceof mysqli){
47
			return $this->db;
48
		}
49
50
		$this->db = mysqli_init();
0 ignored issues
show
Documentation Bug introduced by
It seems like mysqli_init() of type object<mysql> is incompatible with the declared type object<mysqli> of property $db.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
51
52
		if(!$this->db->options(MYSQLI_OPT_CONNECT_TIMEOUT, $this->options->mysqli_timeout)){
53
			throw new DBException('Could not set database timeout.');
54
		}
55
56
		if($this->options->use_ssl){
57
			$this->db->ssl_set(
58
				$this->options->ssl_key,
59
				$this->options->ssl_cert,
60
				$this->options->ssl_ca,
61
				$this->options->ssl_capath,
62
				$this->options->ssl_cipher
63
			);
64
		}
65
66
		if(!$this->db->real_connect(
67
			$this->options->host,
68
			$this->options->username,
69
			$this->options->password,
70
			$this->options->database,
71
			(int)$this->options->port,
72
			$this->options->socket
73
		)){
74
			throw new DBException('Could not connect to the database.');
75
		}
76
77
		/**
78
		 * @see https://mathiasbynens.be/notes/mysql-utf8mb4 How to support full Unicode in MySQL
79
		 */
80
		if(!$this->db->set_charset($this->options->mysql_charset)){
81
			throw new DBException('Could not set database character set.');
82
		}
83
84
		return $this->db;
85
	}
86
87
	/**
88
	 * Closes a database connection
89
	 *
90
	 * @return $this
0 ignored issues
show
Documentation introduced by
Should the return type not be MySQLiDriver|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
91
	 */
92
	public function disconnect(){
93
		$this->db->close();
94
	}
95
96
	/**
97
	 * Returns info about the used php client
98
	 *
99
	 * @return string php's database client string
100
	 */
101
	public function getClientInfo(){
102
		return $this->db->client_info;
103
	}
104
105
	/**
106
	 * Returns info about the database server
107
	 *
108
	 * @return string database's serverinfo string
109
	 */
110
	public function getServerInfo(){
111
		return $this->db->server_info;
112
	}
113
114
	/**
115
	 * Sanitizer.
116
	 *
117
	 * Recursively escapes string values, optional htmlspecialchars()
118
	 *
119
	 * @param array|string $data         array or string to escape
120
	 * @param bool         $specialchars [optional] if true, it performs a htmlspecialchars() on each value given
121
	 *
122
	 * @return array|string array or string. escaped. obviously.
123
	 */
124 View Code Duplication
	public function escape($data, $specialchars = false){
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...
125
126
		if(is_array($data)){
127
128
			foreach($data as $key => $value){
129
				$data[$key] = $this->escape($value, $specialchars);
130
			}
131
132
		}
133
		else if(is_object($data)){
134
135
			foreach($data as $key => $value){
136
				$data->{$key} = $this->escape($value, $specialchars);
137
			}
138
139
		}
140
		else{
141
142
			if($specialchars){
143
				$data = htmlspecialchars($data, ENT_HTML5, 'UTF-8', false);
144
			}
145
146
			$data = $this->db->real_escape_string($data);
147
		}
148
149
		return $data;
150
	}
151
152
	/**
153
	 * Basic SQL query for non prepared statements
154
	 *
155
	 * There is no escaping in here, so make sure, your SQL is clean/escaped.
156
	 * Also, your SQL should NEVER contain user input, use prepared statements in this case.
157
	 *
158
	 * If the query was successful it returns either an array of results or true
159
	 * if it was a void query. On errors, a false will be returned, obviously.
160
	 *
161
	 * @param string $sql         The SQL statement
162
	 * @param string $index       [optional] an index column to assingn as the result's keys
163
	 * @param bool   $assoc       [optional] If true, the fields are named with the respective column names, otherwise numbered
164
	 * @param bool   $fetch_array [optional] fetch the vaues as array instead of object
165
	 *
166
	 * @return array|bool array with results, true on void query success, otherwise false.
167
	 * @throws \chillerlan\Database\DBException
168
	 */
169
	public function raw($sql, $index = '', $assoc = true, $fetch_array = false){
170
171
		try{
172
			$result = $this->db->query($sql);
173
174
			$this->addStats([
175
				'affected_rows' => $this->db->affected_rows,
176
				'error'         => $this->db->error_list,
177
				'insert_id'     => $this->db->insert_id,
178
				'sql'           => $sql,
179
				'index'         => $index,
180
				'assoc'         => $assoc,
181
				'fetch_array'   => $fetch_array,
182
			]);
183
184
			if(is_bool($result)){
185
				return $result;
186
			}
187
188
			$out = [];
189
			$method = 'fetch_'.($assoc ? ($fetch_array ? 'assoc' : 'object') : 'row');
190
			$i = 0 ;
191
192
			// ok, we have a result with one or more rows, loop out the rows and output as array
193 View Code Duplication
			while($row = $result->{$method}()){
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...
194
				$key = $i;
195
196
				if($assoc && !empty($index)){
197
198
					if($fetch_array && isset($row[$index])){
199
						$key = $row[$index];
200
					}
201
					elseif(isset($row->{$index})){
202
						$key = $row->{$index};
203
					}
204
205
				}
206
207
				$out[$key] = $row;
208
				$i++;
209
			}
210
211
			$result->free();
212
213
			return $out;
214
		}
215
		catch(mysqli_sql_exception $mysqli_sql_exception){
216
			throw new DBException($mysqli_sql_exception->getMessage());
217
		}
218
	}
219
220
	/**
221
	 * Prepared statements wrapper
222
	 *
223
	 * Does everything for you: prepares the statement and fetches the results as an object or array
224
	 * just pass a query along with values and you're done. Not meant for multi-inserts.
225
	 *
226
	 * @param string $sql         The SQL statement to prepare
227
	 * @param array  $values      [optional] the value for each "?" in the statement - in the respective order, of course
228
	 * @param string $index       [optional] an index column to assingn as the result's keys
229
	 * @param bool   $assoc       [optional] If true, the fields are named with the respective column names, otherwise numbered
230
	 * @param bool   $fetch_array [optional] fetch the vaues as array instead of object
231
	 *
232
	 * @return array|bool Array with results, true on void query success, otherwise false
233
	 * @throws \chillerlan\Database\DBException
234
	 */
235
	public function prepared($sql, array $values = [], $index = '', $assoc = true, $fetch_array = false){
236
		$stmt = $this->db->stmt_init();
237
238
		if($stmt->prepare($sql)){
239
			throw new DBException('could not prepare statement ('.$sql.')');
240
		}
241
242
		$types = $this->getTypes($values);
243
244
		if(count($values) > 0){
245
			$references = $this->getReferences($values);
246
			array_unshift($references, $types);
247
248
			(new ReflectionMethod($stmt, 'bind_param'))->invokeArgs($stmt, $references);
249
		}
250
251
		$stmt->execute();
252
253
		$this->addStats([
254
			'affected_rows' => $stmt->affected_rows,
255
			'error'         => $stmt->error_list,
256
			'insert_id'     => $stmt->insert_id,
257
			'sql'           => $sql,
258
			'values'        => $values,
259
			'types'         => $types,
260
			'index'         => $index,
261
			'assoc'         => $assoc,
262
			'fetch_array'   => $fetch_array,
263
		]);
264
265
		$result = $stmt->result_metadata();
266
267
		// void result
268
		if(!$result){
269
			return true;
270
		}
271
272
		return $this->getResult($stmt, $result, $assoc, $fetch_array);
273
	}
274
275
	/**
276
	 * Prepared multi line insert
277
	 *
278
	 * Prepared statement multi insert/update
279
	 *
280
	 * @param string $sql    The SQL statement to prepare
281
	 * @param array  $values a multidimensional array with the values, each row represents one line to insert.
282
	 *
283
	 * @return bool true query success, otherwise false
284
	 * @throws \chillerlan\Database\DBException
285
	 */
286
	public function multi($sql, array $values){
287
288
		if(!is_array($values) || count($values) < 1 || !is_array($values[0]) || count($values[0]) < 1){
289
			throw new DBException('invalid data');
290
		}
291
292
		$stmt = $this->db->stmt_init();
293
294
		if(!$stmt->prepare($sql)){
295
			throw new DBException('could not prepare statement ('.$sql.')');
296
		}
297
298
		$reflectionMethod = new ReflectionMethod($stmt, 'bind_param');
299
300
		foreach($values as $row){
301
			$this->insertPreparedRow($stmt, $reflectionMethod, $row);
302
		}
303
304
		$stmt->close();
305
		return true;
306
	}
307
308
	/**
309
	 * Prepared multi line insert/update with callback
310
	 * @see https://gist.github.com/krakjoe/6437782
311
	 * @see https://gist.github.com/krakjoe/9384409
312
	 *
313
	 * @param string         $sql      The SQL statement to prepare
314
	 * @param array          $data     an array with the (raw) data to insert, each row represents one line to insert.
315
	 * @param callable|array $callback a callback that processes the values for each row.
316
	 *
317
	 * @return bool true query success, otherwise false
318
	 * @throws \chillerlan\Database\DBException
319
	 */
320
	public function multi_callback($sql, array $data, $callback){
321
322
		if(count($data) < 1){
323
			throw new DBException('invalid data');
324
		}
325
326
		if(!is_callable($callback) || (is_array($callback) && (!isset($callback[0]) || !is_object($callback[0])))){
327
			throw new DBException('invalid callback');
328
		}
329
330
		$stmt = $this->db->stmt_init();
331
332
		if(!$stmt->prepare($sql)){
333
			throw new DBException('could not prepare statement ('.$sql.')');
334
		}
335
336
		$reflectionMethod = new ReflectionMethod($stmt, 'bind_param');
337
338
		foreach($data as $row){
339
			if($row = call_user_func_array($callback, [$row])){
340
				$this->insertPreparedRow($stmt, $reflectionMethod, $row);
341
			}
342
		}
343
344
		$stmt->close();
345
346
		return true;
347
	}
348
349
	/**
350
	 * @param \mysqli_stmt      $stmt
351
	 * @param \ReflectionMethod $reflectionMethod
352
	 * @param array             $row
353
	 */
354
	protected function insertPreparedRow(mysqli_stmt &$stmt, ReflectionMethod &$reflectionMethod, array &$row){
355
		$references = $this->getReferences($row);
356
		array_unshift($references, $this->getTypes($references));
357
		$reflectionMethod->invokeArgs($stmt, $references);
358
		$stmt->execute();
359
	}
360
361
	/**
362
	 * @param \mysqli_stmt   $stmt
363
	 * @param \mysqli_result $result
364
	 * @param bool           $assoc
365
	 * @param bool           $fetch_array
366
	 *
367
	 * @return array|bool
368
	 */
369
	protected function getResult(mysqli_stmt &$stmt, mysqli_result &$result, $assoc, $fetch_array){
370
		// get the columns and their references
371
		// http://php.net/manual/mysqli-stmt.bind-result.php
372
		$cols = $refs = [];
373
374
		foreach($result->fetch_fields() as $i => &$field){
0 ignored issues
show
Bug introduced by
The expression $result->fetch_fields() cannot be used as a reference.

Let?s assume that you have the following foreach statement:

foreach ($array as &$itemValue) { }

$itemValue is assigned by reference. This is possible because the expression (in the example $array) can be used as a reference target.

However, if we were to replace $array with something different like the result of a function call as in

foreach (getArray() as &$itemValue) { }

then assigning by reference is not possible anymore as there is no target that could be modified.

Available Fixes

1. Do not assign by reference
foreach (getArray() as $itemValue) { }
2. Assign to a local variable first
$array = getArray();
foreach ($array as &$itemValue) {}
3. Return a reference
function &getArray() { $array = array(); return $array; }

foreach (getArray() as &$itemValue) { }
Loading history...
375
			$refs[] = &$cols[$assoc ? $field->name : $i];
376
		}
377
378
		(new ReflectionMethod($stmt, 'bind_result'))->invokeArgs($stmt, $refs);
379
380
		// fetch the data
381
		$output = [];
382
		$count = 0;
383
384
		while($stmt->fetch()){
385
			$row = $fetch_array || !$assoc ? [] : new stdClass;
386
387
			foreach($cols as $field => &$field){
388
				if($fetch_array || !$assoc){
389
					$row[$field] = $field;
390
				}
391
				else{
392
					$row->{$field} = $field;
393
				}
394
			}
395
396
			$key = !empty($index) && isset($cols[$index]) ? $cols[$index] : $count;
397
			$output[$key] = $row;
398
399
			$count++;
400
		}
401
402
		// KTHXBYE!
403
		$stmt->free_result();
404
		$stmt->close();
405
406
		return $count === 0 ? true : $output;
407
	}
408
409
	/**
410
	 * Returns a string of types for the given values
411
	 *
412
	 * @link http://php.net/manual/mysqli-stmt.bind-param.php
413
	 *
414
	 * @param array $values
415
	 *
416
	 * @return string
417
	 * @internal
418
	 */
419
	protected function getTypes(array &$values){
420
421
		$types = [];
422
		foreach($values as &$v){
423
			switch(gettype($v)){
424
				case 'integer': $types[] = 'i'; break;
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
Coding Style introduced by
Terminating statement must be on a line by itself

As per the PSR-2 coding standard, the break (or other terminating) statement must be on a line of its own.

switch ($expr) {
     case "A":
         doSomething();
         break; //wrong
     case "B":
         doSomething();
         break; //right
     case "C:":
         doSomething();
         return true; //right
 }

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
425
				case 'double': $types[] ='d'; break;
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
Coding Style introduced by
Terminating statement must be on a line by itself

As per the PSR-2 coding standard, the break (or other terminating) statement must be on a line of its own.

switch ($expr) {
     case "A":
         doSomething();
         break; //wrong
     case "B":
         doSomething();
         break; //right
     case "C:":
         doSomething();
         return true; //right
 }

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
426
				default: $types[] = 's'; break;
0 ignored issues
show
Coding Style introduced by
The default body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a default statement must start on the line immediately following the statement.

switch ($expr) {
    default:
        doSomething(); //right
        break;
}


switch ($expr) {
    default:

        doSomething(); //wrong
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
Coding Style introduced by
Terminating statement must be on a line by itself

As per the PSR-2 coding standard, the break (or other terminating) statement must be on a line of its own.

switch ($expr) {
     case "A":
         doSomething();
         break; //wrong
     case "B":
         doSomething();
         break; //right
     case "C:":
         doSomething();
         return true; //right
 }

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
427
			}
428
		}
429
430
		return implode($types);
431
	}
432
433
	/**
434
	 * Copies an array to an array of referenced values
435
	 *
436
	 * @param array $row
437
	 *
438
	 * @return array
439
	 * @see http://php.net/manual/mysqli-stmt.bind-param.php
440
	 */
441
	protected function getReferences(array &$row){
442
		$references = [];
443
444
		foreach($row as &$field){
445
			$references[] = &$field;
446
		}
447
448
		return $references;
449
	}
450
451
}
452