Failed Conditions
Pull Request — develop (#3367)
by Benjamin
10:59
created

Connection::prepare()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 6
dl 0
loc 11
ccs 6
cts 6
cp 1
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 1
crap 2
1
<?php
2
3
namespace Doctrine\DBAL;
4
5
use Closure;
6
use Doctrine\Common\EventManager;
7
use Doctrine\DBAL\Cache\ArrayStatement;
8
use Doctrine\DBAL\Cache\CacheException;
9
use Doctrine\DBAL\Cache\QueryCacheProfile;
10
use Doctrine\DBAL\Cache\ResultCacheStatement;
11
use Doctrine\DBAL\Driver\Connection as DriverConnection;
12
use Doctrine\DBAL\Driver\PingableConnection;
13
use Doctrine\DBAL\Driver\ResultStatement;
14
use Doctrine\DBAL\Driver\ServerInfoAwareConnection;
15
use Doctrine\DBAL\Driver\Statement as DriverStatement;
16
use Doctrine\DBAL\Exception\InvalidArgumentException;
17
use Doctrine\DBAL\Platforms\AbstractPlatform;
18
use Doctrine\DBAL\Query\Expression\ExpressionBuilder;
19
use Doctrine\DBAL\Query\QueryBuilder;
20
use Doctrine\DBAL\Schema\AbstractSchemaManager;
21
use Doctrine\DBAL\Types\Type;
22
use Exception;
23
use Throwable;
24
use function array_key_exists;
25
use function array_merge;
26
use function assert;
27
use function implode;
28
use function is_int;
29
use function is_string;
30
use function key;
31
32
/**
33
 * A wrapper around a Doctrine\DBAL\Driver\Connection that adds features like
34
 * events, transaction isolation levels, configuration, emulated transaction nesting,
35
 * lazy connecting and more.
36
 */
37
class Connection implements DriverConnection
38
{
39
    /**
40
     * Constant for transaction isolation level READ UNCOMMITTED.
41
     *
42
     * @deprecated Use TransactionIsolationLevel::READ_UNCOMMITTED.
43
     */
44
    public const TRANSACTION_READ_UNCOMMITTED = TransactionIsolationLevel::READ_UNCOMMITTED;
45
46
    /**
47
     * Constant for transaction isolation level READ COMMITTED.
48
     *
49
     * @deprecated Use TransactionIsolationLevel::READ_COMMITTED.
50
     */
51
    public const TRANSACTION_READ_COMMITTED = TransactionIsolationLevel::READ_COMMITTED;
52
53
    /**
54
     * Constant for transaction isolation level REPEATABLE READ.
55
     *
56
     * @deprecated Use TransactionIsolationLevel::REPEATABLE_READ.
57
     */
58
    public const TRANSACTION_REPEATABLE_READ = TransactionIsolationLevel::REPEATABLE_READ;
59
60
    /**
61
     * Constant for transaction isolation level SERIALIZABLE.
62
     *
63
     * @deprecated Use TransactionIsolationLevel::SERIALIZABLE.
64
     */
65
    public const TRANSACTION_SERIALIZABLE = TransactionIsolationLevel::SERIALIZABLE;
66
67
    /**
68
     * Represents an array of ints to be expanded by Doctrine SQL parsing.
69
     */
70
    public const PARAM_INT_ARRAY = ParameterType::INTEGER + self::ARRAY_PARAM_OFFSET;
71
72
    /**
73
     * Represents an array of strings to be expanded by Doctrine SQL parsing.
74
     */
75
    public const PARAM_STR_ARRAY = ParameterType::STRING + self::ARRAY_PARAM_OFFSET;
76
77
    /**
78
     * Offset by which PARAM_* constants are detected as arrays of the param type.
79
     */
80
    public const ARRAY_PARAM_OFFSET = 100;
81
82
    /**
83
     * The wrapped driver connection.
84
     *
85
     * @var \Doctrine\DBAL\Driver\Connection|null
86
     */
87
    protected $_conn;
88
89
    /** @var Configuration */
90
    protected $_config;
91
92
    /** @var EventManager */
93
    protected $_eventManager;
94
95
    /** @var ExpressionBuilder */
96
    protected $_expr;
97
98
    /**
99
     * Whether or not a connection has been established.
100
     *
101
     * @var bool
102
     */
103
    private $isConnected = false;
104
105
    /**
106
     * The current auto-commit mode of this connection.
107
     *
108
     * @var bool
109
     */
110
    private $autoCommit = true;
111
112
    /**
113
     * The transaction nesting level.
114
     *
115
     * @var int
116
     */
117
    private $transactionNestingLevel = 0;
118
119
    /**
120
     * The currently active transaction isolation level.
121
     *
122
     * @var int
123
     */
124
    private $transactionIsolationLevel;
125
126
    /**
127
     * If nested transactions should use savepoints.
128
     *
129
     * @var bool
130
     */
131
    private $nestTransactionsWithSavepoints = false;
132
133
    /**
134
     * The parameters used during creation of the Connection instance.
135
     *
136
     * @var mixed[]
137
     */
138
    private $params = [];
139
140
    /**
141
     * The DatabasePlatform object that provides information about the
142
     * database platform used by the connection.
143
     *
144
     * @var AbstractPlatform
145
     */
146
    private $platform;
147
148
    /**
149
     * The schema manager.
150
     *
151
     * @var AbstractSchemaManager
152
     */
153
    protected $_schemaManager;
154
155
    /**
156
     * The used DBAL driver.
157
     *
158
     * @var Driver
159
     */
160
    protected $_driver;
161
162
    /**
163
     * Flag that indicates whether the current transaction is marked for rollback only.
164
     *
165
     * @var bool
166
     */
167
    private $isRollbackOnly = false;
168
169
    /** @var int */
170
    protected $defaultFetchMode = FetchMode::ASSOCIATIVE;
171
172
    /**
173
     * Initializes a new instance of the Connection class.
174
     *
175
     * @param mixed[]            $params       The connection parameters.
176
     * @param Driver             $driver       The driver to use.
177
     * @param Configuration|null $config       The configuration, optional.
178
     * @param EventManager|null  $eventManager The event manager, optional.
179
     *
180
     * @throws DBALException
181
     */
182 3336
    public function __construct(
183
        array $params,
184
        Driver $driver,
185
        ?Configuration $config = null,
186
        ?EventManager $eventManager = null
187
    ) {
188 3336
        $this->_driver = $driver;
189 3336
        $this->params  = $params;
190
191 3336
        if (isset($params['pdo'])) {
192 288
            $this->_conn       = $params['pdo'];
193 288
            $this->isConnected = true;
194 288
            unset($this->params['pdo']);
195
        }
196
197 3336
        if (isset($params['platform'])) {
198 672
            if (! $params['platform'] instanceof Platforms\AbstractPlatform) {
199 24
                throw DBALException::invalidPlatformType($params['platform']);
200
            }
201
202 648
            $this->platform = $params['platform'];
203 648
            unset($this->params['platform']);
204
        }
205
206
        // Create default config and event manager if none given
207 3336
        if (! $config) {
208 744
            $config = new Configuration();
209
        }
210
211 3336
        if (! $eventManager) {
212 744
            $eventManager = new EventManager();
213
        }
214
215 3336
        $this->_config       = $config;
216 3336
        $this->_eventManager = $eventManager;
217
218 3336
        $this->_expr = new Query\Expression\ExpressionBuilder($this);
219
220 3336
        $this->autoCommit = $config->getAutoCommit();
221 3336
    }
222
223
    /**
224
     * Gets the parameters used during instantiation.
225
     *
226
     * @return mixed[]
227
     */
228 2952
    public function getParams()
229
    {
230 2952
        return $this->params;
231
    }
232
233
    /**
234
     * Gets the name of the database this Connection is connected to.
235
     *
236
     * @return string
237
     */
238 1530
    public function getDatabase()
239
    {
240 1530
        return $this->_driver->getDatabase($this);
241
    }
242
243
    /**
244
     * Gets the hostname of the currently connected database.
245
     *
246
     * @return string|null
247
     */
248 24
    public function getHost()
249
    {
250 24
        return $this->params['host'] ?? null;
251
    }
252
253
    /**
254
     * Gets the port of the currently connected database.
255
     *
256
     * @return mixed
257
     */
258 24
    public function getPort()
259
    {
260 24
        return $this->params['port'] ?? null;
261
    }
262
263
    /**
264
     * Gets the username used by this connection.
265
     *
266
     * @return string|null
267
     */
268 78
    public function getUsername()
269
    {
270 78
        return $this->params['user'] ?? null;
271
    }
272
273
    /**
274
     * Gets the password used by this connection.
275
     *
276
     * @return string|null
277
     */
278 24
    public function getPassword()
279
    {
280 24
        return $this->params['password'] ?? null;
281
    }
282
283
    /**
284
     * Gets the DBAL driver instance.
285
     *
286
     * @return Driver
287
     */
288 1348
    public function getDriver()
289
    {
290 1348
        return $this->_driver;
291
    }
292
293
    /**
294
     * Gets the Configuration used by the Connection.
295
     *
296
     * @return Configuration
297
     */
298 7182
    public function getConfiguration()
299
    {
300 7182
        return $this->_config;
301
    }
302
303
    /**
304
     * Gets the EventManager used by the Connection.
305
     *
306
     * @return EventManager
307
     */
308 336
    public function getEventManager()
309
    {
310 336
        return $this->_eventManager;
311
    }
312
313
    /**
314
     * Gets the DatabasePlatform for the connection.
315
     *
316
     * @return AbstractPlatform
317
     *
318
     * @throws DBALException
319
     */
320 6037
    public function getDatabasePlatform()
321
    {
322 6037
        if ($this->platform === null) {
323 899
            $this->detectDatabasePlatform();
324
        }
325
326 6013
        return $this->platform;
327
    }
328
329
    /**
330
     * Gets the ExpressionBuilder for the connection.
331
     *
332
     * @return ExpressionBuilder
333
     */
334
    public function getExpressionBuilder()
335
    {
336
        return $this->_expr;
337
    }
338
339
    /**
340
     * Establishes the connection with the database.
341
     *
342
     * @return bool TRUE if the connection was successfully established, FALSE if
343
     *              the connection is already open.
344
     */
345 7323
    public function connect()
346
    {
347 7323
        if ($this->isConnected) {
348 6860
            return false;
349
        }
350
351 1186
        $driverOptions = $this->params['driverOptions'] ?? [];
352 1186
        $user          = $this->params['user'] ?? null;
353 1186
        $password      = $this->params['password'] ?? null;
354
355 1186
        $this->_conn       = $this->_driver->connect($this->params, $user, $password, $driverOptions);
356 1108
        $this->isConnected = true;
357
358 1108
        if ($this->autoCommit === false) {
359 72
            $this->beginTransaction();
360
        }
361
362 1108
        if ($this->_eventManager->hasListeners(Events::postConnect)) {
363 47
            $eventArgs = new Event\ConnectionEventArgs($this);
364 47
            $this->_eventManager->dispatchEvent(Events::postConnect, $eventArgs);
365
        }
366
367 1108
        return true;
368
    }
369
370
    /**
371
     * Detects and sets the database platform.
372
     *
373
     * Evaluates custom platform class and version in order to set the correct platform.
374
     *
375
     * @throws DBALException If an invalid platform was specified for this connection.
376
     */
377 899
    private function detectDatabasePlatform()
378
    {
379 899
        $version = $this->getDatabasePlatformVersion();
380
381 825
        if ($version !== null) {
382 592
            assert($this->_driver instanceof VersionAwarePlatformDriver);
383
384 592
            $this->platform = $this->_driver->createDatabasePlatformForVersion($version);
385
        } else {
386 233
            $this->platform = $this->_driver->getDatabasePlatform();
387
        }
388
389 825
        $this->platform->setEventManager($this->_eventManager);
390 825
    }
391
392
    /**
393
     * Returns the version of the related platform if applicable.
394
     *
395
     * Returns null if either the driver is not capable to create version
396
     * specific platform instances, no explicit server version was specified
397
     * or the underlying driver connection cannot determine the platform
398
     * version without having to query it (performance reasons).
399
     *
400
     * @return string|null
401
     *
402
     * @throws Exception
403
     */
404 899
    private function getDatabasePlatformVersion()
405
    {
406
        // Driver does not support version specific platforms.
407 899
        if (! $this->_driver instanceof VersionAwarePlatformDriver) {
408 209
            return null;
409
        }
410
411
        // Explicit platform version requested (supersedes auto-detection).
412 690
        if (isset($this->params['serverVersion'])) {
413
            return $this->params['serverVersion'];
414
        }
415
416
        // If not connected, we need to connect now to determine the platform version.
417 690
        if ($this->_conn === null) {
418
            try {
419 648
                $this->connect();
420 95
            } catch (Throwable $originalException) {
421 95
                if (empty($this->params['dbname'])) {
422
                    throw $originalException;
423
                }
424
425
                // The database to connect to might not yet exist.
426
                // Retry detection without database name connection parameter.
427 95
                $databaseName           = $this->params['dbname'];
428 95
                $this->params['dbname'] = null;
429
430
                try {
431 95
                    $this->connect();
432 74
                } catch (Throwable $fallbackException) {
433
                    // Either the platform does not support database-less connections
434
                    // or something else went wrong.
435
                    // Reset connection parameters and rethrow the original exception.
436 74
                    $this->params['dbname'] = $databaseName;
437
438 74
                    throw $originalException;
439
                }
440
441
                // Reset connection parameters.
442 21
                $this->params['dbname'] = $databaseName;
443 21
                $serverVersion          = $this->getServerVersion();
444
445
                // Close "temporary" connection to allow connecting to the real database again.
446 21
                $this->close();
447
448 21
                return $serverVersion;
449
            }
450
        }
451
452 616
        return $this->getServerVersion();
453
    }
454
455
    /**
456
     * Returns the database server version if the underlying driver supports it.
457
     *
458
     * @return string|null
459
     */
460 616
    private function getServerVersion()
461
    {
462
        // Automatic platform version detection.
463 616
        if ($this->_conn instanceof ServerInfoAwareConnection &&
464 616
            ! $this->_conn->requiresQueryForServerVersion()
465
        ) {
466 592
            return $this->_conn->getServerVersion();
467
        }
468
469
        // Unable to detect platform version.
470 24
        return null;
471
    }
472
473
    /**
474
     * Returns the current auto-commit mode for this connection.
475
     *
476
     * @see    setAutoCommit
477
     *
478
     * @return bool True if auto-commit mode is currently enabled for this connection, false otherwise.
479
     */
480 48
    public function isAutoCommit()
481
    {
482 48
        return $this->autoCommit === true;
483
    }
484
485
    /**
486
     * Sets auto-commit mode for this connection.
487
     *
488
     * If a connection is in auto-commit mode, then all its SQL statements will be executed and committed as individual
489
     * transactions. Otherwise, its SQL statements are grouped into transactions that are terminated by a call to either
490
     * the method commit or the method rollback. By default, new connections are in auto-commit mode.
491
     *
492
     * NOTE: If this method is called during a transaction and the auto-commit mode is changed, the transaction is
493
     * committed. If this method is called and the auto-commit mode is not changed, the call is a no-op.
494
     *
495
     * @see   isAutoCommit
496
     *
497
     * @param bool $autoCommit True to enable auto-commit mode; false to disable it.
498
     */
499 120
    public function setAutoCommit($autoCommit)
500
    {
501 120
        $autoCommit = (bool) $autoCommit;
502
503
        // Mode not changed, no-op.
504 120
        if ($autoCommit === $this->autoCommit) {
505 24
            return;
506
        }
507
508 120
        $this->autoCommit = $autoCommit;
509
510
        // Commit all currently active transactions if any when switching auto-commit mode.
511 120
        if ($this->isConnected !== true || $this->transactionNestingLevel === 0) {
512 96
            return;
513
        }
514
515 24
        $this->commitAll();
516 24
    }
517
518
    /**
519
     * Sets the fetch mode.
520
     *
521
     * @param int $fetchMode
522
     *
523
     * @return void
524
     */
525 24
    public function setFetchMode($fetchMode)
526
    {
527 24
        $this->defaultFetchMode = $fetchMode;
528 24
    }
529
530
    /**
531
     * Prepares and executes an SQL query and returns the first row of the result
532
     * as an associative array.
533
     *
534
     * @param string         $statement The SQL query.
535
     * @param mixed[]        $params    The query parameters.
536
     * @param int[]|string[] $types     The query parameter types.
537
     *
538
     * @return mixed[]|false False is returned if no rows are found.
539
     *
540
     * @throws DBALException
541
     */
542 1520
    public function fetchAssoc($statement, array $params = [], array $types = [])
543
    {
544 1520
        return $this->executeQuery($statement, $params, $types)->fetch(FetchMode::ASSOCIATIVE);
545
    }
546
547
    /**
548
     * Prepares and executes an SQL query and returns the first row of the result
549
     * as a numerically indexed array.
550
     *
551
     * @param string         $statement The SQL query to be executed.
552
     * @param mixed[]        $params    The prepared statement params.
553
     * @param int[]|string[] $types     The query parameter types.
554
     *
555
     * @return mixed[]|false False is returned if no rows are found.
556
     */
557 89
    public function fetchArray($statement, array $params = [], array $types = [])
558
    {
559 89
        return $this->executeQuery($statement, $params, $types)->fetch(FetchMode::NUMERIC);
560
    }
561
562
    /**
563
     * Prepares and executes an SQL query and returns the value of a single column
564
     * of the first row of the result.
565
     *
566
     * @param string         $statement The SQL query to be executed.
567
     * @param mixed[]        $params    The prepared statement params.
568
     * @param int            $column    The 0-indexed column number to retrieve.
569
     * @param int[]|string[] $types     The query parameter types.
570
     *
571
     * @return mixed|false False is returned if no rows are found.
572
     *
573
     * @throws DBALException
574
     */
575 956
    public function fetchColumn($statement, array $params = [], $column = 0, array $types = [])
576
    {
577 956
        return $this->executeQuery($statement, $params, $types)->fetchColumn($column);
578
    }
579
580
    /**
581
     * Whether an actual connection to the database is established.
582
     *
583
     * @return bool
584
     */
585 94
    public function isConnected()
586
    {
587 94
        return $this->isConnected;
588
    }
589
590
    /**
591
     * Checks whether a transaction is currently active.
592
     *
593
     * @return bool TRUE if a transaction is currently active, FALSE otherwise.
594
     */
595 7158
    public function isTransactionActive()
596
    {
597 7158
        return $this->transactionNestingLevel > 0;
598
    }
599
600
    /**
601
     * Gathers conditions for an update or delete call.
602
     *
603
     * @param mixed[] $identifiers Input array of columns to values
604
     *
605
     * @return string[][] a triplet with:
606
     *                    - the first key being the columns
607
     *                    - the second key being the values
608
     *                    - the third key being the conditions
609
     */
610 375
    private function gatherConditions(array $identifiers)
611
    {
612 375
        $columns    = [];
613 375
        $values     = [];
614 375
        $conditions = [];
615
616 375
        foreach ($identifiers as $columnName => $value) {
617 375
            if ($value === null) {
618 96
                $conditions[] = $this->getDatabasePlatform()->getIsNullExpression($columnName);
619 96
                continue;
620
            }
621
622 327
            $columns[]    = $columnName;
623 327
            $values[]     = $value;
624 327
            $conditions[] = $columnName . ' = ?';
625
        }
626
627 375
        return [$columns, $values, $conditions];
628
    }
629
630
    /**
631
     * Executes an SQL DELETE statement on a table.
632
     *
633
     * Table expression and columns are not escaped and are not safe for user-input.
634
     *
635
     * @param string         $tableExpression The expression of the table on which to delete.
636
     * @param mixed[]        $identifier      The deletion criteria. An associative array containing column-value pairs.
637
     * @param int[]|string[] $types           The types of identifiers.
638
     *
639
     * @return int The number of affected rows.
640
     *
641
     * @throws DBALException
642
     * @throws InvalidArgumentException
643
     */
644 164
    public function delete($tableExpression, array $identifier, array $types = [])
645
    {
646 164
        if (empty($identifier)) {
647 24
            throw InvalidArgumentException::fromEmptyCriteria();
648
        }
649
650 140
        [$columns, $values, $conditions] = $this->gatherConditions($identifier);
651
652 140
        return $this->executeUpdate(
653 140
            'DELETE FROM ' . $tableExpression . ' WHERE ' . implode(' AND ', $conditions),
654 140
            $values,
655 140
            is_string(key($types)) ? $this->extractTypeValues($columns, $types) : $types
656
        );
657
    }
658
659
    /**
660
     * Closes the connection.
661
     *
662
     * @return void
663
     */
664 571
    public function close()
665
    {
666 571
        $this->_conn = null;
667
668 571
        $this->isConnected = false;
669 571
    }
670
671
    /**
672
     * Sets the transaction isolation level.
673
     *
674
     * @param int $level The level to set.
675
     *
676
     * @return int
677
     */
678
    public function setTransactionIsolation($level)
679
    {
680
        $this->transactionIsolationLevel = $level;
681
682
        return $this->executeUpdate($this->getDatabasePlatform()->getSetTransactionIsolationSQL($level));
683
    }
684
685
    /**
686
     * Gets the currently active transaction isolation level.
687
     *
688
     * @return int The current transaction isolation level.
689
     */
690
    public function getTransactionIsolation()
691
    {
692
        if ($this->transactionIsolationLevel === null) {
693
            $this->transactionIsolationLevel = $this->getDatabasePlatform()->getDefaultTransactionIsolationLevel();
694
        }
695
696
        return $this->transactionIsolationLevel;
697
    }
698
699
    /**
700
     * Executes an SQL UPDATE statement on a table.
701
     *
702
     * Table expression and columns are not escaped and are not safe for user-input.
703
     *
704
     * @param string         $tableExpression The expression of the table to update quoted or unquoted.
705
     * @param mixed[]        $data            An associative array containing column-value pairs.
706
     * @param mixed[]        $identifier      The update criteria. An associative array containing column-value pairs.
707
     * @param int[]|string[] $types           Types of the merged $data and $identifier arrays in that order.
708
     *
709
     * @return int The number of affected rows.
710
     *
711
     * @throws DBALException
712
     */
713 235
    public function update($tableExpression, array $data, array $identifier, array $types = [])
714
    {
715 235
        $setColumns = [];
716 235
        $setValues  = [];
717 235
        $set        = [];
718
719 235
        foreach ($data as $columnName => $value) {
720 235
            $setColumns[] = $columnName;
721 235
            $setValues[]  = $value;
722 235
            $set[]        = $columnName . ' = ?';
723
        }
724
725 235
        [$conditionColumns, $conditionValues, $conditions] = $this->gatherConditions($identifier);
726 235
        $columns                                           = array_merge($setColumns, $conditionColumns);
727 235
        $values                                            = array_merge($setValues, $conditionValues);
728
729 235
        if (is_string(key($types))) {
730 120
            $types = $this->extractTypeValues($columns, $types);
731
        }
732
733 235
        $sql = 'UPDATE ' . $tableExpression . ' SET ' . implode(', ', $set)
734 235
                . ' WHERE ' . implode(' AND ', $conditions);
735
736 235
        return $this->executeUpdate($sql, $values, $types);
737
    }
738
739
    /**
740
     * Inserts a table row with specified data.
741
     *
742
     * Table expression and columns are not escaped and are not safe for user-input.
743
     *
744
     * @param string         $tableExpression The expression of the table to insert data into, quoted or unquoted.
745
     * @param mixed[]        $data            An associative array containing column-value pairs.
746
     * @param int[]|string[] $types           Types of the inserted data.
747
     *
748
     * @return int The number of affected rows.
749
     *
750
     * @throws DBALException
751
     */
752 2114
    public function insert($tableExpression, array $data, array $types = [])
753
    {
754 2114
        if (empty($data)) {
755 24
            return $this->executeUpdate('INSERT INTO ' . $tableExpression . ' () VALUES ()');
756
        }
757
758 2090
        $columns = [];
759 2090
        $values  = [];
760 2090
        $set     = [];
761
762 2090
        foreach ($data as $columnName => $value) {
763 2090
            $columns[] = $columnName;
764 2090
            $values[]  = $value;
765 2090
            $set[]     = '?';
766
        }
767
768 2090
        return $this->executeUpdate(
769 2090
            'INSERT INTO ' . $tableExpression . ' (' . implode(', ', $columns) . ')' .
770 2090
            ' VALUES (' . implode(', ', $set) . ')',
771 2090
            $values,
772 2090
            is_string(key($types)) ? $this->extractTypeValues($columns, $types) : $types
773
        );
774
    }
775
776
    /**
777
     * Extract ordered type list from an ordered column list and type map.
778
     *
779
     * @param string[]       $columnList
780
     * @param int[]|string[] $types
781
     *
782
     * @return int[]|string[]
783
     */
784 216
    private function extractTypeValues(array $columnList, array $types)
785
    {
786 216
        $typeValues = [];
787
788 216
        foreach ($columnList as $columnIndex => $columnName) {
789 216
            $typeValues[] = $types[$columnName] ?? ParameterType::STRING;
790
        }
791
792 216
        return $typeValues;
793
    }
794
795
    /**
796
     * Quotes a string so it can be safely used as a table or column name, even if
797
     * it is a reserved name.
798
     *
799
     * Delimiting style depends on the underlying database platform that is being used.
800
     *
801
     * NOTE: Just because you CAN use quoted identifiers does not mean
802
     * you SHOULD use them. In general, they end up causing way more
803
     * problems than they solve.
804
     *
805
     * @param string $str The name to be quoted.
806
     *
807
     * @return string The quoted name.
808
     */
809 24
    public function quoteIdentifier($str)
810
    {
811 24
        return $this->getDatabasePlatform()->quoteIdentifier($str);
812
    }
813
814
    /**
815
     * Quotes a given input parameter.
816
     *
817
     * @param string $input The parameter to be quoted.
818
     *
819
     * @return string The quoted parameter.
820
     */
821 213
    public function quote(string $input) : string
822
    {
823 213
        $this->connect();
824
825 213
        return $this->_conn->quote($input);
0 ignored issues
show
Bug introduced by
The method quote() does not exist on null. ( Ignorable by Annotation )

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

825
        return $this->_conn->/** @scrutinizer ignore-call */ quote($input);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
826
    }
827
828
    /**
829
     * Prepares and executes an SQL query and returns the result as an associative array.
830
     *
831
     * @param string         $sql    The SQL query.
832
     * @param mixed[]        $params The query parameters.
833
     * @param int[]|string[] $types  The query parameter types.
834
     *
835
     * @return mixed[]
836
     */
837 2449
    public function fetchAll($sql, array $params = [], $types = [])
838
    {
839 2449
        return $this->executeQuery($sql, $params, $types)->fetchAll();
840
    }
841
842
    /**
843
     * Prepares an SQL statement.
844
     *
845
     * @param string $sql The SQL statement to prepare.
846
     *
847
     * @throws DBALException
848
     */
849 1028
    public function prepare(string $sql) : DriverStatement
850
    {
851
        try {
852 1028
            $stmt = new Statement($sql, $this);
853 24
        } catch (Throwable $ex) {
854 24
            throw DBALException::driverExceptionDuringQuery($this->_driver, $ex, $sql);
855
        }
856
857 1004
        $stmt->setFetchMode($this->defaultFetchMode);
858
859 1004
        return $stmt;
860
    }
861
862
    /**
863
     * Executes an, optionally parametrized, SQL query.
864
     *
865
     * If the query is parametrized, a prepared statement is used.
866
     * If an SQLLogger is configured, the execution is logged.
867
     *
868
     * @param string                 $query  The SQL query to execute.
869
     * @param mixed[]                $params The parameters to bind to the query, if any.
870
     * @param int[]|string[]|Type[]  $types  The types the previous parameters are in.
871
     * @param QueryCacheProfile|null $qcp    The query cache profile, optional.
872
     *
873
     * @return ResultStatement The executed statement.
874
     *
875
     * @throws DBALException
876
     */
877 4946
    public function executeQuery(string $query, array $params = [], $types = [], ?QueryCacheProfile $qcp = null) : ResultStatement
878
    {
879 4946
        if ($qcp !== null) {
880 240
            return $this->executeCacheQuery($query, $params, $types, $qcp);
881
        }
882
883 4946
        $this->connect();
884
885 4946
        $logger = $this->_config->getSQLLogger();
886 4946
        $logger->startQuery($query, $params, $types);
887
888
        try {
889 4946
            if ($params) {
890 942
                [$query, $params, $types] = SQLParserUtils::expandListParameters($query, $params, $types);
0 ignored issues
show
Bug introduced by
It seems like $types can also be of type Doctrine\DBAL\Types\Type[]; however, parameter $types of Doctrine\DBAL\SQLParserU...:expandListParameters() does only seem to accept integer[]|string[], 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

890
                [$query, $params, $types] = SQLParserUtils::expandListParameters($query, $params, /** @scrutinizer ignore-type */ $types);
Loading history...
891
892 942
                $stmt = $this->_conn->prepare($query);
893 942
                if ($types) {
894 384
                    $this->_bindTypedValues($stmt, $params, $types);
895 384
                    $stmt->execute();
896
                } else {
897 942
                    $stmt->execute($params);
898
                }
899
            } else {
900 4878
                $stmt = $this->_conn->query($query);
901
            }
902 155
        } catch (Throwable $ex) {
903 155
            throw DBALException::driverExceptionDuringQuery($this->_driver, $ex, $query, $this->resolveParams($params, $types));
904
        }
905
906 4791
        $stmt->setFetchMode($this->defaultFetchMode);
907
908 4791
        $logger->stopQuery();
909
910 4791
        return $stmt;
911
    }
912
913
    /**
914
     * Executes a caching query.
915
     *
916
     * @param string                $query  The SQL query to execute.
917
     * @param mixed[]               $params The parameters to bind to the query, if any.
918
     * @param int[]|string[]|Type[] $types  The types the previous parameters are in.
919
     * @param QueryCacheProfile     $qcp    The query cache profile.
920
     *
921
     * @throws CacheException
922
     */
923 288
    public function executeCacheQuery($query, $params, $types, QueryCacheProfile $qcp) : ResultStatement
924
    {
925 288
        $resultCache = $qcp->getResultCacheDriver() ?: $this->_config->getResultCacheImpl();
926 288
        if (! $resultCache) {
927
            throw CacheException::noResultDriverConfigured();
928
        }
929
930 288
        [$cacheKey, $realKey] = $qcp->generateCacheKeys($query, $params, $types, $this->getParams());
0 ignored issues
show
Bug introduced by
It seems like $types can also be of type Doctrine\DBAL\Types\Type[]; however, parameter $types of Doctrine\DBAL\Cache\Quer...le::generateCacheKeys() does only seem to accept integer[]|string[], 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

930
        [$cacheKey, $realKey] = $qcp->generateCacheKeys($query, $params, /** @scrutinizer ignore-type */ $types, $this->getParams());
Loading history...
931
932
        // fetch the row pointers entry
933 288
        $data = $resultCache->fetch($cacheKey);
934
935 288
        if ($data !== false) {
936
            // is the real key part of this row pointers map or is the cache only pointing to other cache keys?
937 216
            if (isset($data[$realKey])) {
938 216
                $stmt = new ArrayStatement($data[$realKey]);
939
            } elseif (array_key_exists($realKey, $data)) {
940
                $stmt = new ArrayStatement([]);
941
            }
942
        }
943
944 288
        if (! isset($stmt)) {
945 240
            $stmt = new ResultCacheStatement($this->executeQuery($query, $params, $types), $resultCache, $cacheKey, $realKey, $qcp->getLifetime());
946
        }
947
948 288
        $stmt->setFetchMode($this->defaultFetchMode);
949
950 288
        return $stmt;
951
    }
952
953
    /**
954
     * Executes an, optionally parametrized, SQL query and returns the result,
955
     * applying a given projection/transformation function on each row of the result.
956
     *
957
     * @param string  $query    The SQL query to execute.
958
     * @param mixed[] $params   The parameters, if any.
959
     * @param Closure $function The transformation function that is applied on each row.
960
     *                           The function receives a single parameter, an array, that
961
     *                           represents a row of the result set.
962
     *
963
     * @return mixed[] The projected result of the query.
964
     */
965
    public function project($query, array $params, Closure $function)
966
    {
967
        $result = [];
968
        $stmt   = $this->executeQuery($query, $params);
969
970
        while ($row = $stmt->fetch()) {
971
            $result[] = $function($row);
972
        }
973
974
        $stmt->closeCursor();
975
976
        return $result;
977
    }
978
979
    /**
980
     * {@inheritDoc}
981
     */
982 474
    public function query(string $sql) : ResultStatement
983
    {
984 474
        $this->connect();
985
986 474
        $logger = $this->_config->getSQLLogger();
987 474
        $logger->startQuery($sql);
988
989
        try {
990 474
            $statement = $this->_conn->query($sql);
991 24
        } catch (Throwable $ex) {
992 24
            throw DBALException::driverExceptionDuringQuery($this->_driver, $ex, $sql);
993
        }
994
995 450
        $statement->setFetchMode($this->defaultFetchMode);
996
997 450
        $logger->stopQuery();
998
999 450
        return $statement;
1000
    }
1001
1002
    /**
1003
     * Executes an SQL INSERT/UPDATE/DELETE query with the given parameters
1004
     * and returns the number of affected rows.
1005
     *
1006
     * This method supports PDO binding types as well as DBAL mapping types.
1007
     *
1008
     * @param string                $query  The SQL query.
1009
     * @param mixed[]               $params The query parameters.
1010
     * @param int[]|string[]|Type[] $types  The parameter types.
1011
     *
1012
     * @throws DBALException
1013
     */
1014 4561
    public function executeUpdate(string $query, array $params = [], array $types = []) : int
1015
    {
1016 4561
        $this->connect();
1017
1018 4561
        $logger = $this->_config->getSQLLogger();
1019 4561
        $logger->startQuery($query, $params, $types);
1020
1021
        try {
1022 4561
            if ($params) {
1023 2264
                [$query, $params, $types] = SQLParserUtils::expandListParameters($query, $params, $types);
0 ignored issues
show
Bug introduced by
It seems like $types can also be of type Doctrine\DBAL\Types\Type[]; however, parameter $types of Doctrine\DBAL\SQLParserU...:expandListParameters() does only seem to accept integer[]|string[], 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

1023
                [$query, $params, $types] = SQLParserUtils::expandListParameters($query, $params, /** @scrutinizer ignore-type */ $types);
Loading history...
1024
1025 2264
                $stmt = $this->_conn->prepare($query);
1026 2257
                if ($types) {
1027 365
                    $this->_bindTypedValues($stmt, $params, $types);
1028 365
                    $stmt->execute();
1029
                } else {
1030 1892
                    $stmt->execute($params);
1031
                }
1032 2222
                $result = $stmt->rowCount();
1033
            } else {
1034 4519
                $result = $this->_conn->exec($query);
1035
            }
1036 2920
        } catch (Throwable $ex) {
1037 2920
            throw DBALException::driverExceptionDuringQuery($this->_driver, $ex, $query, $this->resolveParams($params, $types));
1038
        }
1039
1040 4177
        $logger->stopQuery();
1041
1042 4177
        return $result;
1043
    }
1044
1045
    /**
1046
     * {@inheritDoc}
1047
     */
1048 2296
    public function exec(string $statement) : int
1049
    {
1050 2296
        $this->connect();
1051
1052 2292
        $logger = $this->_config->getSQLLogger();
1053 2292
        $logger->startQuery($statement);
1054
1055
        try {
1056 2292
            $result = $this->_conn->exec($statement);
1057 1780
        } catch (Throwable $ex) {
1058 1780
            throw DBALException::driverExceptionDuringQuery($this->_driver, $ex, $statement);
1059
        }
1060
1061 586
        $logger->stopQuery();
1062
1063 586
        return $result;
1064
    }
1065
1066
    /**
1067
     * Returns the current transaction nesting level.
1068
     *
1069
     * @return int The nesting level. A value of 0 means there's no active transaction.
1070
     */
1071 443
    public function getTransactionNestingLevel()
1072
    {
1073 443
        return $this->transactionNestingLevel;
1074
    }
1075
1076
    /**
1077
     * Fetches the SQLSTATE associated with the last database operation.
1078
     *
1079
     * @return string|null The last error code.
1080
     */
1081
    public function errorCode()
1082
    {
1083
        $this->connect();
1084
1085
        return $this->_conn->errorCode();
1086
    }
1087
1088
    /**
1089
     * {@inheritDoc}
1090
     */
1091
    public function errorInfo()
1092
    {
1093
        $this->connect();
1094
1095
        return $this->_conn->errorInfo();
1096
    }
1097
1098
    /**
1099
     * Returns the ID of the last inserted row, or the last value from a sequence object,
1100
     * depending on the underlying driver.
1101
     *
1102
     * Note: This method may not return a meaningful or consistent result across different drivers,
1103
     * because the underlying database may not even support the notion of AUTO_INCREMENT/IDENTITY
1104
     * columns or sequences.
1105
     *
1106
     * @param string|null $seqName Name of the sequence object from which the ID should be returned.
1107
     *
1108
     * @return string A string representation of the last inserted ID.
1109
     */
1110 169
    public function lastInsertId(?string $seqName = null) : string
1111
    {
1112 169
        $this->connect();
1113
1114 169
        return $this->_conn->lastInsertId($seqName);
1115
    }
1116
1117
    /**
1118
     * Executes a function in a transaction.
1119
     *
1120
     * The function gets passed this Connection instance as an (optional) parameter.
1121
     *
1122
     * If an exception occurs during execution of the function or transaction commit,
1123
     * the transaction is rolled back and the exception re-thrown.
1124
     *
1125
     * @param Closure $func The function to execute transactionally.
1126
     *
1127
     * @return mixed The value returned by $func
1128
     *
1129
     * @throws Throwable
1130
     */
1131 96
    public function transactional(Closure $func)
1132
    {
1133 96
        $this->beginTransaction();
1134
        try {
1135 96
            $res = $func($this);
1136 48
            $this->commit();
1137 48
            return $res;
1138 48
        } catch (Throwable $e) {
1139 48
            $this->rollBack();
1140 48
            throw $e;
1141
        }
1142
    }
1143
1144
    /**
1145
     * Sets if nested transactions should use savepoints.
1146
     *
1147
     * @param bool $nestTransactionsWithSavepoints
1148
     *
1149
     * @return void
1150
     *
1151
     * @throws ConnectionException
1152
     */
1153 47
    public function setNestTransactionsWithSavepoints($nestTransactionsWithSavepoints)
1154
    {
1155 47
        if ($this->transactionNestingLevel > 0) {
1156 46
            throw ConnectionException::mayNotAlterNestedTransactionWithSavepointsInTransaction();
1157
        }
1158
1159 24
        if (! $this->getDatabasePlatform()->supportsSavepoints()) {
1160 1
            throw ConnectionException::savepointsNotSupported();
1161
        }
1162
1163 23
        $this->nestTransactionsWithSavepoints = (bool) $nestTransactionsWithSavepoints;
1164 23
    }
1165
1166
    /**
1167
     * Gets if nested transactions should use savepoints.
1168
     *
1169
     * @return bool
1170
     */
1171 23
    public function getNestTransactionsWithSavepoints()
1172
    {
1173 23
        return $this->nestTransactionsWithSavepoints;
1174
    }
1175
1176
    /**
1177
     * Returns the savepoint name to use for nested transactions are false if they are not supported
1178
     * "savepointFormat" parameter is not set
1179
     *
1180
     * @return mixed A string with the savepoint name or false.
1181
     */
1182 23
    protected function _getNestedTransactionSavePointName()
1183
    {
1184 23
        return 'DOCTRINE2_SAVEPOINT_' . $this->transactionNestingLevel;
1185
    }
1186
1187
    /**
1188
     * Starts a transaction by suspending auto-commit mode.
1189
     */
1190 488
    public function beginTransaction() : void
1191
    {
1192 488
        $this->connect();
1193
1194 488
        ++$this->transactionNestingLevel;
1195
1196 488
        $logger = $this->_config->getSQLLogger();
1197
1198 488
        if ($this->transactionNestingLevel === 1) {
1199 488
            $logger->startQuery('"START TRANSACTION"');
1200 488
            $this->_conn->beginTransaction();
1201 488
            $logger->stopQuery();
1202 71
        } elseif ($this->nestTransactionsWithSavepoints) {
1203 23
            $logger->startQuery('"SAVEPOINT"');
1204 23
            $this->createSavepoint($this->_getNestedTransactionSavePointName());
1205 23
            $logger->stopQuery();
1206
        }
1207 488
    }
1208
1209
    /**
1210
     * Commits the current transaction.
1211
     *
1212
     * @throws ConnectionException If the commit failed due to no active transaction or
1213
     *                                            because the transaction was marked for rollback only.
1214
     */
1215 274
    public function commit() : void
1216
    {
1217 274
        if ($this->transactionNestingLevel === 0) {
1218 24
            throw ConnectionException::noActiveTransaction();
1219
        }
1220 250
        if ($this->isRollbackOnly) {
1221 48
            throw ConnectionException::commitFailedRollbackOnly();
1222
        }
1223
1224 202
        $this->connect();
1225
1226 202
        $logger = $this->_config->getSQLLogger();
1227
1228 202
        if ($this->transactionNestingLevel === 1) {
1229 202
            $logger->startQuery('"COMMIT"');
1230 202
            $this->_conn->commit();
1231 202
            $logger->stopQuery();
1232 47
        } elseif ($this->nestTransactionsWithSavepoints) {
1233 23
            $logger->startQuery('"RELEASE SAVEPOINT"');
1234 23
            $this->releaseSavepoint($this->_getNestedTransactionSavePointName());
1235 23
            $logger->stopQuery();
1236
        }
1237
1238 202
        --$this->transactionNestingLevel;
1239
1240 202
        if ($this->autoCommit !== false || $this->transactionNestingLevel !== 0) {
1241 178
            return;
1242
        }
1243
1244 48
        $this->beginTransaction();
1245 48
    }
1246
1247
    /**
1248
     * Commits all current nesting transactions.
1249
     */
1250 24
    private function commitAll()
1251
    {
1252 24
        while ($this->transactionNestingLevel !== 0) {
1253 24
            if ($this->autoCommit === false && $this->transactionNestingLevel === 1) {
1254
                // When in no auto-commit mode, the last nesting commit immediately starts a new transaction.
1255
                // Therefore we need to do the final commit here and then leave to avoid an infinite loop.
1256 24
                $this->commit();
1257
1258 24
                return;
1259
            }
1260
1261 24
            $this->commit();
1262
        }
1263 24
    }
1264
1265
    /**
1266
     * Cancels any database changes done during the current transaction.
1267
     *
1268
     * @throws ConnectionException If the rollback operation failed.
1269
     */
1270 308
    public function rollBack() : void
1271
    {
1272 308
        if ($this->transactionNestingLevel === 0) {
1273 24
            throw ConnectionException::noActiveTransaction();
1274
        }
1275
1276 284
        $this->connect();
1277
1278 284
        $logger = $this->_config->getSQLLogger();
1279
1280 284
        if ($this->transactionNestingLevel === 1) {
1281 261
            $logger->startQuery('"ROLLBACK"');
1282 261
            $this->transactionNestingLevel = 0;
1283 261
            $this->_conn->rollBack();
1284 261
            $this->isRollbackOnly = false;
1285 261
            $logger->stopQuery();
1286
1287 261
            if ($this->autoCommit === false) {
1288 261
                $this->beginTransaction();
1289
            }
1290 47
        } elseif ($this->nestTransactionsWithSavepoints) {
1291 23
            $logger->startQuery('"ROLLBACK TO SAVEPOINT"');
1292 23
            $this->rollbackSavepoint($this->_getNestedTransactionSavePointName());
1293 23
            --$this->transactionNestingLevel;
1294 23
            $logger->stopQuery();
1295
        } else {
1296 24
            $this->isRollbackOnly = true;
1297 24
            --$this->transactionNestingLevel;
1298
        }
1299 284
    }
1300
1301
    /**
1302
     * Creates a new savepoint.
1303
     *
1304
     * @param string $savepoint The name of the savepoint to create.
1305
     *
1306
     * @return void
1307
     *
1308
     * @throws ConnectionException
1309
     */
1310 24
    public function createSavepoint($savepoint)
1311
    {
1312 24
        if (! $this->getDatabasePlatform()->supportsSavepoints()) {
1313 1
            throw ConnectionException::savepointsNotSupported();
1314
        }
1315
1316 23
        $this->_conn->exec($this->platform->createSavePoint($savepoint));
1317 23
    }
1318
1319
    /**
1320
     * Releases the given savepoint.
1321
     *
1322
     * @param string $savepoint The name of the savepoint to release.
1323
     *
1324
     * @return void
1325
     *
1326
     * @throws ConnectionException
1327
     */
1328 24
    public function releaseSavepoint($savepoint)
1329
    {
1330 24
        if (! $this->getDatabasePlatform()->supportsSavepoints()) {
1331 1
            throw ConnectionException::savepointsNotSupported();
1332
        }
1333
1334 23
        if (! $this->platform->supportsReleaseSavepoints()) {
1335 3
            return;
1336
        }
1337
1338 20
        $this->_conn->exec($this->platform->releaseSavePoint($savepoint));
1339 20
    }
1340
1341
    /**
1342
     * Rolls back to the given savepoint.
1343
     *
1344
     * @param string $savepoint The name of the savepoint to rollback to.
1345
     *
1346
     * @return void
1347
     *
1348
     * @throws ConnectionException
1349
     */
1350 24
    public function rollbackSavepoint($savepoint)
1351
    {
1352 24
        if (! $this->getDatabasePlatform()->supportsSavepoints()) {
1353 1
            throw ConnectionException::savepointsNotSupported();
1354
        }
1355
1356 23
        $this->_conn->exec($this->platform->rollbackSavePoint($savepoint));
1357 23
    }
1358
1359
    /**
1360
     * Gets the wrapped driver connection.
1361
     *
1362
     * @return \Doctrine\DBAL\Driver\Connection
1363
     */
1364 1174
    public function getWrappedConnection()
1365
    {
1366 1174
        $this->connect();
1367
1368 1174
        return $this->_conn;
1369
    }
1370
1371
    /**
1372
     * Gets the SchemaManager that can be used to inspect or change the
1373
     * database schema through the connection.
1374
     *
1375
     * @return AbstractSchemaManager
1376
     */
1377 4185
    public function getSchemaManager()
1378
    {
1379 4185
        if (! $this->_schemaManager) {
1380 278
            $this->_schemaManager = $this->_driver->getSchemaManager($this);
1381
        }
1382
1383 4185
        return $this->_schemaManager;
1384
    }
1385
1386
    /**
1387
     * Marks the current transaction so that the only possible
1388
     * outcome for the transaction to be rolled back.
1389
     *
1390
     * @return void
1391
     *
1392
     * @throws ConnectionException If no transaction is active.
1393
     */
1394 48
    public function setRollbackOnly()
1395
    {
1396 48
        if ($this->transactionNestingLevel === 0) {
1397 24
            throw ConnectionException::noActiveTransaction();
1398
        }
1399 24
        $this->isRollbackOnly = true;
1400 24
    }
1401
1402
    /**
1403
     * Checks whether the current transaction is marked for rollback only.
1404
     *
1405
     * @return bool
1406
     *
1407
     * @throws ConnectionException If no transaction is active.
1408
     */
1409 71
    public function isRollbackOnly()
1410
    {
1411 71
        if ($this->transactionNestingLevel === 0) {
1412 24
            throw ConnectionException::noActiveTransaction();
1413
        }
1414
1415 47
        return $this->isRollbackOnly;
1416
    }
1417
1418
    /**
1419
     * Converts a given value to its database representation according to the conversion
1420
     * rules of a specific DBAL mapping type.
1421
     *
1422
     * @param mixed  $value The value to convert.
1423
     * @param string $type  The name of the DBAL mapping type.
1424
     *
1425
     * @return mixed The converted value.
1426
     */
1427
    public function convertToDatabaseValue($value, $type)
1428
    {
1429
        return Type::getType($type)->convertToDatabaseValue($value, $this->getDatabasePlatform());
1430
    }
1431
1432
    /**
1433
     * Converts a given value to its PHP representation according to the conversion
1434
     * rules of a specific DBAL mapping type.
1435
     *
1436
     * @param mixed  $value The value to convert.
1437
     * @param string $type  The name of the DBAL mapping type.
1438
     *
1439
     * @return mixed The converted type.
1440
     */
1441
    public function convertToPHPValue($value, $type)
1442
    {
1443
        return Type::getType($type)->convertToPHPValue($value, $this->getDatabasePlatform());
1444
    }
1445
1446
    /**
1447
     * Binds a set of parameters, some or all of which are typed with a PDO binding type
1448
     * or DBAL mapping type, to a given statement.
1449
     *
1450
     * @internal Duck-typing used on the $stmt parameter to support driver statements as well as
1451
     *           raw PDOStatement instances.
1452
     *
1453
     * @param DriverStatement       $stmt   The statement to bind the values to.
1454
     * @param mixed[]               $params The map/list of named/positional parameters.
1455
     * @param int[]|string[]|Type[] $types  The parameter types.
1456
     */
1457 701
    private function _bindTypedValues(DriverStatement $stmt, array $params, array $types) : void
1458
    {
1459
        // Check whether parameters are positional or named. Mixing is not allowed, just like in PDO.
1460 701
        if (is_int(key($params))) {
1461
            // Positional parameters
1462 701
            $typeOffset = array_key_exists(0, $types) ? -1 : 0;
1463 701
            $bindIndex  = 1;
1464 701
            foreach ($params as $value) {
1465 701
                $typeIndex = $bindIndex + $typeOffset;
1466 701
                if (isset($types[$typeIndex])) {
1467 701
                    $type                  = $types[$typeIndex];
1468 701
                    [$value, $bindingType] = $this->getBindingInfo($value, $type);
1469 701
                    $stmt->bindValue($bindIndex, $value, $bindingType);
1470
                } else {
1471 47
                    $stmt->bindValue($bindIndex, $value);
1472
                }
1473 701
                ++$bindIndex;
1474
            }
1475
        } else {
1476
            // Named parameters
1477
            foreach ($params as $name => $value) {
1478
                if (isset($types[$name])) {
1479
                    $type                  = $types[$name];
1480
                    [$value, $bindingType] = $this->getBindingInfo($value, $type);
1481
                    $stmt->bindValue($name, $value, $bindingType);
1482
                } else {
1483
                    $stmt->bindValue($name, $value);
1484
                }
1485
            }
1486
        }
1487 701
    }
1488
1489
    /**
1490
     * Gets the binding type of a given type. The given type can be a PDO or DBAL mapping type.
1491
     *
1492
     * @param mixed      $value The value to bind.
1493
     * @param int|string $type  The type to bind (PDO or DBAL).
1494
     *
1495
     * @return mixed[] [0] => the (escaped) value, [1] => the binding type.
1496
     */
1497 701
    private function getBindingInfo($value, $type)
1498
    {
1499 701
        if (is_string($type)) {
1500 264
            $type = Type::getType($type);
1501
        }
1502 701
        if ($type instanceof Type) {
1503 264
            $value       = $type->convertToDatabaseValue($value, $this->getDatabasePlatform());
1504 264
            $bindingType = $type->getBindingType();
1505
        } else {
1506 557
            $bindingType = $type;
1507
        }
1508
1509 701
        return [$value, $bindingType];
1510
    }
1511
1512
    /**
1513
     * Resolves the parameters to a format which can be displayed.
1514
     *
1515
     * @internal This is a purely internal method. If you rely on this method, you are advised to
1516
     *           copy/paste the code as this method may change, or be removed without prior notice.
1517
     *
1518
     * @param mixed[]        $params
1519
     * @param int[]|string[] $types
1520
     *
1521
     * @return mixed[]
1522
     */
1523 3075
    public function resolveParams(array $params, array $types)
1524
    {
1525 3075
        $resolvedParams = [];
1526
1527
        // Check whether parameters are positional or named. Mixing is not allowed, just like in PDO.
1528 3075
        if (is_int(key($params))) {
1529
            // Positional parameters
1530 212
            $typeOffset = array_key_exists(0, $types) ? -1 : 0;
1531 212
            $bindIndex  = 1;
1532 212
            foreach ($params as $value) {
1533 212
                $typeIndex = $bindIndex + $typeOffset;
1534 212
                if (isset($types[$typeIndex])) {
1535
                    $type                       = $types[$typeIndex];
1536
                    [$value]                    = $this->getBindingInfo($value, $type);
1537
                    $resolvedParams[$bindIndex] = $value;
1538
                } else {
1539 212
                    $resolvedParams[$bindIndex] = $value;
1540
                }
1541 212
                ++$bindIndex;
1542
            }
1543
        } else {
1544
            // Named parameters
1545 2866
            foreach ($params as $name => $value) {
1546
                if (isset($types[$name])) {
1547
                    $type                  = $types[$name];
1548
                    [$value]               = $this->getBindingInfo($value, $type);
1549
                    $resolvedParams[$name] = $value;
1550
                } else {
1551
                    $resolvedParams[$name] = $value;
1552
                }
1553
            }
1554
        }
1555
1556 3075
        return $resolvedParams;
1557
    }
1558
1559
    /**
1560
     * Creates a new instance of a SQL query builder.
1561
     *
1562
     * @return QueryBuilder
1563
     */
1564
    public function createQueryBuilder()
1565
    {
1566
        return new Query\QueryBuilder($this);
1567
    }
1568
1569
    /**
1570
     * Ping the server
1571
     *
1572
     * When the server is not available the method returns FALSE.
1573
     * It is responsibility of the developer to handle this case
1574
     * and abort the request or reconnect manually:
1575
     *
1576
     * @return bool
1577
     *
1578
     * @example
1579
     *
1580
     *   if ($conn->ping() === false) {
1581
     *      $conn->close();
1582
     *      $conn->connect();
1583
     *   }
1584
     *
1585
     * It is undefined if the underlying driver attempts to reconnect
1586
     * or disconnect when the connection is not available anymore
1587
     * as long it returns TRUE when a reconnect succeeded and
1588
     * FALSE when the connection was dropped.
1589
     */
1590 24
    public function ping()
1591
    {
1592 24
        $this->connect();
1593
1594 24
        if ($this->_conn instanceof PingableConnection) {
1595 6
            return $this->_conn->ping();
1596
        }
1597
1598
        try {
1599 18
            $this->query($this->getDatabasePlatform()->getDummySelectSQL());
1600
1601 18
            return true;
1602
        } catch (DBALException $e) {
1603
            return false;
1604
        }
1605
    }
1606
}
1607