Passed
Push — 4.3 ( e40788...24b9db )
by Sam
10:17 queued 02:40
created

src/ORM/Connect/MySQLiConnector.php (2 issues)

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';
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('SilverStripe\ORM\Connect\MySQLDatabase', 'connection_charset');
77
        $connCollation = Config::inst()->get('SilverStripe\ORM\Connect\MySQLDatabase', 'connection_collation');
78
79
        $this->dbConn = mysqli_init();
80
81
        // Set SSL parameters if they exist. All parameters are required.
82
        if (array_key_exists('ssl_key', $parameters) &&
83
            array_key_exists('ssl_cert', $parameters) &&
84
            array_key_exists('ssl_ca', $parameters)) {
85
            $this->dbConn->ssl_set(
86
                $parameters['ssl_key'],
87
                $parameters['ssl_cert'],
88
                $parameters['ssl_ca'],
89
                dirname($parameters['ssl_ca']),
90
                array_key_exists('ssl_cipher', $parameters)
91
                    ? $parameters['ssl_cipher']
92
                    : self::config()->get('ssl_cipher_default')
93
            );
94
        }
95
96
        $this->dbConn->real_connect(
97
            $parameters['server'],
98
            $parameters['username'],
99
            $parameters['password'],
100
            $selectedDB,
101
            !empty($parameters['port']) ? $parameters['port'] : ini_get("mysqli.default_port")
102
        );
103
104
        if ($this->dbConn->connect_error) {
105
            $this->databaseError("Couldn't connect to MySQL database | " . $this->dbConn->connect_error);
106
        }
107
108
        // Set charset and collation if given and not null. Can explicitly set to empty string to omit
109
        $charset = isset($parameters['charset'])
110
                ? $parameters['charset']
111
                : $connCharset;
112
113
        if (!empty($charset)) {
114
            $this->dbConn->set_charset($charset);
115
        }
116
117
        $collation = isset($parameters['collation'])
118
            ? $parameters['collation']
119
            : $connCollation;
120
121
        if (!empty($collation)) {
122
            $this->dbConn->query("SET collation_connection = {$collation}");
123
        }
124
    }
125
126
    public function __destruct()
127
    {
128
        if (is_resource($this->dbConn)) {
129
            mysqli_close($this->dbConn);
130
            $this->dbConn = null;
131
        }
132
    }
133
134
    public function escapeString($value)
135
    {
136
        return $this->dbConn->real_escape_string($value);
137
    }
138
139
    public function quoteString($value)
140
    {
141
        $value = $this->escapeString($value);
142
        return "'$value'";
143
    }
144
145
    public function getVersion()
146
    {
147
        return $this->dbConn->server_info;
148
    }
149
150
    /**
151
     * Invoked before any query is executed
152
     *
153
     * @param string $sql
154
     */
155
    protected function beforeQuery($sql)
156
    {
157
        // Clear the last statement
158
        $this->setLastStatement(null);
159
    }
160
161
    public function query($sql, $errorLevel = E_USER_ERROR)
162
    {
163
        $this->beforeQuery($sql);
164
165
        // Benchmark query
166
        $handle = $this->dbConn->query($sql, MYSQLI_STORE_RESULT);
167
168
        if (!$handle || $this->dbConn->error) {
169
            $this->databaseError($this->getLastError(), $errorLevel, $sql);
170
            return null;
171
        }
172
173
        // Some non-select queries return true on success
174
        return new MySQLQuery($this, $handle);
175
    }
176
177
    /**
178
     * Prepares the list of parameters in preparation for passing to mysqli_stmt_bind_param
179
     *
180
     * @param array $parameters List of parameters
181
     * @param array &$blobs Out parameter for list of blobs to bind separately
182
     * @return array List of parameters appropriate for mysqli_stmt_bind_param function
183
     */
184
    public function parsePreparedParameters($parameters, &$blobs)
