PDOConnector::transactionDepth()   A
last analyzed

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 SilverStripe\Dev\Deprecation;
7
use PDO;
8
use PDOStatement;
9
use InvalidArgumentException;
10
11
/**
12
 * PDO driver database connector
13
 */
14
class PDOConnector extends DBConnector implements TransactionManager
15
{
16
17
    /**
18
     * Should ATTR_EMULATE_PREPARES flag be used to emulate prepared statements?
19
     *
20
     * @config
21
     * @var boolean
22
     */
23
    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...
24
25
    /**
26
     * Should we return everything as a string in order to allow transaction savepoints?
27
     * This preserves the behaviour of <= 4.3, including some bugs.
28
     *
29
     * @config
30
     * @var boolean
31
     */
32
    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...
33
34
    /**
35
     * Default strong SSL cipher to be used
36
     *
37
     * @config
38
     * @var string
39
     */
40
    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...
41
42
    /**
43
     * The PDO connection instance
44
     *
45
     * @var PDO
46
     */
47
    protected $pdoConnection = null;
48
49
    /**
50
     * Name of the currently selected database
51
     *
52
     * @var string
53
     */
54
    protected $databaseName = null;
55
56
    /**
57
     * If available, the row count of the last executed statement
58
     *
59
     * @var int|null
60
     */
61
    protected $rowCount = null;
62
63
    /**
64
     * Error generated by the errorInfo() method of the last PDOStatement
65
     *
66
     * @var array|null
67
     */
68
    protected $lastStatementError = null;
69
70
    /**
71
     * List of prepared statements, cached by SQL string
72
     *
73
     * @var array
74
     */
75
    protected $cachedStatements = array();
76
77
    /**
78
     * Driver
79
     * @var string
80
     */
81
    protected $driver = null;
82
83
    /*
84
     * Is a transaction currently active?
85
     * @var bool
86
     */
87
    protected $inTransaction = false;
88
89
    /**
90
     * Flush all prepared statements
91
     */
92
    public function flushStatements()
93
    {
94
        $this->cachedStatements = array();
95
    }
96
97
    /**
98
     * Retrieve a prepared statement for a given SQL string, or return an already prepared version if
99
     * one exists for the given query
100
     *
101
     * @param string $sql
102
     * @return PDOStatementHandle|false
103
     */
104
    public function getOrPrepareStatement($sql)
105
    {
106
        // Return cached statements
107
        if (isset($this->cachedStatements[$sql])) {
108
            return $this->cachedStatements[$sql];
109
        }
110
111
        // Generate new statement
112
        $statement = $this->pdoConnection->prepare(
113
            $sql,
114
            array(PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY)
115
        );
116
117
        // Wrap in a PDOStatementHandle, to cache column metadata
118
        $statementHandle = ($statement === false) ? false : new PDOStatementHandle($statement);
0 ignored issues
show
Bug introduced by
It seems like $statement can also be of type true; however, parameter $statement of SilverStripe\ORM\Connect...ntHandle::__construct() 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

118
        $statementHandle = ($statement === false) ? false : new PDOStatementHandle(/** @scrutinizer ignore-type */ $statement);
Loading history...
119
120
        // Only cache select statements
121
        if (preg_match('/^(\s*)select\b/i', $sql)) {
122
            $this->cachedStatements[$sql] = $statementHandle;
123
        }
124
        return $statementHandle;
125
    }
126
127
    /**
128
     * Is PDO running in emulated mode
129
     *
130
     * @return boolean
131
     */
132
    public static function is_emulate_prepare()
133
    {
134
        return self::config()->get('emulate_prepare');
135
    }
136
137
    public function connect($parameters, $selectDB = false)
138
    {
139
        Deprecation::notice('4.5', 'Use native database drivers in favour of PDO. '
140
            . 'https://github.com/silverstripe/silverstripe-framework/issues/8598');
141
142
        $this->flushStatements();
143
144
        // Note that we don't select the database here until explicitly
145
        // requested via selectDatabase
146
        $this->driver = $parameters['driver'];
147
148
        // Build DSN string
149
        $dsn = array();
150
151
        // Typically this is false, but some drivers will request this
152
        if ($selectDB) {
153
            // Specify complete file path immediately following driver (SQLLite3)
154
            if (!empty($parameters['filepath'])) {
155
                $dsn[] = $parameters['filepath'];
156
            } elseif (!empty($parameters['database'])) {
157
                // Some databases require a selected database at connection (SQLite3, Azure)
158
                if ($parameters['driver'] === 'sqlsrv') {
159
                    $dsn[] = "Database={$parameters['database']}";
160
                } else {
161
                    $dsn[] = "dbname={$parameters['database']}";
162
                }
163
            }
164
        }
165
166
        // Syntax for sql server is slightly different
167
        if ($parameters['driver'] === 'sqlsrv') {
168
            $server = $parameters['server'];
169
            if (!empty($parameters['port'])) {
170
                $server .= ",{$parameters['port']}";
171
            }
172
            $dsn[] = "Server=$server";
173
        } elseif ($parameters['driver'] === 'dblib') {
174
            $server = $parameters['server'];
175
            if (!empty($parameters['port'])) {
176
                $server .= ":{$parameters['port']}";
177
            }
178
            $dsn[] = "host={$server}";
179
        } else {
180
            if (!empty($parameters['server'])) {
181
                // Use Server instead of host for sqlsrv
182
                $dsn[] = "host={$parameters['server']}";
183
            }
184
185
            if (!empty($parameters['port'])) {
186
                $dsn[] = "port={$parameters['port']}";
187
            }
188
        }
189
190
        // Connection charset and collation
191
        $connCharset = Config::inst()->get(MySQLDatabase::class, 'connection_charset');
192
        $connCollation = Config::inst()->get(MySQLDatabase::class, 'connection_collation');
193
194
        // Set charset if given and not null. Can explicitly set to empty string to omit
195
        if (!in_array($parameters['driver'], ['sqlsrv', 'pgsql'])) {
196
            $charset = isset($parameters['charset'])
197
                    ? $parameters['charset']
198
                    : $connCharset;
199
            if (!empty($charset)) {
200
                $dsn[] = "charset=$charset";
201
            }
202
        }
203
204
        // Connection commands to be run on every re-connection
205
        if (!isset($charset)) {
206
            $charset = $connCharset;
207
        }
208
209
        $options = [];
210
        if ($parameters['driver'] === 'mysql') {
211
            $options[PDO::MYSQL_ATTR_INIT_COMMAND] = 'SET NAMES ' . $charset . ' COLLATE ' . $connCollation;
212
        }
213
214
        // Set SSL options if they are defined
215
        if (array_key_exists('ssl_key', $parameters) &&
216
            array_key_exists('ssl_cert', $parameters)
217
        ) {
218
            $options[PDO::MYSQL_ATTR_SSL_KEY] = $parameters['ssl_key'];
219
            $options[PDO::MYSQL_ATTR_SSL_CERT] = $parameters['ssl_cert'];
220
            if (array_key_exists('ssl_ca', $parameters)) {
221
                $options[PDO::MYSQL_ATTR_SSL_CA] = $parameters['ssl_ca'];
222
            }
223
            // use default cipher if not provided
224
            $options[PDO::MYSQL_ATTR_SSL_CIPHER] =
225
                array_key_exists('ssl_cipher', $parameters) ?
226
                $parameters['ssl_cipher'] :
227
                self::config()->get('ssl_cipher_default');
228
        }
229
230
        if (static::config()->get('legacy_types')) {
231
            $options[PDO::ATTR_STRINGIFY_FETCHES] = true;
232
            $options[PDO::ATTR_EMULATE_PREPARES] = true;
233
        } else {
234
            // Set emulate prepares (unless null / default)
235
            $isEmulatePrepares = self::is_emulate_prepare();
236
            if (isset($isEmulatePrepares)) {
237
                $options[PDO::ATTR_EMULATE_PREPARES] = (bool)$isEmulatePrepares;
238
            }
239
240
            // Disable stringified fetches
241
            $options[PDO::ATTR_STRINGIFY_FETCHES] = false;
242
        }
243
244
        // May throw a PDOException if fails
245
        $this->pdoConnection = new PDO(
246
            $this->driver . ':' . implode(';', $dsn),
247
            empty($parameters['username']) ? '' : $parameters['username'],
248
            empty($parameters['password']) ? '' : $parameters['password'],
249
            $options
250
        );
251
252
        // Show selected DB if requested
253
        if ($this->pdoConnection && $selectDB && !empty($parameters['database'])) {
254
            $this->databaseName = $parameters['database'];
255
        }
256
    }
257
258
259
    /**
260
     * Return the driver for this connector
261
     * E.g. 'mysql', 'sqlsrv', 'pgsql'
262
     *
263
     * @return string
264
     */
265
    public function getDriver()
266
    {
267
        return $this->driver;
268
    }
269
270
    public function getVersion()
271
    {
272
        return $this->pdoConnection->getAttribute(PDO::ATTR_SERVER_VERSION);
273
    }
274
275
    public function escapeString($value)
276
    {
277
        $value = $this->quoteString($value);
278
279
        // Since the PDO library quotes the value, we should remove this to maintain
280
        // consistency with MySQLDatabase::escapeString
281
        if (preg_match('/^\'(?<value>.*)\'$/', $value, $matches)) {
282
            $value = $matches['value'];
283
        }
284
        return $value;
285
    }
286
287
    public function quoteString($value)
288
    {
289
        return $this->pdoConnection->quote($value);
290
    }
291
292
    /**
293
     * Invoked before any query is executed
294
     *
295
     * @param string $sql
296
     */
297
    protected function beforeQuery($sql)
298
    {
299
        // Reset state
300
        $this->rowCount = 0;
301
        $this->lastStatementError = null;
302
303
        // Flush if necessary
304
        if ($this->isQueryDDL($sql)) {
305
            $this->flushStatements();
306
        }
307
    }
308
309
    /**
310
     * Executes a query that doesn't return a resultset
311
     *
312
     * @param string $sql The SQL query to execute
313
     * @param integer $errorLevel For errors to this query, raise PHP errors
314
     * using this error level.
315
     * @return int
316
     */
317
    public function exec($sql, $errorLevel = E_USER_ERROR)
318
    {
319
        $this->beforeQuery($sql);
320
321
        // Directly exec this query
322
        $result = $this->pdoConnection->exec($sql);
323
324
        // Check for errors
325
        if ($result !== false) {
326
            return $this->rowCount = $result;
327
        }
328
329
        $this->databaseError($this->getLastError(), $errorLevel, $sql);
330
        return null;
331
    }
332
333
    public function query($sql, $errorLevel = E_USER_ERROR)
334
    {
335
        $this->beforeQuery($sql);
336
337
        // Directly query against connection
338
        $statement = $this->pdoConnection->query($sql);
339
340
        // Generate results
341
        if ($statement === false) {
342
            $this->databaseError($this->getLastError(), $errorLevel, $sql);
343
        } else {
344
            return $this->prepareResults(new PDOStatementHandle($statement), $errorLevel, $sql);
345
        }
346
    }
347
348
    /**
349
     * Determines the PDO::PARAM_* type for a given PHP type string
350
     * @param string $phpType Type of object in PHP
351
     * @return integer PDO Parameter constant value
352
     */
353
    public function getPDOParamType($phpType)
354
    {
355
        switch ($phpType) {
356
            case 'boolean':
357
                return PDO::PARAM_BOOL;
358
            case 'NULL':
359
                return PDO::PARAM_NULL;
360
            case 'integer':
361
                return PDO::PARAM_INT;
362
            case 'object': // Allowed if the object or resource has a __toString method
363
            case 'resource':
364
            case 'float': // Not actually returnable from get_type
365
            case 'double':
366
            case 'string':
367
                return PDO::PARAM_STR;
368
            case 'blob':
369
                return PDO::PARAM_LOB;
370
            case 'array':
371
            case 'unknown type':
372
            default:
373
                throw new InvalidArgumentException("Cannot bind parameter as it is an unsupported type ($phpType)");
374
        }
375
    }
376
377
    /**
378
     * Bind all parameters to a PDOStatement
379
     *
380
     * @param PDOStatement $statement
381
     * @param array $parameters
382
     */
383
    public function bindParameters(PDOStatement $statement, $parameters)
384
    {
385
        // Bind all parameters
386
        $parameterCount = count($parameters);
387
        for ($index = 0; $index < $parameterCount; $index++) {
388
            $value = $parameters[$index];
389
            $phpType = gettype($value);
390
391
            // Allow overriding of parameter type using an associative array
392
            if ($phpType === 'array') {
393
                $phpType = $value['type'];
394
                $value = $value['value'];
395
            }
396
397
            // Check type of parameter
398
            $type = $this->getPDOParamType($phpType);
399
            if ($type === PDO::PARAM_STR) {
400
                $value = (string) $value;
401
            }
402
403
            // Bind this value
404
            $statement->bindValue($index+1, $value, $type);
405
        }
406
    }
407
408
    public function preparedQuery($sql, $parameters, $errorLevel = E_USER_ERROR)
409
    {
410
        $this->beforeQuery($sql);
411
412
        // Fetch cached statement, or create it
413
        $statementHandle = $this->getOrPrepareStatement($sql);
414
415
        // Error handling
416
        if ($statementHandle === false) {
417
            $this->databaseError($this->getLastError(), $errorLevel, $sql, $this->parameterValues($parameters));
418
            return null;
419
        }
420
421
        // Bind parameters
422
        $this->bindParameters($statementHandle->getPDOStatement(), $parameters);
423
        $statementHandle->execute($parameters);
424
425
        // Generate results
426
        return $this->prepareResults($statementHandle, $errorLevel, $sql);
427
    }
428
429
    /**
430
     * Given a PDOStatement that has just been executed, generate results
431
     * and report any errors
432
     *
433
     * @param PDOStatementHandle $statement
434
     * @param int $errorLevel
435
     * @param string $sql
436
     * @param array $parameters
437
     * @return PDOQuery
438
     */
439
    protected function prepareResults(PDOStatementHandle $statement, $errorLevel, $sql, $parameters = array())
440
    {
441
442
        // Catch error
443
        if ($this->hasError($statement)) {
0 ignored issues
show
Bug introduced by
$statement of type SilverStripe\ORM\Connect\PDOStatementHandle is incompatible with the type PDO|PDOStatement expected by parameter $resource of SilverStripe\ORM\Connect\PDOConnector::hasError(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

443
        if ($this->hasError(/** @scrutinizer ignore-type */ $statement)) {
Loading history...
444
            $this->lastStatementError = $statement->errorInfo();
445
            $statement->closeCursor();
446
447
            $this->databaseError($this->getLastError(), $errorLevel, $sql, $this->parameterValues($parameters));
448
449
            return null;
450
        }
451
452
        // Count and return results
453
        $this->rowCount = $statement->rowCount();
454
        return new PDOQuery($statement);
455
    }
456
457
    /**
458
     * Determine if a resource has an attached error
459
     *
460
     * @param PDOStatement|PDO $resource the resource to check
461
     * @return boolean Flag indicating true if the resource has an error
462
     */
463
    protected function hasError($resource)
464
    {
465
        // No error if no resource
466
        if (empty($resource)) {
467
            return false;
468
        }
469
470
        // If the error code is empty the statement / connection has not been run yet
471
        $code = $resource->errorCode();
472
        if (empty($code)) {
473
            return false;
474
        }
475
476
        // Skip 'ok' and undefined 'warning' types.
477
        // @see http://docstore.mik.ua/orelly/java-ent/jenut/ch08_06.htm
478
        return $code !== '00000' && $code !== '01000';
479
    }
480
481
    public function getLastError()
482
    {
483
        $error = null;
484
        if ($this->lastStatementError) {
485
            $error = $this->lastStatementError;
486
        } elseif ($this->hasError($this->pdoConnection)) {
487
            $error = $this->pdoConnection->errorInfo();
488
        }
489
        if ($error) {
490
            return sprintf("%s-%s: %s", $error[0], $error[1], $error[2]);
491
        }
492
        return null;
493
    }
494
495
    public function getGeneratedID($table)
496
    {
497
        return (int) $this->pdoConnection->lastInsertId();
498
    }
499
500
    public function affectedRows()
501
    {
502
        return $this->rowCount;
503
    }
504
505
    public function selectDatabase($name)
506
    {
507
        $this->exec("USE \"{$name}\"");
508
        $this->databaseName = $name;
509
        return true;
510
    }
511
512
    public function getSelectedDatabase()
513
    {
514
        return $this->databaseName;
515
    }
516
517
    public function unloadDatabase()
518
    {
519
        $this->databaseName = null;
520
    }
521
522
    public function isActive()
523
    {
524
        return $this->databaseName && $this->pdoConnection;
525
    }
526
527
    public function transactionStart($transactionMode = false, $sessionCharacteristics = false)
528
    {
529
        $this->inTransaction = true;
530
531
        if ($transactionMode) {
532
            $this->query("SET TRANSACTION $transactionMode");
533
        }
534
535
        if ($this->pdoConnection->beginTransaction()) {
536
            if ($sessionCharacteristics) {
537
                $this->query("SET SESSION CHARACTERISTICS AS TRANSACTION $sessionCharacteristics");
538
            }
539
            return true;
540
        }
541
        return false;
542
    }
543
544
    public function transactionEnd()
545
    {
546
        $this->inTransaction = false;
547
        return $this->pdoConnection->commit();
548
    }
549
550
    public function transactionRollback($savepoint = null)
551
    {
552
        if ($savepoint) {
553
            if ($this->supportsSavepoints()) {
554
                $this->exec("ROLLBACK TO SAVEPOINT $savepoint");
555
            } else {
556
                throw new DatabaseException("Savepoints not supported on this PDO connection");
557
            }
558
        }
559
560
        $this->inTransaction = false;
561
        return $this->pdoConnection->rollBack();
562
    }
563
564
    public function transactionDepth()
565
    {
566
        return (int)$this->inTransaction;
567
    }
568
569
    public function transactionSavepoint($savepoint = null)
570
    {
571
        if ($this->supportsSavepoints()) {
572
            $this->exec("SAVEPOINT $savepoint");
573
        } else {
574
            throw new DatabaseException("Savepoints not supported on this PDO connection");
575
        }
576
    }
577
578
    public function supportsSavepoints()
579
    {
580
        return static::config()->get('legacy_types');
581
    }
582
}
583