Completed
Push — master ( 2fdc96...4f1f24 )
by Damian
12:09
created

MySQLiConnector   C

Complexity

Total Complexity 56

Size/Duplication

Total Lines 293
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Importance

Changes 1
Bugs 0 Features 1
Metric Value
wmc 56
c 1
b 0
f 1
lcom 1
cbo 4
dl 0
loc 293
rs 6.5957

19 Methods

Rating   Name   Duplication   Size   Complexity  
A setLastStatement() 0 3 1
A prepareStatement() 0 7 1
F connect() 0 42 9
A __destruct() 0 6 2
A escapeString() 0 3 1
A quoteString() 0 4 1
A getVersion() 0 3 1
A beforeQuery() 0 4 1
A query() 0 14 3
C parsePreparedParameters() 0 51 14
A bindParameters() 0 11 2
C preparedQuery() 0 42 8
A selectDatabase() 0 8 2
A getSelectedDatabase() 0 3 1
A unloadDatabase() 0 3 1
A isActive() 0 3 3
A affectedRows() 0 3 1
A getGeneratedID() 0 3 1
A getLastError() 0 7 3

How to fix   Complexity   

Complex Class

Complex classes like MySQLiConnector often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use MySQLiConnector, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * Connector for MySQL using the MySQLi method
5
 * @package framework
6
 * @subpackage model
7
 */
