Passed
Pull Request — master (#7716)
by Damian
10:26 queued 01:42
created

PDOConnector::connect()   F

Complexity

Conditions 25
Paths 8000

Size

Total Lines 99
Code Lines 54

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 25
eloc 54
nc 8000
nop 2
dl 0
loc 99
rs 2
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 PDO;
7
use PDOStatement;
8
use InvalidArgumentException;
9
10
/**
11
 * PDO driver database connector
12
 */
13
class PDOConnector extends DBConnector
14
{
15
16
    /**
17
     * Should ATTR_EMULATE_PREPARES flag be used to emulate prepared statements?
18
     *
19
     * @config
20
     * @var boolean
21
     */
22
    private static $emulate_prepare = false;
0 ignored issues
show
introduced by
The private property $emulate_prepare is not used, and could be removed.
Loading history...
23
24
    /**
25
     * Default strong SSL cipher to be used
26
     *
27
     * @config
28
     * @var string
29
     */
30
    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...
31
32
    /**
33
     * The PDO connection instance
34
     *
35
     * @var PDO
36
     */
37
    protected $pdoConnection = null;
38
39
    /**
40
     * Name of the currently selected database
41
     *
42
     * @var string
43
     */
44
    protected $databaseName = null;
45
46
    /**
47
     * If available, the row count of the last executed statement
48
     *
49
     * @var int|null
50
     */
51
    protected $rowCount = null;
52
53
    /**
54
     * Error generated by the errorInfo() method of the last PDOStatement
55
     *
56
     * @var array|null
57
     */
58
    protected $lastStatementError = null;
59
60
    /**
61
     * List of prepared statements, cached by SQL string
62
     *
63
     * @var array
64
     */
65
    protected $cachedStatements = array();
66
67
    /**
68
     * Flush all prepared statements
69
     */
70
    public function flushStatements()
71
    {
72
        $this->cachedStatements = array();
73
    }
74
75
    /**
76
     * Retrieve a prepared statement for a given SQL string, or return an already prepared version if
77
     * one exists for the given query
78
     *
79
     * @param string $sql
80
     * @return PDOStatement
81
     */
82
    public function getOrPrepareStatement($sql)
83
    {
84
        // Return cached statements
85
        if (isset($this->cachedStatements[$sql])) {
86
            return $this->cachedStatements[$sql];
87
        }
88
89
        // Generate new statement
90
        $statement = $this->pdoConnection->prepare(
91
            $sql,
92
            array(PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY)
93
        );
94
95
        // Only cache select statements
96
        if (preg_match('/^(\s*)select\b/i', $sql)) {
97
            $this->cachedStatements[$sql] = $statement;
98
        }
99
        return $statement;
100
    }
101
102
    /**
103
     * Is PDO running in emulated mode
104
     *
105
     * @return boolean
106
     */
107
    public static function is_emulate_prepare()
108
    {
109
        return Config::inst()->get('SilverStripe\ORM\Connect\PDOConnector', 'emulate_prepare');
110
    }
111
112
    public function connect($parameters, $selectDB = false)
113
    {
114
        $this->flushStatements();
115
116
        // Build DSN string
117
        // Note that we don't select the database here until explicitly
118
        // requested via selectDatabase
119
        $driver = $parameters['driver'] . ":";
120
        $dsn = array();
121
122
        // Typically this is false, but some drivers will request this
123
        if ($selectDB) {
124
            // Specify complete file path immediately following driver (SQLLite3)
125
            if (!empty($parameters['filepath'])) {
126
                $dsn[] = $parameters['filepath'];
127
            } elseif (!empty($parameters['database'])) {
128
                // Some databases require a selected database at connection (SQLite3, Azure)
129
                if ($parameters['driver'] === 'sqlsrv') {
130
                    $dsn[] = "Database={$parameters['database']}";
131
                } else {
132
                    $dsn[] = "dbname={$parameters['database']}";
133
                }
134
            }
135
        }
136
137
        // Syntax for sql server is slightly different
138
        if ($parameters['driver'] === 'sqlsrv') {
139
            $server = $parameters['server'];
140
            if (!empty($parameters['port'])) {
141
                $server .= ",{$parameters['port']}";
142
            }
143
            $dsn[] = "Server=$server";
144
        } elseif ($parameters['driver'] === 'dblib') {
145
            $server = $parameters['server'];
146
            if (!empty($parameters['port'])) {
147
                $server .= ":{$parameters['port']}";
148
            }
149
            $dsn[] = "host={$server}";
150
        } else {
151
            if (!empty($parameters['server'])) {
152
                // Use Server instead of host for sqlsrv
153
                $dsn[] = "host={$parameters['server']}";
154
            }
155
156
            if (!empty($parameters['port'])) {
157
                $dsn[] = "port={$parameters['port']}";
158
            }
159
        }
160
161
        // Connection charset and collation
162
        $connCharset = Config::inst()->get('SilverStripe\ORM\Connect\MySQLDatabase', 'connection_charset');
163
        $connCollation = Config::inst()->get('SilverStripe\ORM\Connect\MySQLDatabase', 'connection_collation');
164
165
        // Set charset if given and not null. Can explicitly set to empty string to omit
166
        if (!in_array($parameters['driver'], ['sqlsrv', 'pgsql'])) {
167
            $charset = isset($parameters['charset'])
168
                    ? $parameters['charset']
169
                    : $connCharset;
170
            if (!empty($charset)) {
171
                $dsn[] = "charset=$charset";
172
            }
173
        }
174
175
        // Connection commands to be run on every re-connection
176
        if (!isset($charset)) {
177
            $charset = $connCharset;
178
        }
179
        $options = array(
180
            PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES ' . $charset . ' COLLATE ' . $connCollation
181
        );
182
183
        // Set SSL options if they are defined
184
        if (array_key_exists('ssl_key', $parameters) &&
185
            array_key_exists('ssl_cert', $parameters)
186
        ) {
187
            $options[PDO::MYSQL_ATTR_SSL_KEY] = $parameters['ssl_key'];
188
            $options[PDO::MYSQL_ATTR_SSL_CERT] = $parameters['ssl_cert'];
189
            if (array_key_exists('ssl_ca', $parameters)) {
190
                $options[PDO::MYSQL_ATTR_SSL_CA] = $parameters['ssl_ca'];
191
            }
192
            // use default cipher if not provided
193
            $options[PDO::MYSQL_ATTR_SSL_CIPHER] = array_key_exists('ssl_cipher', $parameters) ? $parameters['ssl_cipher'] : self::config()->get('ssl_cipher_default');
194
        }
195
196
        if (self::is_emulate_prepare()) {
197
            $options[PDO::ATTR_EMULATE_PREPARES] = true;
198
        }
199
200
        // May throw a PDOException if fails
201
        $this->pdoConnection = new PDO(
202
            $driver.implode(';', $dsn),
203
            empty($parameters['username']) ? '' : $parameters['username'],
204
            empty($parameters['password']) ? '' : $parameters['password'],
205
            $options
206
        );
207
208
        // Show selected DB if requested
209
        if ($this->pdoConnection && $selectDB && !empty($parameters['database'])) {
210
            $this->databaseName = $parameters['database'];
211
        }
212
    }
213
214
    public function getVersion()
215
    {
216
        return $this->pdoConnection->getAttribute(PDO::ATTR_SERVER_VERSION);
217
    }
218
219
    public function escapeString($value)
220
    {
221
        $value = $this->quoteString($value);
222
223
        // Since the PDO library quotes the value, we should remove this to maintain
224
        // consistency with MySQLDatabase::escapeString
225
        if (preg_match('/^\'(?<value>.*)\'$/', $value, $matches)) {
226
            $value = $matches['value'];
227
        }
228
        return $value;
229
    }
230
231
    public function quoteString($value)
232
    {
233
        return $this->pdoConnection->quote($value);
234
    }
235
236
    /**
237
     * Invoked before any query is executed
238
     *
239
     * @param string $sql
240
     */
241
    protected function beforeQuery($sql)
242
    {
243
        // Reset state
244
        $this->rowCount = 0;
245
        $this->lastStatementError = null;
246
247
        // Flush if necessary
248
        if ($this->isQueryDDL($sql)) {
249
            $this->flushStatements();
250
        }
251
    }
252
253
    /**
254
     * Executes a query that doesn't return a resultset
255
     *
256
     * @param string $sql The SQL query to execute
257
     * @param integer $errorLevel For errors to this query, raise PHP errors
258
     * using this error level.
259
     * @return int
260
     */
261
    public function exec($sql, $errorLevel = E_USER_ERROR)
262
    {
263
        $this->beforeQuery($sql);
264
265
        // Directly exec this query
266
        $result = $this->pdoConnection->exec($sql);
267
268
        // Check for errors
269
        if ($result !== false) {
270
            return $this->rowCount = $result;
271
        }
272
273
        $this->databaseError($this->getLastError(), $errorLevel, $sql);
274
        return null;
275
    }
276
277
    public function query($sql, $errorLevel = E_USER_ERROR)
278
    {
279
        $this->beforeQuery($sql);
280
281
        // Directly query against connection
282
        $statement = $this->pdoConnection->query($sql);
283
284
        // Generate results
285
        return $this->prepareResults($statement, $errorLevel, $sql);
286
    }
287
288
    /**
289
     * Determines the PDO::PARAM_* type for a given PHP type string
290
     * @param string $phpType Type of object in PHP
291
     * @return integer PDO Parameter constant value
292
     */
293
    public function getPDOParamType($phpType)
294
    {
295
        switch ($phpType) {
296
            case 'boolean':
297
                return PDO::PARAM_BOOL;
298
            case 'NULL':
299
                return PDO::PARAM_NULL;
300
            case 'integer':
301
                return PDO::PARAM_INT;
302
            case 'object': // Allowed if the object or resource has a __toString method
303
            case 'resource':
304
            case 'float': // Not actually returnable from get_type
305
            case 'double':
306
            case 'string':
307
                return PDO::PARAM_STR;
308
            case 'blob':
309
                return PDO::PARAM_LOB;
310
            case 'array':
311
            case 'unknown type':
312
            default:
313
                throw new InvalidArgumentException("Cannot bind parameter as it is an unsupported type ($phpType)");
314
        }
315
    }
316
317
    /**
318
     * Bind all parameters to a PDOStatement
319
     *
320
     * @param PDOStatement $statement
321
     * @param array $parameters
322
     */
323
    public function bindParameters(PDOStatement $statement, $parameters)
324
    {
325
        // Bind all parameters
326
        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...
327
            $value = $parameters[$index];
328
            $phpType = gettype($value);
329
330
            // Allow overriding of parameter type using an associative array
331
            if ($phpType === 'array') {
332
                $phpType = $value['type'];
333
                $value = $value['value'];
334
            }
335
336
            // Check type of parameter
337
            $type = $this->getPDOParamType($phpType);
338
            if ($type === PDO::PARAM_STR) {
339
                $value = strval($value);
340
            }
341
342
            // Bind this value
343
            $statement->bindValue($index+1, $value, $type);
344
        }
345
    }
346
347
    public function preparedQuery($sql, $parameters, $errorLevel = E_USER_ERROR)
348
    {
349
        $this->beforeQuery($sql);
350
351
        // Prepare statement
352
        $statement = $this->getOrPrepareStatement($sql);
353
354
        // Bind and invoke statement safely
355
        if ($statement) {
356
            $this->bindParameters($statement, $parameters);
357
            $statement->execute($parameters);
358
        }
359
360
        // Generate results
361
        return $this->prepareResults($statement, $errorLevel, $sql);
362
    }
363
364
    /**
365
     * Given a PDOStatement that has just been executed, generate results
366
     * and report any errors
367
     *
368
     * @param PDOStatement $statement
369
     * @param int $errorLevel
370
     * @param string $sql
371
     * @param array $parameters
372
     * @return PDOQuery
373
     */
374
    protected function prepareResults($statement, $errorLevel, $sql, $parameters = array())
375
    {
376
377
        // Record row-count and errors of last statement
378
        if ($this->hasError($statement)) {
379
            $this->lastStatementError = $statement->errorInfo();
380
        } elseif ($statement) {
381
            // Count and return results
382
            $this->rowCount = $statement->rowCount();
383
            return new PDOQuery($statement);
384
        }
385
386
        // Ensure statement is closed
387
        if ($statement) {
388
            $statement->closeCursor();
389
            unset($statement);
390
        }
391
392
        // Report any errors
393
        if ($parameters) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $parameters 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...
394
            $parameters = $this->parameterValues($parameters);
395
        }
396
        $this->databaseError($this->getLastError(), $errorLevel, $sql, $parameters);
397
        return null;
398
    }
