Passed
Push — int-types ( e9891a...c34c73 )
by Sam
06:45
created

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

332
        return $this->prepareResults(/** @scrutinizer ignore-type */ $statement, $errorLevel, $sql);
Loading history...
333
    }
334
335
    /**
336
     * Determines the PDO::PARAM_* type for a given PHP type string
337
     * @param string $phpType Type of object in PHP
338
     * @return integer PDO Parameter constant value
339
     */
340
    public function getPDOParamType($phpType)
341
    {
342
        switch ($phpType) {
343
            case 'boolean':
344
                return PDO::PARAM_BOOL;
345
            case 'NULL':
346
                return PDO::PARAM_NULL;
347
            case 'integer':
348
                return PDO::PARAM_INT;
349
            case 'object': // Allowed if the object or resource has a __toString method
350
            case 'resource':
351
            case 'float': // Not actually returnable from get_type
352
            case 'double':
353
            case 'string':
354
                return PDO::PARAM_STR;
355
            case 'blob':
356
                return PDO::PARAM_LOB;
357
            case 'array':
358
            case 'unknown type':
359
            default:
360
                throw new InvalidArgumentException("Cannot bind parameter as it is an unsupported type ($phpType)");
361
        }
362
    }
363
364
    /**
365
     * Bind all parameters to a PDOStatement
366
     *
367
     * @param PDOStatement $statement
368
     * @param array $parameters
369
     */
370
    public function bindParameters(PDOStatement $statement, $parameters)
371
    {
372
        // Bind all parameters
373
        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...
374
            $value = $parameters[$index];
375
            $phpType = gettype($value);
376
377
            // Allow overriding of parameter type using an associative array
378
            if ($phpType === 'array') {
379
                $phpType = $value['type'];
380
                $value = $value['value'];
381
            }
382
383
            // Check type of parameter
384
            $type = $this->getPDOParamType($phpType);
385
            if ($type === PDO::PARAM_STR) {
386
                $value = strval($value);
387
            }
388
389
            // Bind this value
390
            $statement->bindValue($index+1, $value, $type);
391
        }
392
    }
393
394
    public function preparedQuery($sql, $parameters, $errorLevel = E_USER_ERROR)
395
    {
396
        $this->beforeQuery($sql);
397
398
        // Prepare statement
399
        $statement = $this->getOrPrepareStatement($sql);
400
401
        // Bind and invoke statement safely
402
        if ($statement) {
0 ignored issues
show
introduced by
$statement is of type PDOStatement, thus it always evaluated to true.
Loading history...
403
            $this->bindParameters($statement, $parameters);
404
            $statement->execute($parameters);
405
        }
406
407
        // Generate results
408
        return $this->prepareResults($statement, $errorLevel, $sql);
409
    }
410
411
    /**
412
     * Given a PDOStatement that has just been executed, generate results
413
     * and report any errors
414
     *
415
     * @param PDOStatement $statement
416
     * @param int $errorLevel
417
     * @param string $sql
418
     * @param array $parameters
419
     * @return PDOQuery
420
     */
421
    protected function prepareResults($statement, $errorLevel, $sql, $parameters = array())
422
    {
423
424
        // Record row-count and errors of last statement
425
        if ($this->hasError($statement)) {
426
            $this->lastStatementError = $statement->errorInfo();
427
        } elseif ($statement) {
0 ignored issues
show
introduced by
$statement is of type PDOStatement, thus it always evaluated to true.
Loading history...
428
            // Count and return results
429
            $this->rowCount = $statement->rowCount();
430
            return new PDOQuery($statement, $this);
431
        }
432
433
        // Ensure statement is closed
434
        if ($statement) {
0 ignored issues
show
introduced by
$statement is of type PDOStatement, thus it always evaluated to true.
Loading history...
435
            $statement->closeCursor();
436
            unset($statement);
437
        }
438
439
        // Report any errors
440
        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...
441
            $parameters = $this->parameterValues($parameters);
442
        }
