Completed
Push — master ( 612476...c205d6 )
by smiley
04:22
created

MySQLiDriver::getReferences()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 19
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 19
rs 9.2
c 0
b 0
f 0
cc 4
eloc 11
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;
14
15
use chillerlan\Database\DBException;
16
use chillerlan\Database\DBResult;
17
use mysqli;
18
19
/**
20
 *
21
 */
22
class MySQLiDriver extends DBDriverAbstract{
23
24
	/**
25
	 * Holds the database resource object
26
	 *
27
	 * @var mysqli
28
	 */
29
	protected $db;
30
31
	/**
32
	 * Establishes a database connection and returns the connection object
33
	 *
34
	 * @return \chillerlan\Database\Drivers\DBDriverInterface
35
	 * @throws DBException
36
	 */
37
	public function connect():DBDriverInterface{
38
39
		if($this->db instanceof mysqli){
40
			return $this;
41
		}
42
43
		try{
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
			$this->db->options(MYSQLI_OPT_CONNECT_TIMEOUT, $this->options->mysqli_timeout);
47
48
			// @codeCoverageIgnoreStart
49
			if($this->options->use_ssl){
50
				$this->db->ssl_set(
51
					$this->options->ssl_key,
52
					$this->options->ssl_cert,
53
					$this->options->ssl_ca,
54
					$this->options->ssl_capath,
55
					$this->options->ssl_cipher
56
				);
57
			}
58
			// @codeCoverageIgnoreEnd
59
60
			$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
69
			/**
70
			 * @see https://mathiasbynens.be/notes/mysql-utf8mb4 How to support full Unicode in MySQL
71
			 */
72
			$this->db->set_charset($this->options->mysql_charset);
73
74
			return $this;
75
		}
76
		catch(\Exception $e){
77
			throw new DBException('db error: [MySQLiDriver]: '.$e->getMessage());
78
		}
79
80
	}
81
82
	/**
83
	 * Closes a database connection
84
	 *
85
	 * @return bool
86
	 */
87
	public function disconnect():bool{
88
		return $this->db->close();
89
	}
90
91
	/**
92
	 * Returns info about the used php client
93
	 *
94
	 * @return string php's database client string
95
	 */
96
	public function getClientInfo():string{
97
		return $this->db->client_info;
98
	}
99
100
	/**
101
	 * Returns info about the database server
102
	 *
103
	 * @return string database's serverinfo string
104
	 */
105
	public function getServerInfo():string{
106
		return $this->db->server_info;
107
	}
108
109
	/**
110
	 * @param $data
111
	 *
112
	 * @return string
113
	 */
114
	public function escape($data){
115
		return $this->db->real_escape_string($data);
116
	}
117
118
	/**
119
	 * @param string      $sql
120
	 * @param string|null $index
121
	 * @param bool        $assoc
122
	 *
123
	 * @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...
124
	 */
125
	protected function __raw(string $sql, string $index = null, bool $assoc = true){
126
		$result = $this->db->query($sql);
127
128
		if(is_bool($result)){
129
			return $result;
130
		}
131
132
		$r = $this->getResult([$result, 'fetch_'.($assoc ? 'assoc' : 'row')], [], $index, $assoc);
133
134
		$result->free();
135
136
		return $r;
137
	}
138
139
	/**
140
	 * @param string      $sql
141
	 * @param array       $values
142
	 * @param string|null $index
143
	 * @param bool        $assoc
144
	 *
145
	 * @return bool|\chillerlan\Database\DBResult
146
	 */
147
	protected function __prepared(string $sql, array $values = [], string $index = null, bool $assoc = true){
148
		$stmt = $this->db->stmt_init();
149
		$stmt->prepare($sql);
150
151
		if(count($values) > 0){
152
			call_user_func_array([$stmt, 'bind_param'], $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
		call_user_func_array([$stmt, 'bind_result'], $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
		foreach($values as $row){
225
			call_user_func_array([$stmt, 'bind_param'], $this->getReferences($row));
226
227
			$stmt->execute();
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
		foreach($data as $row){
247
			if($row = call_user_func_array($callback, [$row])){
248
				call_user_func_array([$stmt, 'bind_param'], $this->getReferences($row));
249
250
				$stmt->execute();
251
			}
252
		}
253
254
		$stmt->close();
255
256
		return true;
257
	}
258
259
260
	/**
261
	 * Copies an array to an array of referenced values
262
	 *
263
	 * @param array $row
264
	 *
265
	 * @return array
266
	 * @see http://php.net/manual/mysqli-stmt.bind-param.php
267
	 */
268
	protected function getReferences(array $row){
269
		$references = [];
270
		$types      = [];
271
272
		foreach($row as &$field){
273
274
			switch(gettype($field)){
275
				case 'integer': $types[] = 'i'; break;
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...
276
				case 'double' : $types[] = 'd'; break; // @codeCoverageIgnore
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...
277
				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...
278
			}
279
280
			$references[] = &$field;
281
		}
282
283
		array_unshift($references, implode('', $types));
284
285
		return $references;
286
	}
287
288
}
289