399
400
    /**
401
     * Determine if a resource has an attached error
402
     *
403
     * @param PDOStatement|PDO $resource the resource to check
404
     * @return boolean Flag indicating true if the resource has an error
405
     */
406
    protected function hasError($resource)
407
    {
408
        // No error if no resource
409
        if (empty($resource)) {
410
            return false;
411
        }
412
413
        // If the error code is empty the statement / connection has not been run yet
414
        $code = $resource->errorCode();
415
        if (empty($code)) {
416
            return false;
417
        }
418
419
        // Skip 'ok' and undefined 'warning' types.
420
        // @see http://docstore.mik.ua/orelly/java-ent/jenut/ch08_06.htm
421
        return $code !== '00000' && $code !== '01000';
422
    }
423
424
    public function getLastError()
425
    {
426
        $error = null;
427
        if ($this->lastStatementError) {
428
            $error = $this->lastStatementError;
429
        } elseif ($this->hasError($this->pdoConnection)) {
430
            $error = $this->pdoConnection->errorInfo();
431
        }
432
        if ($error) {
433
            return sprintf("%s-%s: %s", $error[0], $error[1], $error[2]);
434
        }
435
        return null;
436
    }
437
438
    public function getGeneratedID($table)
439
    {
440
        return $this->pdoConnection->lastInsertId();
441
    }
442
443
    public function affectedRows()
444
    {
445
        return $this->rowCount;
446
    }
447
448
    public function selectDatabase($name)
449
    {
450
        $this->exec("USE \"{$name}\"");
451
        $this->databaseName = $name;
452
        return true;
453
    }
454
455
    public function getSelectedDatabase()
456
    {
457
        return $this->databaseName;
458
    }
459
460
    public function unloadDatabase()
461
    {
462
        $this->databaseName = null;
463
    }
464
465
    public function isActive()
466
    {
467
        return $this->databaseName && $this->pdoConnection;
468
    }
469
}
470