Passed
Push — int-types ( 274c73...c29a72 )
by Sam
05:04
created

PDOConnector::quoteString()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
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 implements TransactionManager
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
     * Is a transaction currently active?
69
     * @var bool
70
     */
71
    protected $inTransaction = false;
72
73
    /**
74
     * Driver
75
     * @var string
76
     */
77
    private $driver = null;
78
79
    /**
80
     * Flush all prepared statements
81
     */
82
    public function flushStatements()
83
    {
84
        $this->cachedStatements = array();
85
    }
86
87
    /**
88
     * Retrieve a prepared statement for a given SQL string, or return an already prepared version if
89
     * one exists for the given query
90
     *
91
     * @param string $sql
92
     * @return PDOStatement
93
     */
94
    public function getOrPrepareStatement($sql)
95
    {
96
        // Return cached statements
97
        if (isset($this->cachedStatements[$sql])) {
98
            return $this->cachedStatements[$sql];
99
        }
100
101
        // Generate new statement
102
        $statement = $this->pdoConnection->prepare(
103
            $sql,
104
            array(PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY)
105
        );
106
107
        // Only cache select statements
108
        if (preg_match('/^(\s*)select\b/i', $sql)) {
109
            $this->cachedStatements[$sql] = $statement;
110
        }
111
        return $statement;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $statement also could return the type boolean which is incompatible with the documented return type PDOStatement.
Loading history...
112
    }
113
114
    /**
115
     * Is PDO running in emulated mode
116
     *
117
     * @return boolean
118
     */
119
    public static function is_emulate_prepare()
120
    {
121
        return static::config()->get('emulate_prepare');
122
    }
123
124
    public function connect($parameters, $selectDB = false)
125
    {
126
        $this->flushStatements();
127
128
        // Note that we don't select the database here until explicitly
129
        // requested via selectDatabase
130
        $this->driver = $parameters['driver'];
131
132
        // Build DSN string
133
        $dsn = array();
134
135
        // Typically this is false, but some drivers will request this
136
        if ($selectDB) {
137
            // Specify complete file path immediately following driver (SQLLite3)
138
            if (!empty($parameters['filepath'])) {
139
                $dsn[] = $parameters['filepath'];
140
            } elseif (!empty($parameters['database'])) {
141
                // Some databases require a selected database at connection (SQLite3, Azure)
142
                if ($parameters['driver'] === 'sqlsrv') {
143
                    $dsn[] = "Database={$parameters['database']}";
144
                } else {
145
                    $dsn[] = "dbname={$parameters['database']}";
146
                }
147
            }
148
        }
149
150
        // Syntax for sql server is slightly different
151
        if ($parameters['driver'] === 'sqlsrv') {
152
            $server = $parameters['server'];
153
            if (!empty($parameters['port'])) {
154
                $server .= ",{$parameters['port']}";
155
            }
156
            $dsn[] = "Server=$server";
157
        } elseif ($parameters['driver'] === 'dblib') {
158
            $server = $parameters['server'];
159
            if (!empty($parameters['port'])) {
160
                $server .= ":{$parameters['port']}";
161
            }
162
            $dsn[] = "host={$server}";
163
        } else {
164
            if (!empty($parameters['server'])) {
165
                // Use Server instead of host for sqlsrv
166
                $dsn[] = "host={$parameters['server']}";
167
            }
168
169
            if (!empty($parameters['port'])) {
170
                $dsn[] = "port={$parameters['port']}";
171
            }
172
        }
173
174
        // Connection charset and collation
175
        $connCharset = Config::inst()->get('SilverStripe\ORM\Connect\MySQLDatabase', 'connection_charset');
176
        $connCollation = Config::inst()->get('SilverStripe\ORM\Connect\MySQLDatabase', 'connection_collation');
177
178
        // Set charset if given and not null. Can explicitly set to empty string to omit
179
        if (!in_array($parameters['driver'], ['sqlsrv', 'pgsql'])) {
180
            $charset = isset($parameters['charset'])
181
                    ? $parameters['charset']
182
                    : $connCharset;
183
            if (!empty($charset)) {
184
                $dsn[] = "charset=$charset";
185
            }
186
        }
187
188
        // Connection commands to be run on every re-connection
189
        if (!isset($charset)) {
190
            $charset = $connCharset;
191
        }
192
193
        $options = [];
194
        if ($parameters['driver'] === 'mysql') {
195
            $options[PDO::MYSQL_ATTR_INIT_COMMAND] = 'SET NAMES ' . $charset . ' COLLATE ' . $connCollation;
196
        }
197
198
        // Set SSL options if they are defined
199
        if (array_key_exists('ssl_key', $parameters) &&
200
            array_key_exists('ssl_cert', $parameters)
201
        ) {
202
            $options[PDO::MYSQL_ATTR_SSL_KEY] = $parameters['ssl_key'];
203
            $options[PDO::MYSQL_ATTR_SSL_CERT] = $parameters['ssl_cert'];
204
            if (array_key_exists('ssl_ca', $parameters)) {
205
                $options[PDO::MYSQL_ATTR_SSL_CA] = $parameters['ssl_ca'];
206
            }
207
            // use default cipher if not provided
208
            $options[PDO::MYSQL_ATTR_SSL_CIPHER] = array_key_exists('ssl_cipher', $parameters) ? $parameters['ssl_cipher'] : self::config()->get('ssl_cipher_default');
209
        }
210
211
        // Set emulate prepares (unless null / default)
212
        $isEmulatePrepares = self::is_emulate_prepare();
213
        if (isset($isEmulatePrepares)) {
214
            $options[PDO::ATTR_EMULATE_PREPARES] = (bool)$isEmulatePrepares;
215
        }
216
217
        // Disable stringified fetches
218
        $options[PDO::ATTR_STRINGIFY_FETCHES] = false;
219
220
        // May throw a PDOException if fails
221
        $this->pdoConnection = new PDO(
222
            $this->driver . ':' . implode(';', $dsn),
223
            empty($parameters['username']) ? '' : $parameters['username'],
224
            empty($parameters['password']) ? '' : $parameters['password'],
225
            $options
226
        );
227
228
        // Show selected DB if requested
229
        if ($this->pdoConnection && $selectDB && !empty($parameters['database'])) {
230
            $this->databaseName = $parameters['database'];
231
        }
232
    }
