MySQLiConnector::connect()   F
last analyzed

Complexity

Conditions 14
Paths 512

Size

Total Lines 65
Code Lines 39

Duplication

Lines 0
Ratio 0 %

Importance

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