185
    {
186
        $types = '';
187
        $values = array();
188
        $blobs = array();
189
        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...
190
            $value = $parameters[$index];
191
            $phpType = gettype($value);
192
193
            // Allow overriding of parameter type using an associative array
194
            if ($phpType === 'array') {
195
                $phpType = $value['type'];
196
                $value = $value['value'];
197
            }
198
199
            // Convert php variable type to one that makes mysqli_stmt_bind_param happy
200
            // @see http://www.php.net/manual/en/mysqli-stmt.bind-param.php
201
            switch ($phpType) {
202
                case 'boolean':
203
                case 'integer':
204
                    $types .= 'i';
205
                    break;
206
                case 'float': // Not actually returnable from gettype
207
                case 'double':
208
                    $types .= 'd';
209
                    break;
210
                case 'object': // Allowed if the object or resource has a __toString method
211
                case 'resource':
212
                case 'string':
213
                case 'NULL': // Take care that a where clause should use "where XX is null" not "where XX = null"
214
                    $types .= 's';
215
                    break;
216
                case 'blob':
217
                    $types .= 'b';
218
                    // Blobs must be sent via send_long_data and set to null here
219
                    $blobs[] = array(
220
                        'index' => $index,
221
                        'value' => $value
222
                    );
223
                    $value = null;
224
                    break;
225
                case 'array':
226
                case 'unknown type':
227
                default:
228
                    user_error(
229
                        "Cannot bind parameter \"$value\" as it is an unsupported type ($phpType)",
230
                        E_USER_ERROR
231
                    );
232
                    break;
233
            }
234
            $values[] = $value;
235
        }
236
        return array_merge(array($types), $values);
237
    }
238
239
    /**
240
     * Binds a list of parameters to a statement
241
     *
242
     * @param mysqli_stmt $statement MySQLi statement
243
     * @param array $parameters List of parameters to pass to bind_param
244
     */
245
    public function bindParameters(mysqli_stmt $statement, array $parameters)
246
    {
247
        // Because mysqli_stmt::bind_param arguments must be passed by reference
248
        // we need to do a bit of hackery
249
        $boundNames = [];
250
        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...
251
            $boundName = "param$i";
252
            $$boundName = $parameters[$i];
253
            $boundNames[] = &$$boundName;
254
        }
255
        call_user_func_array(array($statement, 'bind_param'), $boundNames);
256
    }
257
258
    public function preparedQuery($sql, $parameters, $errorLevel = E_USER_ERROR)
259
    {
260
        // Shortcut to basic query when not given parameters
261
        if (empty($parameters)) {
262
            return $this->query($sql, $errorLevel);
263
        }
264
265
        $this->beforeQuery($sql);
266
267
        // Type check, identify, and prepare parameters for passing to the statement bind function
268
        $parsedParameters = $this->parsePreparedParameters($parameters, $blobs);
269
270
        // Benchmark query
271
        $statement = $this->prepareStatement($sql, $success);
272
        if ($success) {
273
            if ($parsedParameters) {
274
                $this->bindParameters($statement, $parsedParameters);
275
            }
276
277
            // Bind any blobs given
278
            foreach ($blobs as $blob) {
279
                $statement->send_long_data($blob['index'], $blob['value']);
280
            }
281
282
            // Safely execute the statement
283
            $statement->execute();
284
        }
285
286
        if (!$success || $statement->error) {
287
            $values = $this->parameterValues($parameters);
288
            $this->databaseError($this->getLastError(), $errorLevel, $sql, $values);
289
            return null;
290
        }
291
292
        // Non-select queries will have no result data
293
        $metaData = $statement->result_metadata();
294
        if ($metaData) {
295
            return new MySQLStatement($statement, $metaData);
296
        } else {
297
            // Replicate normal behaviour of ->query() on non-select calls
298
            return new MySQLQuery($this, true);
299
        }
300
    }
301
302
    public function selectDatabase($name)
303
    {
304
        if ($this->dbConn->select_db($name)) {
305
            $this->databaseName = $name;
306
            return true;
307
        } else {
308
            return false;
309
        }
310
    }
311
312
    public function getSelectedDatabase()
313
    {
314
        return $this->databaseName;
315
    }
316
317
    public function unloadDatabase()
318
    {
319
        $this->databaseName = null;
320
    }
321
322
    public function isActive()
323
    {
324
        return $this->databaseName && $this->dbConn && empty($this->dbConn->connect_error);
325
    }
326
327
    public function affectedRows()
328
    {
329
        return $this->dbConn->affected_rows;
330
    }
331
332
    public function getGeneratedID($table)
333
    {
334
        return $this->dbConn->insert_id;
335
    }
336
337
    public function getLastError()
338
    {
339
        // Check if a statement was used for the most recent query
340
        if ($this->lastStatement && $this->lastStatement->error) {
341
            return $this->lastStatement->error;
342
        }
343
        return $this->dbConn->error;
344
    }
345
}
346