Passed
Push — int-types ( ac3d76 )
by Sam
09:27 queued 36s
created

MySQLiConnector::parsePreparedParameters()   C

Complexity

Conditions 14
Paths 25

Size

Total Lines 53
Code Lines 40

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 14
eloc 40
nc 25
nop 2
dl 0
loc 53
rs 6.2666
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace SilverStripe\ORM\Connect;
4
5
use SilverStripe\Core\Config\Config;
6
use mysqli;
7
use mysqli_stmt;
8
9
/**
10
 * Connector for MySQL using the MySQLi method
11
 */
12
class MySQLiConnector extends DBConnector
13
{
14
15
    /**
16
     * Default strong SSL cipher to be used
17
     *
18
     * @config
19
     * @var string
20
     */
21
    private static $ssl_cipher_default = 'DHE-RSA-AES256-SHA';
0 ignored issues
show
introduced by
The private property $ssl_cipher_default is not used, and could be removed.
Loading history...
22
23
    /**
24
     * Connection to the MySQL database
25
     *
26
     * @var mysqli
27
     */
28
    protected $dbConn = null;
29
30
    /**
31
     * Name of the currently selected database
32
     *
33
     * @var string
34
     */
35
    protected $databaseName = null;
36
37
    /**
38
     * The most recent statement returned from MySQLiConnector->preparedQuery
39
     *
40
     * @var mysqli_stmt
41
     */
42
    protected $lastStatement = null;
43
44
    /**
45
     * Store the most recent statement for later use
46
     *
47
     * @param mysqli_stmt $statement
48
     */
49
    protected function setLastStatement($statement)
50
    {
51
        $this->lastStatement = $statement;
52
    }
53
54
    /**
55
     * Retrieve a prepared statement for a given SQL string
56
     *
57
     * @param string $sql
58
     * @param boolean &$success
59
     * @return mysqli_stmt
60
     */
61
    public function prepareStatement($sql, &$success)
62
    {
63
        // Record last statement for error reporting
64
        $statement = $this->dbConn->stmt_init();
65
        $this->setLastStatement($statement);
66
        $success = $statement->prepare($sql);
67
        return $statement;
68
    }
69
70
    public function connect($parameters, $selectDB = false)
71
    {
72
        // Normally $selectDB is set to false by the MySQLDatabase controller, as per convention
73
        $selectedDB = ($selectDB && !empty($parameters['database'])) ? $parameters['database'] : null;
74
75
        // Connection charset and collation
76
        $connCharset = $this->config()->get('connection_charset');
77
        $connCollation = $this->config()->get('connection_collation');
78
79
        $this->dbConn = mysqli_init();
80
81
        // Use native types (MysqlND only)
82
        if (defined('MYSQLI_OPT_INT_AND_FLOAT_NATIVE')) {
83
            $this->dbConn->options(MYSQLI_OPT_INT_AND_FLOAT_NATIVE, true);
84
85
        // The alternative is not ideal, throw a notice-level error
86
        } else {
87
            user_error(
88
                'mysqlnd PHP library is not available, numeric values will be fetched from the DB as strings',
89
                E_USER_NOTICE
90
            );
91
        }
92
93
        // Set SSL parameters if they exist. All parameters are required.
94
        if (array_key_exists('ssl_key', $parameters) &&
95
            array_key_exists('ssl_cert', $parameters) &&
96
            array_key_exists('ssl_ca', $parameters)) {
97
            $this->dbConn->ssl_set(
98
                $parameters['ssl_key'],
99
                $parameters['ssl_cert'],
100
                $parameters['ssl_ca'],
101
                dirname($parameters['ssl_ca']),
102
                array_key_exists('ssl_cipher', $parameters)
103
                    ? $parameters['ssl_cipher']
104
                    : self::config()->get('ssl_cipher_default')
105
            );
106
        }
107
108
        $this->dbConn->real_connect(
109
            $parameters['server'],
110
            $parameters['username'],
111
            $parameters['password'],
112
            $selectedDB,
113
            !empty($parameters['port']) ? $parameters['port'] : ini_get("mysqli.default_port")
0 ignored issues
show
Bug introduced by
It seems like ! empty($parameters['por...('mysqli.default_port') can also be of type string; however, parameter $port of mysqli::real_connect() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

113
            /** @scrutinizer ignore-type */ !empty($parameters['port']) ? $parameters['port'] : ini_get("mysqli.default_port")
Loading history...
114
        );
115
116
        if ($this->dbConn->connect_error) {
117
            $this->databaseError("Couldn't connect to MySQL database | " . $this->dbConn->connect_error);
118
        }
119
120
        // Set charset and collation if given and not null. Can explicitly set to empty string to omit
121
        $charset = isset($parameters['charset'])
122
                ? $parameters['charset']
123
                : $connCharset;
124
125
        if (!empty($charset)) {
126
            $this->dbConn->set_charset($charset);
127
        }
128
129
        $collation = isset($parameters['collation'])
130
            ? $parameters['collation']
131
            : $connCollation;
132
133
        if (!empty($collation)) {
134
            $this->dbConn->query("SET collation_connection = {$collation}");
135
        }
136
    }
137
138
    public function __destruct()
139
    {
140
        if (is_resource($this->dbConn)) {
0 ignored issues
show
introduced by
The condition is_resource($this->dbConn) is always false.
Loading history...
141
            mysqli_close($this->dbConn);
142
            $this->dbConn = null;
143
        }
144
    }
145
146
    public function escapeString($value)
147
    {
148
        return $this->dbConn->real_escape_string($value);
149
    }
150
151
    public function quoteString($value)
152
    {
153
        $value = $this->escapeString($value);
154
        return "'$value'";
155
    }
156
157
    public function getVersion()
158
    {
159
        return $this->dbConn->server_info;
160
    }
161
162
    /**
163
     * Invoked before any query is executed
164
     *
165
     * @param string $sql
166
     */
167
    protected function beforeQuery($sql)
0 ignored issues
show
Unused Code introduced by
The parameter $sql is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

167
    protected function beforeQuery(/** @scrutinizer ignore-unused */ $sql)

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

Loading history...
168
    {
169
        // Clear the last statement
170
        $this->setLastStatement(null);
171
    }
172
173
    public function query($sql, $errorLevel = E_USER_ERROR)
174
    {
175
        $this->beforeQuery($sql);
176
177
        // Benchmark query
178
        $handle = $this->dbConn->query($sql, MYSQLI_STORE_RESULT);
179
180
        if (!$handle || $this->dbConn->error) {
181
            $this->databaseError($this->getLastError(), $errorLevel, $sql);
182
            return null;
183
        }
184
185
        // Some non-select queries return true on success
186
        return new MySQLQuery($this, $handle);
187
    }
188
189
    /**
190
     * Prepares the list of parameters in preparation for passing to mysqli_stmt_bind_param
191
     *
192
     * @param array $parameters List of parameters
193
     * @param array &$blobs Out parameter for list of blobs to bind separately
194
     * @return array List of parameters appropriate for mysqli_stmt_bind_param function
195
     */
196
    public function parsePreparedParameters($parameters, &$blobs)
197
    {
198
        $types = '';
199
        $values = array();
200
        $blobs = array();
201
        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...
202
            $value = $parameters[$index];
203
            $phpType = gettype($value);
204
205
            // Allow overriding of parameter type using an associative array
206
            if ($phpType === 'array') {
207
                $phpType = $value['type'];
208
                $value = $value['value'];
209
            }
210
211
            // Convert php variable type to one that makes mysqli_stmt_bind_param happy
212
            // @see http://www.php.net/manual/en/mysqli-stmt.bind-param.php
213
            switch ($phpType) {
214
                case 'boolean':
215
                case 'integer':
216
                    $types .= 'i';
217
                    break;
218
                case 'float': // Not actually returnable from gettype
219
                case 'double':
220
                    $types .= 'd';
221
                    break;
222
                case 'object': // Allowed if the object or resource has a __toString method
223
                case 'resource':
224
                case 'string':
225
                case 'NULL': // Take care that a where clause should use "where XX is null" not "where XX = null"
226
                    $types .= 's';
227
                    break;
228
                case 'blob':
229
                    $types .= 'b';
230
                    // Blobs must be sent via send_long_data and set to null here
231
                    $blobs[] = array(
232
                        'index' => $index,
233
                        'value' => $value
234
                    );
235
                    $value = null;
236
                    break;
237
                case 'array':
238
                case 'unknown type':
239
                default:
240
                    user_error(
241
                        "Cannot bind parameter \"$value\" as it is an unsupported type ($phpType)",
242
                        E_USER_ERROR
243
                    );
244
                    break;
245
            }
246
            $values[] = $value;
247
        }
248
        return array_merge(array($types), $values);
249
    }
250
251
    /**
252
     * Binds a list of parameters to a statement
253
     *
254
     * @param mysqli_stmt $statement MySQLi statement
255
     * @param array $parameters List of parameters to pass to bind_param
256
     */
257
    public function bindParameters(mysqli_stmt $statement, array $parameters)
258
    {
259
        // Because mysqli_stmt::bind_param arguments must be passed by reference
260
        // we need to do a bit of hackery
261
        $boundNames = [];
262
        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...
263
            $boundName = "param$i";
264
            $$boundName = $parameters[$i];
265
            $boundNames[] = &$$boundName;
266
        }
267
        call_user_func_array(array($statement, 'bind_param'), $boundNames);
268
    }
269
270
    public function preparedQuery($sql, $parameters, $errorLevel = E_USER_ERROR)
271
    {
272
        // Shortcut to basic query when not given parameters
273
        if (empty($parameters)) {
274
            return $this->query($sql, $errorLevel);
275
        }
276
277
        $this->beforeQuery($sql);
278
279
        // Type check, identify, and prepare parameters for passing to the statement bind function
280
        $parsedParameters = $this->parsePreparedParameters($parameters, $blobs);
281
282
        // Benchmark query
283
        $statement = $this->prepareStatement($sql, $success);
284
        if ($success) {
285
            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...
286
                $this->bindParameters($statement, $parsedParameters);
287
            }
288
289
            // Bind any blobs given
290
            foreach ($blobs as $blob) {
291
                $statement->send_long_data($blob['index'], $blob['value']);
292
            }
293
294
            // Safely execute the statement
295
            $statement->execute();
296
        }
297
298
        if (!$success || $statement->error) {
299
            $values = $this->parameterValues($parameters);
300
            $this->databaseError($this->getLastError(), $errorLevel, $sql, $values);
301
            return null;
302
        }
303
304
        // Non-select queries will have no result data
305
        $metaData = $statement->result_metadata();
306
        if ($metaData) {
0 ignored issues
show
introduced by
$metaData is of type mysqli_result, thus it always evaluated to true.
Loading history...
307
            return new MySQLStatement($statement, $metaData);
308
        } else {
309
            // Replicate normal behaviour of ->query() on non-select calls
310
            return new MySQLQuery($this, true);
311
        }
312
    }
313
314
    public function selectDatabase($name)
315
    {
316
        if ($this->dbConn->select_db($name)) {
317
            $this->databaseName = $name;
318
            return true;
319
        } else {
320
            return false;
321
        }
322
    }
323
324
    public function getSelectedDatabase()
325
    {
326
        return $this->databaseName;
327
    }
328
329
    public function unloadDatabase()
330
    {
331
        $this->databaseName = null;
332
    }
333
334
    public function isActive()
335
    {
336
        return $this->databaseName && $this->dbConn && empty($this->dbConn->connect_error);
337
    }
338
339
    public function affectedRows()
340
    {
341
        return $this->dbConn->affected_rows;
342
    }
343
344
    public function getGeneratedID($table)
345
    {
346
        return $this->dbConn->insert_id;
347
    }
348
349
    public function getLastError()
350
    {
351
        // Check if a statement was used for the most recent query
352
        if ($this->lastStatement && $this->lastStatement->error) {
353
            return $this->lastStatement->error;
354
        }
355
        return $this->dbConn->error;
356
    }
357
}
358