Completed
Push — master ( 319aa4...523298 )
by smiley
04:24
created

MySQLiDriver::insertPreparedRow()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
eloc 3
nc 1
nop 3
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;
14
15
use chillerlan\Database\DBException;
16
use chillerlan\Database\DBResult;
17
use mysqli, mysqli_result, mysqli_sql_exception, mysqli_stmt;
18
use ReflectionMethod, stdClass, Exception;
19
20
/**
21
 *
22
 */
23
class MySQLiDriver extends DBDriverAbstract{
24
25
	/**
26
	 * Holds the database resource object
27
	 *
28
	 * @var mysqli
29
	 */
30
	protected $db;
31
32
	/**
33
	 * Establishes a database connection and returns the connection object
34
	 *
35
	 * @return \chillerlan\Database\Drivers\DBDriverInterface
36
	 * @throws DBException
37
	 */
38
	public function connect():DBDriverInterface{
39
40
		if($this->db instanceof mysqli){
41
			return $this;
42
		}
43
44
		$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...
45
46
		if(!$this->db->options(MYSQLI_OPT_CONNECT_TIMEOUT, $this->options->mysqli_timeout)){
47
			throw new DBException('Could not set database timeout.');
48
		}
49
50
		if($this->options->use_ssl){
51
			$this->db->ssl_set(
52
				$this->options->ssl_key,
53
				$this->options->ssl_cert,
54
				$this->options->ssl_ca,
55
				$this->options->ssl_capath,
56
				$this->options->ssl_cipher
57
			);
58
		}
59
60
		if(!$this->db->real_connect(
61
			$this->options->host,
62
			$this->options->username,
63
			$this->options->password,
64
			$this->options->database,
65
			(int)$this->options->port,
66
			$this->options->socket
67
		)){
68
			throw new DBException('Could not connect to the database.');
69
		}
70
71
		/**
72
		 * @see https://mathiasbynens.be/notes/mysql-utf8mb4 How to support full Unicode in MySQL
73
		 */
74
		if(!$this->db->set_charset($this->options->mysql_charset)){
75
			throw new DBException('Could not set database character set.');
76
		}
77
78
		return $this;
79
	}
80
81
	/**
82
	 * Closes a database connection
83
	 *
84
	 * @return bool
85
	 */
86
	public function disconnect():bool{
87
		return $this->db->close();
88
	}
89
90
	/**
91
	 * Returns info about the used php client
92
	 *
93
	 * @return string php's database client string
94
	 */
95
	public function getClientInfo():string{
96
		return $this->db->client_info;
97
	}
98
99
	/**
100
	 * Returns info about the database server
101
	 *
102
	 * @return string database's serverinfo string
103
	 */
104
	public function getServerInfo():string{
105
		return $this->db->server_info;
106
	}
107
108
	/**
109
	 * @param $data
110
	 *
111
	 * @return string
112
	 */
113
	protected function __escape($data){
114
		return $this->db->real_escape_string($data);
115
	}
116
117
	/**
118
	 * @param string      $sql
119
	 * @param string|null $index
120
	 * @param bool        $assoc
121
	 *
122
	 * @return bool|\chillerlan\Database\DBResult|\mysqli_result
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use boolean|DBResult.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
123
	 */
124
	protected function __raw(string $sql, string $index = null, bool $assoc = true){
125
		$result = $this->db->query($sql);
126
127
		if(is_bool($result)){
128
			return $result;
129
		}
130
131
		$r = $this->getResult([$result, 'fetch_'.($assoc ? 'assoc' : 'row')], [], $index, $assoc);
132
133
		$result->free();
134
135
		return $r;
136
	}
137
138
	/**
139
	 * @param string      $sql
140
	 * @param array       $values
141
	 * @param string|null $index
142
	 * @param bool        $assoc
143
	 *
144
	 * @return bool|\chillerlan\Database\DBResult
145
	 */
146
	protected function __prepared(string $sql, array $values = [], string $index = null, bool $assoc = true){
147
		$stmt = $this->db->stmt_init();
148
149
		$stmt->prepare($sql);
150
151
		if(count($values) > 0){
152
			(new ReflectionMethod($stmt, 'bind_param'))->invokeArgs($stmt, $this->getReferences($values));
153
		}
154
155
		$stmt->execute();
156
157
		$result = $stmt->result_metadata();
158
159
		if(is_bool($result)){
160
			return true; // @todo: returning $result causes trouble on prepared INSERT first line. why???
161
		}
162
163
		// get the columns and their references
164
		// http://php.net/manual/mysqli-stmt.bind-result.php
165
		$cols = [];
166
		$refs = [];
167
168
		foreach($result->fetch_fields() as $k => &$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...
169
			$refs[] = &$cols[$assoc ? $field->name : $k];
170
		}
171
172
		(new ReflectionMethod($stmt, 'bind_result'))->invokeArgs($stmt, $refs);
173
174
		// fetch the data
175
		$output = new DBResult;
176
		$i      = 0;
177
178
		while($stmt->fetch()){
179
			$row = [];
180
			$key = $i;
181
182
			foreach($cols as $field => &$data){
183
				$row[$field] = $data;
184
			}
185
186
			if($assoc && !empty($index)){
187
				$key = $row[$index] ?? $i;
188
			}
189
190
			$output[$key] = $row;
191
			$i++;
192
		}
193
194
		// KTHXBYE!
195
		$stmt->free_result();
196
		$stmt->close();
197
198
		return $i === 0 ? true : $output;
199
200
		/*
0 ignored issues
show
Unused Code Comprehensibility introduced by
59% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
201
				$this->addStats([
202
					'affected_rows' => $stmt->affected_rows,
203
					'error'         => $stmt->error_list,
204
					'insert_id'     => $stmt->insert_id,
205
					'sql'           => $sql,
206
					'values'        => $values,
207
					'types'         => $types,
208
					'index'         => $index,
209
					'assoc'         => $assoc,
210
				]);
211
		*/
212
	}
213
214
	/**
215
	 * @param string $sql
216
	 * @param array  $values
217
	 *
218
	 * @return bool
219
	 */
220
	protected function __multi(string $sql, array $values){
221
		$stmt = $this->db->stmt_init();
222
		$stmt->prepare($sql);
223
224
		$reflectionMethod = new ReflectionMethod($stmt, 'bind_param');
225
226
		foreach($values as $row){
227
			$this->insertPreparedRow($stmt, $reflectionMethod, $row);
228
		}
229
230
		$stmt->close();
231
232
		return true;
233
	}
234
235
	/**
236
	 * @param string $sql
237
	 * @param array  $data
238
	 * @param        $callback
239
	 *
240
	 * @return bool
241
	 */
242
	protected function __multi_callback(string $sql, array $data, $callback){
243
		$stmt = $this->db->stmt_init();
244
		$stmt->prepare($sql);
245
246
		$reflectionMethod = new ReflectionMethod($stmt, 'bind_param');
247
248
		foreach($data as $row){
249
			if($row = call_user_func_array($callback, [$row])){
250
				$this->insertPreparedRow($stmt, $reflectionMethod, $row);
251
			}
252
		}
253
254
		$stmt->close();
255
256
		return true;
257
	}
258
259
	/**
260
	 * @param \mysqli_stmt      $stmt
261
	 * @param \ReflectionMethod $reflectionMethod
262
	 * @param array             $row
263
	 *
264
	 * @return void
265
	 */
266
	protected function insertPreparedRow(mysqli_stmt &$stmt, ReflectionMethod &$reflectionMethod, array &$row){
267
		$reflectionMethod->invokeArgs($stmt, $this->getReferences($row));
268
		$stmt->execute();
269
	}
270
271
	/**
272
	 * Returns a string of types for the given values
273
	 *
274
	 * @link http://php.net/manual/mysqli-stmt.bind-param.php
275
	 *
276
	 * @param array $values
277
	 *
278
	 * @return string
279
	 * @internal
280
	 */
281
	protected function getTypes(array &$values){
282
283
		$types = [];
284
		foreach($values as &$v){
285
			switch(gettype($v)){
286
				case 'integer':
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

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

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

Loading history...
287
					$types[] = 'i';
288
					break;
289
				case 'double':
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

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

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

Loading history...
290
					$types[] = 'd';
291
					break;
292
				default:
293
					$types[] = 's';
294
					break;
295
			}
296
		}
297
298
		return implode($types);
299
	}
300
301
	/**
302
	 * Copies an array to an array of referenced values
303
	 *
304
	 * @param array $row
305
	 *
306
	 * @return array
307
	 * @see http://php.net/manual/mysqli-stmt.bind-param.php
308
	 */
309
	protected function getReferences(array &$row){
310
		$references = [];
311
312
		foreach($row as &$field){
313
			$references[] = &$field;
314
		}
315
316
		array_unshift($references, $this->getTypes($row));
317
318
		return $references;
319
	}
320
321
}
322