233
234
235
    /**
236
     * Return the driver for this connector
237
     * E.g. 'mysql', 'sqlsrv', 'pgsql'
238
     */
239
    public function getDriver()
240
    {
241
        return $this->driver;
242
    }
243
244
    public function getVersion()
245
    {
246
        return $this->pdoConnection->getAttribute(PDO::ATTR_SERVER_VERSION);
247
    }
248
249
    public function escapeString($value)
250
    {
251
        $value = $this->quoteString($value);
252
253
        // Since the PDO library quotes the value, we should remove this to maintain
254
        // consistency with MySQLDatabase::escapeString
255
        if (preg_match('/^\'(?<value>.*)\'$/', $value, $matches)) {
256
            $value = $matches['value'];
257
        }
258
        return $value;
259
    }
260
261
    public function quoteString($value)
262
    {
263
        return $this->pdoConnection->quote($value);
264
    }
265
266
    /**
267
     * Invoked before any query is executed
268
     *
269
     * @param string $sql
270
     */
271
    protected function beforeQuery($sql)
272
    {
273
        // Reset state
274
        $this->rowCount = 0;
275
        $this->lastStatementError = null;
276
277
        // Flush if necessary
278
        if ($this->isQueryDDL($sql)) {
279
            $this->flushStatements();
280
        }
281
    }
282
283
    /**
284
     * Executes a query that doesn't return a resultset
285
     *
286
     * @param string $sql The SQL query to execute
287
     * @param integer $errorLevel For errors to this query, raise PHP errors
288
     * using this error level.
289
     * @return int
290
     */
291
    public function exec($sql, $errorLevel = E_USER_ERROR)