8
class MySQLiConnector extends DBConnector {
9
10
	/**
11
	 * Connection to the MySQL database
12
	 *
13
	 * @var MySQLi
14
	 */
15
	protected $dbConn = null;
16
17
	/**
18
	 * Name of the currently selected database
19
	 *
20
	 * @var string
21
	 */
22
	protected $databaseName = null;
23
24
	/**
25
	 * The most recent statement returned from MySQLiConnector->preparedQuery
26
	 *
27
	 * @var mysqli_stmt
28
	 */
29
	protected $lastStatement = null;
30
31
	/**
32
	 * Store the most recent statement for later use
33
	 *
34
	 * @param mysqli_stmt $statement
35
	 */
36
	protected function setLastStatement($statement) {
37
		$this->lastStatement = $statement;
38
	}
39
40
	/**
41
	 * Retrieve a prepared statement for a given SQL string
42
	 *
43
	 * @param string $sql
44
	 * @param boolean &$success
45
	 * @return mysqli_stmt
46
	 */
47
	public function prepareStatement($sql, &$success) {
48
		// Record last statement for error reporting
49
		$statement = $this->dbConn->stmt_init();
50
		$this->setLastStatement($statement);
51
		$success = $statement->prepare($sql);
52
		return $statement;
53
	}
54
55
	public function connect($parameters, $selectDB = false) {
56
		// Normally $selectDB is set to false by the MySQLDatabase controller, as per convention
57
		$selectedDB = ($selectDB && !empty($parameters['database'])) ? $parameters['database'] : null;
58
59
		// Connection charset and collation
60
		$connCharset = Config::inst()->get('MySQLDatabase', 'connection_charset');
61
		$connCollation = Config::inst()->get('MySQLDatabase', 'connection_collation');
62
63
		if(!empty($parameters['port'])) {
64
			$this->dbConn = new MySQLi(
65
				$parameters['server'],
66
				$parameters['username'],
67
				$parameters['password'],
68
				$selectedDB,
69
				$parameters['port']
70
			);
71
		} else {
72
			$this->dbConn = new MySQLi(
73
				$parameters['server'],
74
				$parameters['username'],
75
				$parameters['password'],
76
				$selectedDB
77
			);
78
		}
79
80
		if ($this->dbConn->connect_error) {
81
			$this->databaseError("Couldn't connect to MySQL database | " . $this->dbConn->connect_error);
82
		}
83
84
		// Set charset and collation if given and not null. Can explicitly set to empty string to omit
85
		$charset = isset($parameters['charset'])
86
				? $parameters['charset']
87
				: $connCharset;
88
89
		if (!empty($charset)) $this->dbConn->set_charset($charset);
90
91
		$collation = isset($parameters['collation'])
92
			? $parameters['collation']
93
			: $connCollation;
94
95
		if (!empty($collation)) $this->dbConn->query("SET collation_connection = {$collation}");
96
	}
97
98
	public function __destruct() {
99
		if ($this->dbConn) {
100
			mysqli_close($this->dbConn);
101
			$this->dbConn = null;
102
		}
103
	}
104
105
	public function escapeString($value) {
106
		return $this->dbConn->real_escape_string($value);
107
	}
108
109
	public function quoteString($value) {
110
		$value = $this->escapeString($value);
111
		return "'$value'";
112
	}
113
114
	public function getVersion() {
115
		return $this->dbConn->server_info;
116
	}
117
118
	/**
119
	 * Invoked before any query is executed
120
	 *
121
	 * @param string $sql
122
	 */
123
	protected function beforeQuery($sql) {
0 ignored issues
show
Unused Code introduced by
The parameter $sql is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
124
		// Clear the last statement
125
		$this->setLastStatement(null);
126
	}
127
128
	public function query($sql, $errorLevel = E_USER_ERROR) {
129
		$this->beforeQuery($sql);
130
131
		// Benchmark query
132
		$handle = $this->dbConn->query($sql, MYSQLI_STORE_RESULT);
133
134
		if (!$handle || $this->dbConn->error) {
135
			$this->databaseError($this->getLastError(), $errorLevel, $sql);
136
			return null;
137
		}
138
139
		// Some non-select queries return true on success
140
		return new MySQLQuery($this, $handle);
141
	}
142
143
	/**
144
	 * Prepares the list of parameters in preparation for passing to mysqli_stmt_bind_param
145
	 *
146
	 * @param array $parameters List of parameters
147
	 * @param array &$blobs Out parameter for list of blobs to bind separately
148
	 * @return array List of parameters appropriate for mysqli_stmt_bind_param function
149
	 */
150
	public function parsePreparedParameters($parameters, &$blobs) {
151
		$types = '';
152
		$values = array();
153
		$blobs = array();
154
		for($index = 0; $index < count($parameters); $index++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
155
			$value = $parameters[$index];
156
			$phpType = gettype($value);
157
158
			// Allow overriding of parameter type using an associative array
159
			if($phpType === 'array') {
160
				$phpType = $value['type'];
161
				$value = $value['value'];
162
			}
163
164
			// Convert php variable type to one that makes mysqli_stmt_bind_param happy
165
			// @see http://www.php.net/manual/en/mysqli-stmt.bind-param.php
166
			switch($phpType) {
167
				case 'boolean':
168
				case 'integer':
169
					$types .= 'i';
170
					break;
171
				case 'float': // Not actually returnable from gettype
172
				case 'double':
173
					$types .= 'd';
174
					break;
175
				case 'object': // Allowed if the object or resource has a __toString method
176
				case 'resource':
177
				case 'string':
178
				case 'NULL': // Take care that a where clause should use "where XX is null" not "where XX = null"
179
					$types .= 's';
180
					break;
181
				case 'blob':
182
					$types .= 'b';
183
					// Blobs must be sent via send_long_data and set to null here
184
					$blobs[] = array(
185
						'index' => $index,
186
						'value' => $value
187
					);
188
					$value = null;
189
					break;
190
				case 'array':
191
				case 'unknown type':
192
				default:
193
					user_error("Cannot bind parameter \"$value\" as it is an unsupported type ($phpType)",
194
						E_USER_ERROR);
195
					break;
196
			}
197
			$values[] = $value;
198
		}
199
		return array_merge(array($types), $values);
200
	}
201
202
	/**
203
	 * Binds a list of parameters to a statement
204
	 *
205
	 * @param mysqli_stmt $statement MySQLi statement
206
	 * @param array $parameters List of parameters to pass to bind_param
207
	 */
208
	public function bindParameters(mysqli_stmt $statement, array $parameters) {
209
		// Because mysqli_stmt::bind_param arguments must be passed by reference
210
		// we need to do a bit of hackery
211
		for ($i = 0; $i < count($parameters); $i++)
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
212
		{
213
			$boundName = "param$i";
214
			$$boundName = $parameters[$i];
215
			$boundNames[] = &$$boundName;
0 ignored issues
show
Coding Style Comprehensibility introduced by
$boundNames was never initialized. Although not strictly required by PHP, it is generally a good practice to add $boundNames = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
216
		}
217
		call_user_func_array( array($statement, 'bind_param'), $boundNames);
0 ignored issues
show
Bug introduced by
The variable $boundNames does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
218
	}
219
220
	public function preparedQuery($sql, $parameters, $errorLevel = E_USER_ERROR) {
221
		// Shortcut to basic query when not given parameters
222
		if(empty($parameters)) {
223
			return $this->query($sql, $errorLevel);
224
		}
225
226
		$this->beforeQuery($sql);
227
228
		// Type check, identify, and prepare parameters for passing to the statement bind function
229
		$parsedParameters = $this->parsePreparedParameters($parameters, $blobs);
230
231
		// Benchmark query
232
		$statement = $this->prepareStatement($sql, $success);
233
		if($success) {
234
			if($parsedParameters) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $parsedParameters of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
235
				$this->bindParameters($statement, $parsedParameters);
236
			}
237
238
			// Bind any blobs given
239
			foreach($blobs as $blob) {
240
				$statement->send_long_data($blob['index'], $blob['value']);
241
			}
242
243
			// Safely execute the statement
244
			$statement->execute();
245
		}
246
247
		if (!$success || $statement->error) {
248
			$values = $this->parameterValues($parameters);
249
			$this->databaseError($this->getLastError(), $errorLevel, $sql, $values);
250
			return null;
251
		}
252
253
		// Non-select queries will have no result data
254
		$metaData = $statement->result_metadata();
255
		if($metaData) {
256
			return new MySQLStatement($statement, $metaData);
257
		} else {
258
			// Replicate normal behaviour of ->query() on non-select calls
259
			return new MySQLQuery($this, true);
260
		}
261
	}
262
263
	public function selectDatabase($name) {
264
		if ($this->dbConn->select_db($name)) {
265
			$this->databaseName = $name;
266
			return true;
267
		} else {
268
			return false;
269
		}
270
	}
271
272
	public function getSelectedDatabase() {
273
		return $this->databaseName;
274
	}
275
276
	public function unloadDatabase() {
277
		$this->databaseName = null;
278
	}
279
280
	public function isActive() {
281
		return $this->databaseName && $this->dbConn && empty($this->dbConn->connect_error);
282
	}
283
284
	public function affectedRows() {
285
		return $this->dbConn->affected_rows;
286
	}
287
288
	public function getGeneratedID($table) {
289
		return $this->dbConn->insert_id;
290
	}
291
292
	public function getLastError() {
293
		// Check if a statement was used for the most recent query
294
		if($this->lastStatement && $this->lastStatement->error) {
295
			return $this->lastStatement->error;
296
		}
297
		return $this->dbConn->error;
298
	}
299
300
}
301