443
        $this->databaseError($this->getLastError(), $errorLevel, $sql, $parameters);
444
        return null;
445
    }
446
447
    /**
448
     * Determine if a resource has an attached error
449
     *
450
     * @param PDOStatement|PDO $resource the resource to check
451
     * @return boolean Flag indicating true if the resource has an error
452
     */
453
    protected function hasError($resource)
454
    {
455
        // No error if no resource
456
        if (empty($resource)) {
457
            return false;
458
        }
459
460
        // If the error code is empty the statement / connection has not been run yet
461
        $code = $resource->errorCode();
462
        if (empty($code)) {
463
            return false;
464
        }
465
466
        // Skip 'ok' and undefined 'warning' types.
467
        // @see http://docstore.mik.ua/orelly/java-ent/jenut/ch08_06.htm
468
        return $code !== '00000' && $code !== '01000';
469
    }
470
471
    public function getLastError()
472
    {
473
        $error = null;
474
        if ($this->lastStatementError) {
475
            $error = $this->lastStatementError;
476
        } elseif ($this->hasError($this->pdoConnection)) {
477
            $error = $this->pdoConnection->errorInfo();
478
        }
479
        if ($error) {
480
            return sprintf("%s-%s: %s", $error[0], $error[1], $error[2]);
481
        }
482
        return null;
483
    }
484
485
    public function getGeneratedID($table)
486
    {
487
        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...
488
    }
489
490
    public function affectedRows()
491
    {
492
        return $this->rowCount;
493
    }
494
495
    public function selectDatabase($name)
496
    {
497
        $this->exec("USE \"{$name}\"");
498
        $this->databaseName = $name;
499
        return true;
500
    }
501
502
    public function getSelectedDatabase()
503
    {
504
        return $this->databaseName;
505
    }
506
507
    public function unloadDatabase()
508
    {
509
        $this->databaseName = null;
510
    }
511
512
    public function isActive()
513
    {
514
        return $this->databaseName && $this->pdoConnection;
515
    }
516
517
    /**
518
     * @inherit
519
     */
520
    public function transactionStart($transactionMode = false, $sessionCharacteristics = false)
521
    {
522
        $this->inTransaction = true;
523
        if ($this->pdoConnection->beginTransaction()) {
524
            if ($transactionMode) {
525
                $this->query("SET TRANSACTION $transactionMode");
526
            }
527
            if ($sessionCharacteristics) {
528
                $this->query("SET SESSION CHARACTERISTICS AS TRANSACTION $sessionCharacteristics");
529
            }
530
            return true;
531
        }
532
        return false;
533
    }
534
535
    /**
536
     * @inherit
537
     */
538
    public function transactionEnd()
539
    {
540
        $this->inTransaction = false;
541
        return $this->pdoConnection->commit();
542
    }
543
544
    /**
545
     * @inherit
546
     */
547
    public function transactionRollback($savepoint = null)
548
    {
549
        if ($savepoint) {
550
            if ($this->supportsSavepoints()) {
551
                $this->exec("ROLLBACK TO SAVEPOINT $savepoint");
552
            } else {
553
                throw new DatabaseException("Savepoints not supported on this PDO connection");
554
            }
555
        }
556
557
        $this->inTransaction = false;
558
        return $this->pdoConnection->rollBack();
559
    }
560
561
    /**
562
     * @inherit
563
     */
564
    public function transactionDepth()
565
    {
566
        return (int)$this->inTransaction;
567
    }
568
569
    /**
570
     * @inherit
571
     */
572
    public function transactionSavepoint($savepoint = null)
573
    {
574
        if ($this->supportsSavepoints()) {
575
            $this->exec("SAVEPOINT $savepoint");
576
        } else {
577
            throw new DatabaseException("Savepoints not supported on this PDO connection");
578
        }
579
    }
580
581
    /**
582
     * @inherit
583
     */
584
    public function supportsSavepoints()
585
    {
586
        return static::config()->get('legacy_types');
587
    }
588
}
589