292
    {
293
        $this->beforeQuery($sql);
294
295
        // Directly exec this query
296
        $result = $this->pdoConnection->exec($sql);
297
298
        // Check for errors
299
        if ($result !== false) {
300
            return $this->rowCount = $result;
301
        }
302
303
        $this->databaseError($this->getLastError(), $errorLevel, $sql);
304
        return null;
305
    }
306
307
    public function query($sql, $errorLevel = E_USER_ERROR)
308
    {
309
        $this->beforeQuery($sql);
310
311
        // Directly query against connection
312
        $statement = $this->pdoConnection->query($sql);
313
314
        // Generate results
315
        return $this->prepareResults($statement, $errorLevel, $sql);
0 ignored issues
show
Bug introduced by
It seems like $statement can also be of type boolean; however, parameter $statement of SilverStripe\ORM\Connect...ector::prepareResults() does only seem to accept PDOStatement, 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

315
        return $this->prepareResults(/** @scrutinizer ignore-type */ $statement, $errorLevel, $sql);
Loading history...
316
    }
317
318
    /**
319
     * Determines the PDO::PARAM_* type for a given PHP type string
320
     * @param string $phpType Type of object in PHP
321
     * @return integer PDO Parameter constant value
322
     */
323
    public function getPDOParamType($phpType)
324
    {
325
        switch ($phpType) {
326
            case 'boolean':
327
                return PDO::PARAM_BOOL;
328
            case 'NULL':
329
                return PDO::PARAM_NULL;
330
            case 'integer':
331
                return PDO::PARAM_INT;
332
            case 'object': // Allowed if the object or resource has a __toString method
333
            case 'resource':
334
            case 'float': // Not actually returnable from get_type
335
            case 'double':
336
            case 'string':
337
                return PDO::PARAM_STR;
338
            case 'blob':
339
                return PDO::PARAM_LOB;
340
            case 'array':
341
            case 'unknown type':
342
            default:
343
                throw new InvalidArgumentException("Cannot bind parameter as it is an unsupported type ($phpType)");
344
        }
345
    }
346
347
    /**
348
     * Bind all parameters to a PDOStatement
349
     *
350
     * @param PDOStatement $statement
351
     * @param array $parameters
352
     */
353
    public function bindParameters(PDOStatement $statement, $parameters)
354
    {
355
        // Bind all parameters
356
        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...
357
            $value = $parameters[$index];
358
            $phpType = gettype($value);
359
360
            // Allow overriding of parameter type using an associative array
361
            if ($phpType === 'array') {
362
                $phpType = $value['type'];
363
                $value = $value['value'];
364
            }
365
366
            // Check type of parameter
367
            $type = $this->getPDOParamType($phpType);
368
            if ($type === PDO::PARAM_STR) {
369
                $value = strval($value);
370
            }
371
372
            // Bind this value
373
            $statement->bindValue($index+1, $value, $type);
374
        }
375
    }
376
377
    public function preparedQuery($sql, $parameters, $errorLevel = E_USER_ERROR)
378
    {
379
        $this->beforeQuery($sql);
380
381
        // Prepare statement
382
        $statement = $this->getOrPrepareStatement($sql);
383
384
        // Bind and invoke statement safely
385
        if ($statement) {
0 ignored issues
show
introduced by
$statement is of type PDOStatement, thus it always evaluated to true.
Loading history...
386
            $this->bindParameters($statement, $parameters);
387
            $statement->execute($parameters);
388
        }
389
390
        // Generate results
391
        return $this->prepareResults($statement, $errorLevel, $sql);
392
    }
393
394
    /**
395
     * Given a PDOStatement that has just been executed, generate results
396
     * and report any errors
397
     *
398
     * @param PDOStatement $statement
399
     * @param int $errorLevel
400
     * @param string $sql
401
     * @param array $parameters
402
     * @return PDOQuery
403
     */
404
    protected function prepareResults($statement, $errorLevel, $sql, $parameters = array())
405
    {
406
407
        // Record row-count and errors of last statement
408
        if ($this->hasError($statement)) {
409
            $this->lastStatementError = $statement->errorInfo();
410
        } elseif ($statement) {
0 ignored issues
show
introduced by
$statement is of type PDOStatement, thus it always evaluated to true.
Loading history...
411
            // Count and return results
412
            $this->rowCount = $statement->rowCount();
413
            return new PDOQuery($statement, $this);
414
        }
415
416
        // Ensure statement is closed
417
        if ($statement) {
0 ignored issues
show
introduced by
$statement is of type PDOStatement, thus it always evaluated to true.
Loading history...
418
            $statement->closeCursor();
419
            unset($statement);
420
        }
421
422
        // Report any errors
423
        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...
424
            $parameters = $this->parameterValues($parameters);
425
        }
426
        $this->databaseError($this->getLastError(), $errorLevel, $sql, $parameters);
427
        return null;
428
    }
429
430
    /**
431
     * Determine if a resource has an attached error
432
     *
433
     * @param PDOStatement|PDO $resource the resource to check
434
     * @return boolean Flag indicating true if the resource has an error
435
     */
436
    protected function hasError($resource)
437
    {
438
        // No error if no resource
439
        if (empty($resource)) {
440
            return false;
441
        }
442
443
        // If the error code is empty the statement / connection has not been run yet
444
        $code = $resource->errorCode();
445
        if (empty($code)) {
446
            return false;
447
        }
448
449
        // Skip 'ok' and undefined 'warning' types.
450
        // @see http://docstore.mik.ua/orelly/java-ent/jenut/ch08_06.htm
451
        return $code !== '00000' && $code !== '01000';
452
    }
453
454
    public function getLastError()
455
    {
456
        $error = null;
457
        if ($this->lastStatementError) {
458
            $error = $this->lastStatementError;
459
        } elseif ($this->hasError($this->pdoConnection)) {
460
            $error = $this->pdoConnection->errorInfo();
461
        }
462
        if ($error) {
463
            return sprintf("%s-%s: %s", $error[0], $error[1], $error[2]);
464
        }
465
        return null;
466
    }
467
468
    public function getGeneratedID($table)
469
    {
470
        return $this->pdoConnection->lastInsertId();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->pdoConnection->lastInsertId() returns the type string which is incompatible with the return type mandated by SilverStripe\ORM\Connect...ector::getGeneratedID() of integer.

In the issue above, the returned value is violating the contract defined by the mentioned interface.

Let's take a look at an example:

interface HasName {
    /** @return string */
    public function getName();
}

class Name {
    public $name;
}

class User implements HasName {
    /** @return string|Name */
    public function getName() {
        return new Name('foo'); // This is a violation of the ``HasName`` interface
                                // which only allows a string value to be returned.
    }
}
Loading history...
471
    }
472
473
    public function affectedRows()
474
    {
475
        return $this->rowCount;
476
    }
477
478
    public function selectDatabase($name)
479
    {
480
        $this->exec("USE \"{$name}\"");
481
        $this->databaseName = $name;
482
        return true;
483
    }
484
485
    public function getSelectedDatabase()
486
    {
487
        return $this->databaseName;
488
    }
489
490
    public function unloadDatabase()
491
    {
492
        $this->databaseName = null;
493
    }
494
495
    public function isActive()
496
    {
497
        return $this->databaseName && $this->pdoConnection;
498
    }
499
500
    /**
501
     * @inherit
502
     */
503
    public function transactionStart($transactionMode = false, $sessionCharacteristics = false)
504
    {
505
        $this->inTransaction = true;
506
        return $this->pdoConnection->beginTransaction();
507
    }
508
509
    /**
510
     * @inherit
511
     */
512
    public function transactionEnd()
513
    {
514
        $this->inTransaction = false;
515
        return $this->pdoConnection->commit();
516
    }
517
518
    /**
519
     * @inherit
520
     */
521
    public function transactionRollback()
522
    {
523
        $this->inTransaction = false;
524
        return $this->pdoConnection->rollBack();
525
    }
526
527
    /**
528
     * @inherit
529
     */
530
    public function transactionDepth()
531
    {
532
        return (int)$this->inTransaction;
533
    }
534
}
535