Failed Conditions
Pull Request — develop (#3525)
by Jonathan
12:46
created

Connection::executeUpdate()   A

Complexity

Conditions 4
Paths 11

Size

Total Lines 30
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 4

Importance

Changes 0
Metric Value
eloc 19
dl 0
loc 30
ccs 17
cts 17
cp 1
rs 9.6333
c 0
b 0
f 0
cc 4
nc 11
nop 3
crap 4
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\DBAL;
6
7
use Closure;
8
use Doctrine\Common\EventManager;
9
use Doctrine\DBAL\Cache\ArrayStatement;
10
use Doctrine\DBAL\Cache\CacheException;
11
use Doctrine\DBAL\Cache\Exception\NoResultDriverConfigured;
12
use Doctrine\DBAL\Cache\QueryCacheProfile;
13
use Doctrine\DBAL\Cache\ResultCacheStatement;
14
use Doctrine\DBAL\Driver\Connection as DriverConnection;
15
use Doctrine\DBAL\Driver\DriverException;
16
use Doctrine\DBAL\Driver\PingableConnection;
17
use Doctrine\DBAL\Driver\ResultStatement;
18
use Doctrine\DBAL\Driver\ServerInfoAwareConnection;
19
use Doctrine\DBAL\Driver\Statement as DriverStatement;
20
use Doctrine\DBAL\Exception\CommitFailedRollbackOnly;
21
use Doctrine\DBAL\Exception\EmptyCriteriaNotAllowed;
22
use Doctrine\DBAL\Exception\InvalidArgumentException;
23
use Doctrine\DBAL\Exception\InvalidPlatformType;
24
use Doctrine\DBAL\Exception\MayNotAlterNestedTransactionWithSavepointsInTransaction;
25
use Doctrine\DBAL\Exception\NoActiveTransaction;
26
use Doctrine\DBAL\Exception\SavepointsNotSupported;
27
use Doctrine\DBAL\Platforms\AbstractPlatform;
28
use Doctrine\DBAL\Query\Expression\ExpressionBuilder;
29
use Doctrine\DBAL\Query\QueryBuilder;
30
use Doctrine\DBAL\Schema\AbstractSchemaManager;
31
use Doctrine\DBAL\Types\Type;
32
use Exception;
33
use Throwable;
34
use function array_key_exists;
35
use function assert;
36
use function implode;
37
use function is_int;
38
use function is_string;
39
use function key;
40
41
/**
42
 * A wrapper around a Doctrine\DBAL\Driver\Connection that adds features like
43
 * events, transaction isolation levels, configuration, emulated transaction nesting,
44
 * lazy connecting and more.
45
 */
46
class Connection implements DriverConnection
47
{
48
    /**
49
     * Constant for transaction isolation level READ UNCOMMITTED.
50
     *
51
     * @deprecated Use TransactionIsolationLevel::READ_UNCOMMITTED.
52
     */
53
    public const TRANSACTION_READ_UNCOMMITTED = TransactionIsolationLevel::READ_UNCOMMITTED;
54
55
    /**
56
     * Constant for transaction isolation level READ COMMITTED.
57
     *
58
     * @deprecated Use TransactionIsolationLevel::READ_COMMITTED.
59
     */
60
    public const TRANSACTION_READ_COMMITTED = TransactionIsolationLevel::READ_COMMITTED;
61
62
    /**
63
     * Constant for transaction isolation level REPEATABLE READ.
64
     *
65
     * @deprecated Use TransactionIsolationLevel::REPEATABLE_READ.
66
     */
67
    public const TRANSACTION_REPEATABLE_READ = TransactionIsolationLevel::REPEATABLE_READ;
68
69
    /**
70
     * Constant for transaction isolation level SERIALIZABLE.
71
     *
72
     * @deprecated Use TransactionIsolationLevel::SERIALIZABLE.
73
     */
74
    public const TRANSACTION_SERIALIZABLE = TransactionIsolationLevel::SERIALIZABLE;
75
76
    /**
77
     * Represents an array of ints to be expanded by Doctrine SQL parsing.
78
     */
79
    public const PARAM_INT_ARRAY = ParameterType::INTEGER + self::ARRAY_PARAM_OFFSET;
80
81
    /**
82
     * Represents an array of strings to be expanded by Doctrine SQL parsing.
83
     */
84
    public const PARAM_STR_ARRAY = ParameterType::STRING + self::ARRAY_PARAM_OFFSET;
85
86
    /**
87
     * Offset by which PARAM_* constants are detected as arrays of the param type.
88
     */
89
    public const ARRAY_PARAM_OFFSET = 100;
90
91
    /**
92
     * The wrapped driver connection.
93
     *
94
     * @var \Doctrine\DBAL\Driver\Connection|null
95
     */
96
    protected $_conn;
97
98
    /** @var Configuration */
99
    protected $_config;
100
101
    /** @var EventManager */
102
    protected $_eventManager;
103
104
    /** @var ExpressionBuilder */
105
    protected $_expr;
106
107
    /**
108
     * Whether or not a connection has been established.
109
     *
110
     * @var bool
111
     */
112
    private $isConnected = false;
113
114
    /**
115
     * The current auto-commit mode of this connection.
116
     *
117
     * @var bool
118
     */
119
    private $autoCommit = true;
120
121
    /**
122
     * The transaction nesting level.
123
     *
124
     * @var int
125
     */
126
    private $transactionNestingLevel = 0;
127
128
    /**
129
     * The currently active transaction isolation level.
130
     *
131
     * @var int
132
     */
133
    private $transactionIsolationLevel;
134
135
    /**
136
     * If nested transactions should use savepoints.
137
     *
138
     * @var bool
139
     */
140
    private $nestTransactionsWithSavepoints = false;
141
142
    /**
143
     * The parameters used during creation of the Connection instance.
144
     *
145
     * @var mixed[]
146
     */
147
    private $params = [];
148
149
    /**
150
     * The DatabasePlatform object that provides information about the
151
     * database platform used by the connection.
152
     *
153
     * @var AbstractPlatform
154
     */
155
    private $platform;
156
157
    /**
158
     * The schema manager.
159
     *
160
     * @var AbstractSchemaManager|null
161
     */
162
    protected $_schemaManager;
163
164
    /**
165
     * The used DBAL driver.
166
     *
167
     * @var Driver
168
     */
169
    protected $_driver;
170
171
    /**
172
     * Flag that indicates whether the current transaction is marked for rollback only.
173
     *
174
     * @var bool
175
     */
176
    private $isRollbackOnly = false;
177
178
    /** @var int */
179
    protected $defaultFetchMode = FetchMode::ASSOCIATIVE;
180
181
    /**
182
     * Initializes a new instance of the Connection class.
183
     *
184
     * @param mixed[]            $params       The connection parameters.
185
     * @param Driver             $driver       The driver to use.
186
     * @param Configuration|null $config       The configuration, optional.
187
     * @param EventManager|null  $eventManager The event manager, optional.
188
     *
189
     * @throws DBALException
190
     */
191 10235
    public function __construct(
192
        array $params,
193
        Driver $driver,
194
        ?Configuration $config = null,
195
        ?EventManager $eventManager = null
196
    ) {
197 10235
        $this->_driver = $driver;
198 10235
        $this->params  = $params;
199
200 10235
        if (isset($params['pdo'])) {
201 9230
            $this->_conn       = $params['pdo'];
202 9230
            $this->isConnected = true;
203 9230
            unset($this->params['pdo']);
204
        }
205
206 10235
        if (isset($params['platform'])) {
207 9411
            if (! $params['platform'] instanceof Platforms\AbstractPlatform) {
208 8983
                throw InvalidPlatformType::new($params['platform']);
209
            }
210
211 9409
            $this->platform = $params['platform'];
212 9409
            unset($this->params['platform']);
213
        }
214
215
        // Create default config and event manager if none given
216 10235
        if (! $config) {
217 9766
            $config = new Configuration();
218
        }
219
220 10235
        if (! $eventManager) {
221 9539
            $eventManager = new EventManager();
222
        }
223
224 10235
        $this->_config       = $config;
225 10235
        $this->_eventManager = $eventManager;
226
227 10235
        $this->_expr = new Query\Expression\ExpressionBuilder($this);
228
229 10235
        $this->autoCommit = $config->getAutoCommit();
230 10235
    }
231
232
    /**
233
     * Gets the parameters used during instantiation.
234
     *
235
     * @return mixed[]
236
     */
237 9412
    public function getParams()
238
    {
239 9412
        return $this->params;
240
    }
241
242
    /**
243
     * Gets the name of the database this Connection is connected to.
244
     *
245
     * @return string
246
     */
247 8633
    public function getDatabase()
248
    {
249 8633
        return $this->_driver->getDatabase($this);
250
    }
251
252
    /**
253
     * Gets the hostname of the currently connected database.
254
     *
255
     * @return string|null
256
     */
257 9833
    public function getHost()
258
    {
259 9833
        return $this->params['host'] ?? null;
260
    }
261
262
    /**
263
     * Gets the port of the currently connected database.
264
     *
265
     * @return mixed
266
     */
267 9808
    public function getPort()
268
    {
269 9808
        return $this->params['port'] ?? null;
270
    }
271
272
    /**
273
     * Gets the username used by this connection.
274
     *
275
     * @return string|null
276
     */
277 9810
    public function getUsername()
278
    {
279 9810
        return $this->params['user'] ?? null;
280
    }
281
282
    /**
283
     * Gets the password used by this connection.
284
     *
285
     * @return string|null
286
     */
287 9783
    public function getPassword()
288
    {
289 9783
        return $this->params['password'] ?? null;
290
    }
291
292
    /**
293
     * Gets the DBAL driver instance.
294
     *
295
     * @return Driver
296
     */
297 9886
    public function getDriver()
298
    {
299 9886
        return $this->_driver;
300
    }
301
302
    /**
303
     * Gets the Configuration used by the Connection.
304
     *
305
     * @return Configuration
306
     */
307 10502
    public function getConfiguration()
308
    {
309 10502
        return $this->_config;
310
    }
311
312
    /**
313
     * Gets the EventManager used by the Connection.
314
     *
315
     * @return EventManager
316
     */
317 9775
    public function getEventManager()
318
    {
319 9775
        return $this->_eventManager;
320
    }
321
322
    /**
323
     * Gets the DatabasePlatform for the connection.
324
     *
325
     * @return AbstractPlatform
326
     *
327
     * @throws DBALException
328
     */
329 10254
    public function getDatabasePlatform()
330
    {
331 10254
        if ($this->platform === null) {
332 9760
            $this->detectDatabasePlatform();
333
        }
334
335 10252
        return $this->platform;
336
    }
337
338
    /**
339
     * Gets the ExpressionBuilder for the connection.
340
     *
341
     * @return ExpressionBuilder
342
     */
343
    public function getExpressionBuilder()
344
    {
345
        return $this->_expr;
346
    }
347
348
    /**
349
     * Establishes the connection with the database.
350
     *
351
     * @throws DriverException
352
     */
353 10393
    public function connect() : void
354
    {
355 10393
        if ($this->isConnected) {
356 10111
            return;
357
        }
358
359 9819
        $driverOptions = $this->params['driverOptions'] ?? [];
360 9819
        $user          = $this->params['user'] ?? null;
361 9819
        $password      = $this->params['password'] ?? null;
362
363 9819
        $this->_conn       = $this->_driver->connect($this->params, $user, $password, $driverOptions);
364 9811
        $this->isConnected = true;
365
366 9811
        if ($this->autoCommit === false) {
367 9487
            $this->beginTransaction();
368
        }
369
370 9811
        if (! $this->_eventManager->hasListeners(Events::postConnect)) {
371 9713
            return;
372
        }
373
374 9779
        $eventArgs = new Event\ConnectionEventArgs($this);
375 9779
        $this->_eventManager->dispatchEvent(Events::postConnect, $eventArgs);
376 9779
    }
377
378
    /**
379
     * Detects and sets the database platform.
380
     *
381
     * Evaluates custom platform class and version in order to set the correct platform.
382
     *
383
     * @throws DBALException If an invalid platform was specified for this connection.
384
     */
385 9760
    private function detectDatabasePlatform()
386
    {
387 9760
        $version = $this->getDatabasePlatformVersion();
388
389 9758
        if ($version !== null) {
390 9058
            assert($this->_driver instanceof VersionAwarePlatformDriver);
391
392 9058
            $this->platform = $this->_driver->createDatabasePlatformForVersion($version);
393
        } else {
394 9756
            $this->platform = $this->_driver->getDatabasePlatform();
395
        }
396
397 9758
        $this->platform->setEventManager($this->_eventManager);
398 9758
    }
399
400
    /**
401
     * Returns the version of the related platform if applicable.
402
     *
403
     * Returns null if either the driver is not capable to create version
404
     * specific platform instances, no explicit server version was specified
405
     * or the underlying driver connection cannot determine the platform
406
     * version without having to query it (performance reasons).
407
     *
408
     * @return string|null
409
     *
410
     * @throws Exception
411
     */
412 9760
    private function getDatabasePlatformVersion()
413
    {
414
        // Driver does not support version specific platforms.
415 9760
        if (! $this->_driver instanceof VersionAwarePlatformDriver) {
416 9756
            return null;
417
        }
418
419
        // Explicit platform version requested (supersedes auto-detection).
420 9060
        if (isset($this->params['serverVersion'])) {
421
            return $this->params['serverVersion'];
422
        }
423
424
        // If not connected, we need to connect now to determine the platform version.
425 9060
        if ($this->_conn === null) {
426
            try {
427 9060
                $this->connect();
428 8958
            } catch (Throwable $originalException) {
429 8958
                if (empty($this->params['dbname'])) {
430
                    throw $originalException;
431
                }
432
433
                // The database to connect to might not yet exist.
434
                // Retry detection without database name connection parameter.
435 8958
                $databaseName           = $this->params['dbname'];
436 8958
                $this->params['dbname'] = null;
437
438
                try {
439 8958
                    $this->connect();
440 8958
                } catch (Throwable $fallbackException) {
441
                    // Either the platform does not support database-less connections
442
                    // or something else went wrong.
443
                    // Reset connection parameters and rethrow the original exception.
444 8958
                    $this->params['dbname'] = $databaseName;
445
446 8958
                    throw $originalException;
447
                }
448
449
                // Reset connection parameters.
450 7157
                $this->params['dbname'] = $databaseName;
451 7157
                $serverVersion          = $this->getServerVersion();
452
453
                // Close "temporary" connection to allow connecting to the real database again.
454 7157
                $this->close();
455
456 7157
                return $serverVersion;
457
            }
458
        }
459
460 9058
        return $this->getServerVersion();
461
    }
462
463
    /**
464
     * Returns the database server version if the underlying driver supports it.
465
     *
466
     * @return string|null
467
     */
468 9058
    private function getServerVersion()
469
    {
470 9058
        $connection = $this->getWrappedConnection();
471
472
        // Automatic platform version detection.
473 9058
        if ($connection instanceof ServerInfoAwareConnection && ! $connection->requiresQueryForServerVersion()) {
474 9058
            return $connection->getServerVersion();
475
        }
476
477
        // Unable to detect platform version.
478
        return null;
479
    }
480
481
    /**
482
     * Returns the current auto-commit mode for this connection.
483
     *
484
     * @see    setAutoCommit
485
     *
486
     * @return bool True if auto-commit mode is currently enabled for this connection, false otherwise.
487
     */
488 9535
    public function isAutoCommit()
489
    {
490 9535
        return $this->autoCommit === true;
491
    }
492
493
    /**
494
     * Sets auto-commit mode for this connection.
495
     *
496
     * If a connection is in auto-commit mode, then all its SQL statements will be executed and committed as individual
497
     * transactions. Otherwise, its SQL statements are grouped into transactions that are terminated by a call to either
498
     * the method commit or the method rollback. By default, new connections are in auto-commit mode.
499
     *
500
     * NOTE: If this method is called during a transaction and the auto-commit mode is changed, the transaction is
501
     * committed. If this method is called and the auto-commit mode is not changed, the call is a no-op.
502
     *
503
     * @see   isAutoCommit
504
     *
505
     * @throws ConnectionException
506
     * @throws DriverException
507
     */
508 9516
    public function setAutoCommit(bool $autoCommit) : void
509
    {
510
        // Mode not changed, no-op.
511 9516
        if ($autoCommit === $this->autoCommit) {
512
            return;
513
        }
514
515 9516
        $this->autoCommit = $autoCommit;
516
517
        // Commit all currently active transactions if any when switching auto-commit mode.
518 9516
        if ($this->isConnected !== true || $this->transactionNestingLevel === 0) {
519 9514
            return;
520
        }
521
522 9408
        $this->commitAll();
523 9408
    }
524
525
    /**
526
     * Sets the fetch mode.
527
     *
528
     * @param int $fetchMode
529
     */
530 3999
    public function setFetchMode($fetchMode) : void
531
    {
532 3999
        $this->defaultFetchMode = $fetchMode;
533 3999
    }
534
535
    /**
536
     * Prepares and executes an SQL query and returns the first row of the result
537
     * as an associative array.
538
     *
539
     * @param string         $statement The SQL query.
540
     * @param mixed[]        $params    The query parameters.
541
     * @param int[]|string[] $types     The query parameter types.
542
     *
543
     * @return mixed[]|false False is returned if no rows are found.
544
     *
545
     * @throws DBALException
546
     */
547 9363
    public function fetchAssoc($statement, array $params = [], array $types = [])
548
    {
549 9363
        return $this->executeQuery($statement, $params, $types)->fetch(FetchMode::ASSOCIATIVE);
550
    }
551
552
    /**
553
     * Prepares and executes an SQL query and returns the first row of the result
554
     * as a numerically indexed array.
555
     *
556
     * @param string         $statement The SQL query to be executed.
557
     * @param mixed[]        $params    The prepared statement params.
558
     * @param int[]|string[] $types     The query parameter types.
559
     *
560
     * @return mixed[]|false False is returned if no rows are found.
561
     */
562 9264
    public function fetchArray($statement, array $params = [], array $types = [])
563
    {
564 9264
        return $this->executeQuery($statement, $params, $types)->fetch(FetchMode::NUMERIC);
565
    }
566
567
    /**
568
     * Prepares and executes an SQL query and returns the value of a single column
569
     * of the first row of the result.
570
     *
571
     * @param string         $statement The SQL query to be executed.
572
     * @param mixed[]        $params    The prepared statement params.
573
     * @param int            $column    The 0-indexed column number to retrieve.
574
     * @param int[]|string[] $types     The query parameter types.
575
     *
576
     * @return mixed|false False is returned if no rows are found.
577
     *
578
     * @throws DBALException
579
     */
580 9326
    public function fetchColumn($statement, array $params = [], $column = 0, array $types = [])
581
    {
582 9326
        return $this->executeQuery($statement, $params, $types)->fetchColumn($column);
583
    }
584
585
    /**
586
     * Whether an actual connection to the database is established.
587
     *
588
     * @return bool
589
     */
590 9997
    public function isConnected()
591
    {
592 9997
        return $this->isConnected;
593
    }
594
595
    /**
596
     * Checks whether a transaction is currently active.
597
     *
598
     * @return bool TRUE if a transaction is currently active, FALSE otherwise.
599
     */
600 10600
    public function isTransactionActive()
601
    {
602 10600
        return $this->transactionNestingLevel > 0;
603
    }
604
605
    /**
606
     * Adds identifier condition to the query components
607
     *
608
     * @param mixed[]  $identifier Map of key columns to their values
609
     * @param string[] $columns    Column names
610
     * @param mixed[]  $values     Column values
611
     * @param string[] $conditions Key conditions
612
     *
613
     * @throws DBALException
614
     */
615 9386
    private function addIdentifierCondition(
616
        array $identifier,
617
        array &$columns,
618
        array &$values,
619
        array &$conditions
620
    ) : void {
621 9386
        $platform = $this->getDatabasePlatform();
622
623 9386
        foreach ($identifier as $columnName => $value) {
624 9386
            if ($value === null) {
625 9339
                $conditions[] = $platform->getIsNullExpression($columnName);
626 9339
                continue;
627
            }
628
629 9382
            $columns[]    = $columnName;
630 9382
            $values[]     = $value;
631 9382
            $conditions[] = $columnName . ' = ?';
632
        }
633 9386
    }
634
635
    /**
636
     * Executes an SQL DELETE statement on a table.
637
     *
638
     * Table expression and columns are not escaped and are not safe for user-input.
639
     *
640
     * @param string         $tableExpression The expression of the table on which to delete.
641
     * @param mixed[]        $identifier      The deletion criteria. An associative array containing column-value pairs.
642
     * @param int[]|string[] $types           The types of identifiers.
643
     *
644
     * @return int The number of affected rows.
645
     *
646
     * @throws DBALException
647
     * @throws InvalidArgumentException
648
     */
649 9320
    public function delete($tableExpression, array $identifier, array $types = [])
650
    {
651 9320
        if (empty($identifier)) {
652 9183
            throw EmptyCriteriaNotAllowed::new();
653
        }
654
655 9318
        $columns = $values = $conditions = [];
656
657 9318
        $this->addIdentifierCondition($identifier, $columns, $values, $conditions);
658
659 9318
        return $this->executeUpdate(
660 9318
            'DELETE FROM ' . $tableExpression . ' WHERE ' . implode(' AND ', $conditions),
661 12
            $values,
662 9318
            is_string(key($types)) ? $this->extractTypeValues($columns, $types) : $types
663
        );
664
    }
665
666
    /**
667
     * Closes the connection.
668
     *
669
     * @return void
670
     */
671 8899
    public function close()
672
    {
673 8899
        $this->_conn = null;
674
675 8899
        $this->isConnected = false;
676 8899
    }
677
678
    /**
679
     * Sets the transaction isolation level.
680
     *
681
     * @param int $level The level to set.
682
     *
683
     * @return int
684
     */
685
    public function setTransactionIsolation($level)
686
    {
687
        $this->transactionIsolationLevel = $level;
688
689
        return $this->executeUpdate($this->getDatabasePlatform()->getSetTransactionIsolationSQL($level));
690
    }
691
692
    /**
693
     * Gets the currently active transaction isolation level.
694
     *
695
     * @return int The current transaction isolation level.
696
     */
697
    public function getTransactionIsolation()
698
    {
699
        if ($this->transactionIsolationLevel === null) {
700
            $this->transactionIsolationLevel = $this->getDatabasePlatform()->getDefaultTransactionIsolationLevel();
701
        }
702
703
        return $this->transactionIsolationLevel;
704
    }
705
706
    /**
707
     * Executes an SQL UPDATE statement on a table.
708
     *
709
     * Table expression and columns are not escaped and are not safe for user-input.
710
     *
711
     * @param string         $tableExpression The expression of the table to update quoted or unquoted.
712
     * @param mixed[]        $data            An associative array containing column-value pairs.
713
     * @param mixed[]        $identifier      The update criteria. An associative array containing column-value pairs.
714
     * @param int[]|string[] $types           Types of the merged $data and $identifier arrays in that order.
715
     *
716
     * @return int The number of affected rows.
717
     *
718
     * @throws DBALException
719
     */
720 9374
    public function update($tableExpression, array $data, array $identifier, array $types = [])
721
    {
722 9374
        $columns = $values = $conditions = $set = [];
723
724 9374
        foreach ($data as $columnName => $value) {
725 9374
            $columns[] = $columnName;
726 9374
            $values[]  = $value;
727 9374
            $set[]     = $columnName . ' = ?';
728
        }
729
730 9374
        $this->addIdentifierCondition($identifier, $columns, $values, $conditions);
731
732 9374
        if (is_string(key($types))) {
733 9366
            $types = $this->extractTypeValues($columns, $types);
734
        }
735
736 9374
        $sql = 'UPDATE ' . $tableExpression . ' SET ' . implode(', ', $set)
737 9374
                . ' WHERE ' . implode(' AND ', $conditions);
738
739 9374
        return $this->executeUpdate($sql, $values, $types);
740
    }
741
742
    /**
743
     * Inserts a table row with specified data.
744
     *
745
     * Table expression and columns are not escaped and are not safe for user-input.
746
     *
747
     * @param string         $tableExpression The expression of the table to insert data into, quoted or unquoted.
748
     * @param mixed[]        $data            An associative array containing column-value pairs.
749
     * @param int[]|string[] $types           Types of the inserted data.
750
     *
751
     * @return int The number of affected rows.
752
     *
753
     * @throws DBALException
754
     */
755 9539
    public function insert($tableExpression, array $data, array $types = [])
756
    {
757 9539
        if (empty($data)) {
758 9383
            return $this->executeUpdate('INSERT INTO ' . $tableExpression . ' () VALUES ()');
759
        }
760
761 9312
        $columns = [];
762 9312
        $values  = [];
763 9312
        $set     = [];
764
765 9312
        foreach ($data as $columnName => $value) {
766 9312
            $columns[] = $columnName;
767 9312
            $values[]  = $value;
768 9312
            $set[]     = '?';
769
        }
770
771 9312
        return $this->executeUpdate(
772 9312
            'INSERT INTO ' . $tableExpression . ' (' . implode(', ', $columns) . ')' .
773 9312
            ' VALUES (' . implode(', ', $set) . ')',
774 156
            $values,
775 9312
            is_string(key($types)) ? $this->extractTypeValues($columns, $types) : $types
776
        );
777
    }
778
779
    /**
780
     * Extract ordered type list from an ordered column list and type map.
781
     *
782
     * @param int[]|string[] $columnList
783
     * @param int[]|string[] $types
784
     *
785
     * @return int[]|string[]
786
     */
787 9374
    private function extractTypeValues(array $columnList, array $types)
788
    {
789 9374
        $typeValues = [];
790
791 9374
        foreach ($columnList as $columnIndex => $columnName) {
792 9374
            $typeValues[] = $types[$columnName] ?? ParameterType::STRING;
793
        }
794
795 9374
        return $typeValues;
796
    }
797
798
    /**
799
     * Quotes a string so it can be safely used as a table or column name, even if
800
     * it is a reserved name.
801
     *
802
     * Delimiting style depends on the underlying database platform that is being used.
803
     *
804
     * NOTE: Just because you CAN use quoted identifiers does not mean
805
     * you SHOULD use them. In general, they end up causing way more
806
     * problems than they solve.
807
     *
808
     * @param string $str The name to be quoted.
809
     *
810
     * @return string The quoted name.
811
     */
812 7547
    public function quoteIdentifier($str)
813
    {
814 7547
        return $this->getDatabasePlatform()->quoteIdentifier($str);
815
    }
816
817
    /**
818
     * {@inheritDoc}
819
     */
820 7803
    public function quote(string $input) : string
821
    {
822 7803
        return $this->getWrappedConnection()->quote($input);
823
    }
824
825
    /**
826
     * Prepares and executes an SQL query and returns the result as an associative array.
827
     *
828
     * @param string         $sql    The SQL query.
829
     * @param mixed[]        $params The query parameters.
830
     * @param int[]|string[] $types  The query parameter types.
831
     *
832
     * @return mixed[]
833
     */
834 9405
    public function fetchAll($sql, array $params = [], $types = [])
835
    {
836 9405
        return $this->executeQuery($sql, $params, $types)->fetchAll();
837
    }
838
839
    /**
840
     * Prepares an SQL statement.
841
     *
842
     * @param string $sql The SQL statement to prepare.
843
     *
844
     * @throws DBALException
845
     */
846 9757
    public function prepare(string $sql) : DriverStatement
847
    {
848
        try {
849 9757
            $stmt = new Statement($sql, $this);
850 9583
        } catch (Throwable $ex) {
851 9583
            throw DBALException::driverExceptionDuringQuery($this->_driver, $ex, $sql);
852
        }
853
854 9280
        $stmt->setFetchMode($this->defaultFetchMode);
855
856 9280
        return $stmt;
857
    }
858
859
    /**
860
     * Executes an, optionally parametrized, SQL query.
861
     *
862
     * If the query is parametrized, a prepared statement is used.
863
     * If an SQLLogger is configured, the execution is logged.
864
     *
865
     * @param string                 $query  The SQL query to execute.
866
     * @param mixed[]                $params The parameters to bind to the query, if any.
867
     * @param int[]|string[]         $types  The types the previous parameters are in.
868
     * @param QueryCacheProfile|null $qcp    The query cache profile, optional.
869
     *
870
     * @return ResultStatement The executed statement.
871
     *
872
     * @throws DBALException
873
     */
874 10025
    public function executeQuery(string $query, array $params = [], $types = [], ?QueryCacheProfile $qcp = null) : ResultStatement
875
    {
876 10025
        if ($qcp !== null) {
877 3930
            return $this->executeCacheQuery($query, $params, $types, $qcp);
878
        }
879
880 10025
        $connection = $this->getWrappedConnection();
881
882 10025
        $logger = $this->_config->getSQLLogger();
883 10025
        $logger->startQuery($query, $params, $types);
884
885
        try {
886 10025
            if ($params) {
887 7577
                [$query, $params, $types] = SQLParserUtils::expandListParameters($query, $params, $types);
888
889 7577
                $stmt = $connection->prepare($query);
890 7577
                if ($types) {
891 7502
                    $this->_bindTypedValues($stmt, $params, $types);
892 7502
                    $stmt->execute();
893
                } else {
894 7577
                    $stmt->execute($params);
895
                }
896
            } else {
897 10017
                $stmt = $connection->query($query);
898
            }
899 9647
        } catch (Throwable $ex) {
900 9647
            throw DBALException::driverExceptionDuringQuery($this->_driver, $ex, $query, $this->resolveParams($params, $types));
901
        }
902
903 8246
        $stmt->setFetchMode($this->defaultFetchMode);
904
905 8246
        $logger->stopQuery();
906
907 8246
        return $stmt;
908
    }
909
910
    /**
911
     * Executes a caching query.
912
     *
913
     * @param string            $query  The SQL query to execute.
914
     * @param mixed[]           $params The parameters to bind to the query, if any.
915
     * @param int[]|string[]    $types  The types the previous parameters are in.
916
     * @param QueryCacheProfile $qcp    The query cache profile.
917
     *
918
     * @throws CacheException
919
     */
920 9057
    public function executeCacheQuery($query, $params, $types, QueryCacheProfile $qcp) : ResultStatement
921
    {
922 9057
        $resultCache = $qcp->getResultCacheDriver() ?: $this->_config->getResultCacheImpl();
923
924 9057
        if ($resultCache === null) {
925
            throw NoResultDriverConfigured::new();
926
        }
927
928 9057
        [$cacheKey, $realKey] = $qcp->generateCacheKeys($query, $params, $types, $this->getParams());
929
930
        // fetch the row pointers entry
931 9057
        $data = $resultCache->fetch($cacheKey);
932
933 9057
        if ($data !== false) {
934
            // is the real key part of this row pointers map or is the cache only pointing to other cache keys?
935 9049
            if (isset($data[$realKey])) {
936 9049
                $stmt = new ArrayStatement($data[$realKey]);
937
            } elseif (array_key_exists($realKey, $data)) {
938
                $stmt = new ArrayStatement([]);
939
            }
940
        }
941
942 9057
        if (! isset($stmt)) {
943 3930
            $stmt = new ResultCacheStatement($this->executeQuery($query, $params, $types), $resultCache, $cacheKey, $realKey, $qcp->getLifetime());
944
        }
945
946 9057
        $stmt->setFetchMode($this->defaultFetchMode);
947
948 9057
        return $stmt;
949
    }
950
951
    /**
952
     * Executes an, optionally parametrized, SQL query and returns the result,
953
     * applying a given projection/transformation function on each row of the result.
954
     *
955
     * @param string  $query    The SQL query to execute.
956
     * @param mixed[] $params   The parameters, if any.
957
     * @param Closure $function The transformation function that is applied on each row.
958
     *                           The function receives a single parameter, an array, that
959
     *                           represents a row of the result set.
960
     *
961
     * @return mixed[] The projected result of the query.
962
     */
963
    public function project($query, array $params, Closure $function)
964
    {
965
        $result = [];
966
        $stmt   = $this->executeQuery($query, $params);
967
968
        while ($row = $stmt->fetch()) {
969
            $result[] = $function($row);
970
        }
971
972
        $stmt->closeCursor();
973
974
        return $result;
975
    }
976
977
    /**
978
     * {@inheritDoc}
979
     */
980 9688
    public function query(string $sql) : ResultStatement
981
    {
982 9688
        $connection = $this->getWrappedConnection();
983
984 9688
        $logger = $this->_config->getSQLLogger();
985 9688
        $logger->startQuery($sql);
986
987
        try {
988 9688
            $statement = $connection->query($sql);
989 9658
        } catch (Throwable $ex) {
990 9658
            throw DBALException::driverExceptionDuringQuery($this->_driver, $ex, $sql);
991
        }
992
993 8198
        $statement->setFetchMode($this->defaultFetchMode);
994
995 8198
        $logger->stopQuery();
996
997 8198
        return $statement;
998
    }
999
1000
    /**
1001
     * Executes an SQL INSERT/UPDATE/DELETE query with the given parameters
1002
     * and returns the number of affected rows.
1003
     *
1004
     * This method supports PDO binding types as well as DBAL mapping types.
1005
     *
1006
     * @param string         $query  The SQL query.
1007
     * @param mixed[]        $params The query parameters.
1008
     * @param int[]|string[] $types  The parameter types.
1009
     *
1010
     * @throws DBALException
1011
     */
1012 9966
    public function executeUpdate(string $query, array $params = [], array $types = []) : int
1013
    {
1014 9966
        $connection = $this->getWrappedConnection();
1015
1016 9966
        $logger = $this->_config->getSQLLogger();
1017 9966
        $logger->startQuery($query, $params, $types);
1018
1019
        try {
1020 9966
            if ($params) {
1021 9326
                [$query, $params, $types] = SQLParserUtils::expandListParameters($query, $params, $types);
1022
1023 9326
                $stmt = $connection->prepare($query);
1024
1025 9326
                if ($types) {
1026 8219
                    $this->_bindTypedValues($stmt, $params, $types);
1027 8219
                    $stmt->execute();
1028
                } else {
1029 9300
                    $stmt->execute($params);
1030
                }
1031 9322
                $result = $stmt->rowCount();
1032
            } else {
1033 9962
                $result = $connection->exec($query);
1034
            }
1035 9888
        } catch (Throwable $ex) {
1036 9888
            throw DBALException::driverExceptionDuringQuery($this->_driver, $ex, $query, $this->resolveParams($params, $types));
1037
        }
1038
1039 9498
        $logger->stopQuery();
1040
1041 9498
        return $result;
1042
    }
1043
1044
    /**
1045
     * {@inheritDoc}
1046
     */
1047 9837
    public function exec(string $statement) : int
1048
    {
1049 9837
        $connection = $this->getWrappedConnection();
1050
1051 9831
        $logger = $this->_config->getSQLLogger();
1052 9831
        $logger->startQuery($statement);
1053
1054
        try {
1055 9831
            $result = $connection->exec($statement);
1056 9791
        } catch (Throwable $ex) {
1057 9791
            throw DBALException::driverExceptionDuringQuery($this->_driver, $ex, $statement);
1058
        }
1059
1060 5571
        $logger->stopQuery();
1061
1062 5571
        return $result;
1063
    }
1064
1065
    /**
1066
     * Returns the current transaction nesting level.
1067
     *
1068
     * @return int The nesting level. A value of 0 means there's no active transaction.
1069
     */
1070 9436
    public function getTransactionNestingLevel()
1071
    {
1072 9436
        return $this->transactionNestingLevel;
1073
    }
1074
1075
    /**
1076
     * Returns the ID of the last inserted row, or the last value from a sequence object,
1077
     * depending on the underlying driver.
1078
     *
1079
     * Note: This method may not return a meaningful or consistent result across different drivers,
1080
     * because the underlying database may not even support the notion of AUTO_INCREMENT/IDENTITY
1081
     * columns or sequences.
1082
     *
1083
     * @param string|null $seqName Name of the sequence object from which the ID should be returned.
1084
     *
1085
     * @return string A string representation of the last inserted ID.
1086
     */
1087 735
    public function lastInsertId($seqName = null)
1088
    {
1089 735
        return $this->getWrappedConnection()->lastInsertId($seqName);
1090
    }
1091
1092
    /**
1093
     * Executes a function in a transaction.
1094
     *
1095
     * The function gets passed this Connection instance as an (optional) parameter.
1096
     *
1097
     * If an exception occurs during execution of the function or transaction commit,
1098
     * the transaction is rolled back and the exception re-thrown.
1099
     *
1100
     * @param Closure $func The function to execute transactionally.
1101
     *
1102
     * @return mixed The value returned by $func
1103
     *
1104
     * @throws Throwable
1105
     */
1106 7876
    public function transactional(Closure $func)
1107
    {
1108 7876
        $this->beginTransaction();
1109
        try {
1110 7876
            $res = $func($this);
1111 7847
            $this->commit();
1112
1113 7847
            return $res;
1114 7872
        } catch (Throwable $e) {
1115 7872
            $this->rollBack();
1116 7872
            throw $e;
1117
        }
1118
    }
1119
1120
    /**
1121
     * Sets if nested transactions should use savepoints.
1122
     *
1123
     * @param bool $nestTransactionsWithSavepoints
1124
     *
1125
     * @return void
1126
     *
1127
     * @throws ConnectionException
1128
     */
1129 7972
    public function setNestTransactionsWithSavepoints($nestTransactionsWithSavepoints)
1130
    {
1131 7972
        if ($this->transactionNestingLevel > 0) {
1132 7972
            throw MayNotAlterNestedTransactionWithSavepointsInTransaction::new();
1133
        }
1134
1135 7970
        if (! $this->getDatabasePlatform()->supportsSavepoints()) {
1136
            throw SavepointsNotSupported::new();
1137
        }
1138
1139 7970
        $this->nestTransactionsWithSavepoints = (bool) $nestTransactionsWithSavepoints;
1140 7970
    }
1141
1142
    /**
1143
     * Gets if nested transactions should use savepoints.
1144
     *
1145
     * @return bool
1146
     */
1147 7970
    public function getNestTransactionsWithSavepoints()
1148
    {
1149 7970
        return $this->nestTransactionsWithSavepoints;
1150
    }
1151
1152
    /**
1153
     * Returns the savepoint name to use for nested transactions are false if they are not supported
1154
     * "savepointFormat" parameter is not set
1155
     *
1156
     * @return mixed A string with the savepoint name or false.
1157
     */
1158 7970
    protected function _getNestedTransactionSavePointName()
1159
    {
1160 7970
        return 'DOCTRINE2_SAVEPOINT_' . $this->transactionNestingLevel;
1161
    }
1162
1163
    /**
1164
     * {@inheritDoc}
1165
     */
1166 9521
    public function beginTransaction() : void
1167
    {
1168 9521
        $connection = $this->getWrappedConnection();
1169
1170 9521
        ++$this->transactionNestingLevel;
1171
1172 9521
        $logger = $this->_config->getSQLLogger();
1173
1174 9521
        if ($this->transactionNestingLevel === 1) {
1175 9521
            $logger->startQuery('"START TRANSACTION"');
1176
1177
            try {
1178 9521
                $connection->beginTransaction();
1179 9521
            } finally {
1180 9521
                $logger->stopQuery();
1181
            }
1182 9412
        } elseif ($this->nestTransactionsWithSavepoints) {
1183 7970
            $logger->startQuery('"SAVEPOINT"');
1184 7970
            $this->createSavepoint($this->_getNestedTransactionSavePointName());
1185 7970
            $logger->stopQuery();
1186
        }
1187 9521
    }
1188
1189
    /**
1190
     * {@inheritDoc}
1191
     *
1192
     * @throws ConnectionException If the commit failed due to no active transaction or
1193
     *                                            because the transaction was marked for rollback only.
1194
     */
1195 9955
    public function commit() : void
1196
    {
1197 9955
        if ($this->transactionNestingLevel === 0) {
1198 9933
            throw NoActiveTransaction::new();
1199
        }
1200 9478
        if ($this->isRollbackOnly) {
1201 8022
            throw CommitFailedRollbackOnly::new();
1202
        }
1203
1204 9474
        $connection = $this->getWrappedConnection();
1205
1206 9474
        $logger = $this->_config->getSQLLogger();
1207
1208 9474
        if ($this->transactionNestingLevel === 1) {
1209 9474
            $logger->startQuery('"COMMIT"');
1210
1211
            try {
1212 9474
                $connection->commit();
1213 9474
            } finally {
1214 9474
                $logger->stopQuery();
1215
            }
1216 9410
        } elseif ($this->nestTransactionsWithSavepoints) {
1217 7970
            $logger->startQuery('"RELEASE SAVEPOINT"');
1218 7970
            $this->releaseSavepoint($this->_getNestedTransactionSavePointName());
1219 7970
            $logger->stopQuery();
1220
        }
1221
1222 9474
        --$this->transactionNestingLevel;
1223
1224 9474
        if ($this->autoCommit !== false || $this->transactionNestingLevel !== 0) {
1225 9422
            return;
1226
        }
1227
1228 9460
        $this->beginTransaction();
1229 9460
    }
1230
1231
    /**
1232
     * Commits all current nesting transactions.
1233
     *
1234
     * @throws ConnectionException
1235
     * @throws DriverException
1236
     */
1237 9408
    private function commitAll() : void
1238
    {
1239 9408
        while ($this->transactionNestingLevel !== 0) {
1240 9408
            if ($this->autoCommit === false && $this->transactionNestingLevel === 1) {
1241
                // When in no auto-commit mode, the last nesting commit immediately starts a new transaction.
1242
                // Therefore we need to do the final commit here and then leave to avoid an infinite loop.
1243 9408
                $this->commit();
1244
1245 9408
                return;
1246
            }
1247
1248 9408
            $this->commit();
1249
        }
1250 9408
    }
1251
1252
    /**
1253
     * {@inheritDoc}
1254
     *
1255
     * @throws ConnectionException If the rollback operation failed.
1256
     */
1257 9930
    public function rollBack() : void
1258
    {
1259 9930
        if ($this->transactionNestingLevel === 0) {
1260 9908
            throw NoActiveTransaction::new();
1261
        }
1262
1263 9453
        $connection = $this->getWrappedConnection();
1264
1265 9453
        $logger = $this->_config->getSQLLogger();
1266
1267 9453
        if ($this->transactionNestingLevel === 1) {
1268 9451
            $logger->startQuery('"ROLLBACK"');
1269 9451
            $this->transactionNestingLevel = 0;
1270
1271
            try {
1272 9451
                $connection->rollBack();
1273 9451
            } finally {
1274 9451
                $this->isRollbackOnly = false;
1275 9451
                $logger->stopQuery();
1276
1277 9451
                if ($this->autoCommit === false) {
1278 9451
                    $this->beginTransaction();
1279
                }
1280
            }
1281 7997
        } elseif ($this->nestTransactionsWithSavepoints) {
1282 7970
            $logger->startQuery('"ROLLBACK TO SAVEPOINT"');
1283 7970
            $this->rollbackSavepoint($this->_getNestedTransactionSavePointName());
1284 7970
            --$this->transactionNestingLevel;
1285 7970
            $logger->stopQuery();
1286
        } else {
1287 7995
            $this->isRollbackOnly = true;
1288 7995
            --$this->transactionNestingLevel;
1289
        }
1290 9453
    }
1291
1292
    /**
1293
     * Creates a new savepoint.
1294
     *
1295
     * @param string $savepoint The name of the savepoint to create.
1296
     *
1297
     * @return void
1298
     *
1299
     * @throws ConnectionException
1300
     */
1301 7970
    public function createSavepoint($savepoint)
1302
    {
1303 7970
        if (! $this->getDatabasePlatform()->supportsSavepoints()) {
1304
            throw SavepointsNotSupported::new();
1305
        }
1306
1307 7970
        $this->getWrappedConnection()->exec($this->platform->createSavePoint($savepoint));
1308 7970
    }
1309
1310
    /**
1311
     * Releases the given savepoint.
1312
     *
1313
     * @param string $savepoint The name of the savepoint to release.
1314
     *
1315
     * @return void
1316
     *
1317
     * @throws ConnectionException
1318
     */
1319 7970
    public function releaseSavepoint($savepoint)
1320
    {
1321 7970
        if (! $this->getDatabasePlatform()->supportsSavepoints()) {
1322
            throw SavepointsNotSupported::new();
1323
        }
1324
1325 7970
        if (! $this->platform->supportsReleaseSavepoints()) {
1326 2
            return;
1327
        }
1328
1329 7968
        $this->getWrappedConnection()->exec($this->platform->releaseSavePoint($savepoint));
1330 7968
    }
1331
1332
    /**
1333
     * Rolls back to the given savepoint.
1334
     *
1335
     * @param string $savepoint The name of the savepoint to rollback to.
1336
     *
1337
     * @return void
1338
     *
1339
     * @throws ConnectionException
1340
     */
1341 7970
    public function rollbackSavepoint($savepoint)
1342
    {
1343 7970
        if (! $this->getDatabasePlatform()->supportsSavepoints()) {
1344
            throw SavepointsNotSupported::new();
1345
        }
1346
1347 7970
        $this->getWrappedConnection()->exec($this->platform->rollbackSavePoint($savepoint));
1348 7970
    }
1349
1350
    /**
1351
     * Gets the wrapped driver connection.
1352
     *
1353
     * @return DriverConnection
1354
     */
1355 10353
    public function getWrappedConnection()
1356
    {
1357 10353
        $this->connect();
1358 10347
        assert($this->_conn instanceof DriverConnection);
1359
1360 10347
        return $this->_conn;
1361
    }
1362
1363
    /**
1364
     * Gets the SchemaManager that can be used to inspect or change the
1365
     * database schema through the connection.
1366
     *
1367
     * @return AbstractSchemaManager
1368
     */
1369 9185
    public function getSchemaManager()
1370
    {
1371 9185
        if ($this->_schemaManager === null) {
1372 8901
            $this->_schemaManager = $this->_driver->getSchemaManager($this);
1373
        }
1374
1375 9185
        return $this->_schemaManager;
1376
    }
1377
1378
    /**
1379
     * Marks the current transaction so that the only possible
1380
     * outcome for the transaction to be rolled back.
1381
     *
1382
     * @return void
1383
     *
1384
     * @throws ConnectionException If no transaction is active.
1385
     */
1386 9885
    public function setRollbackOnly()
1387
    {
1388 9885
        if ($this->transactionNestingLevel === 0) {
1389 9883
            throw NoActiveTransaction::new();
1390
        }
1391 8020
        $this->isRollbackOnly = true;
1392 8020
    }
1393
1394
    /**
1395
     * Checks whether the current transaction is marked for rollback only.
1396
     *
1397
     * @return bool
1398
     *
1399
     * @throws ConnectionException If no transaction is active.
1400
     */
1401 9887
    public function isRollbackOnly()
1402
    {
1403 9887
        if ($this->transactionNestingLevel === 0) {
1404 9883
            throw NoActiveTransaction::new();
1405
        }
1406
1407 7997
        return $this->isRollbackOnly;
1408
    }
1409
1410
    /**
1411
     * Converts a given value to its database representation according to the conversion
1412
     * rules of a specific DBAL mapping type.
1413
     *
1414
     * @param mixed  $value The value to convert.
1415
     * @param string $type  The name of the DBAL mapping type.
1416
     *
1417
     * @return mixed The converted value.
1418
     */
1419
    public function convertToDatabaseValue($value, $type)
1420
    {
1421
        return Type::getType($type)->convertToDatabaseValue($value, $this->getDatabasePlatform());
1422
    }
1423
1424
    /**
1425
     * Converts a given value to its PHP representation according to the conversion
1426
     * rules of a specific DBAL mapping type.
1427
     *
1428
     * @param mixed  $value The value to convert.
1429
     * @param string $type  The name of the DBAL mapping type.
1430
     *
1431
     * @return mixed The converted type.
1432
     */
1433
    public function convertToPHPValue($value, $type)
1434
    {
1435
        return Type::getType($type)->convertToPHPValue($value, $this->getDatabasePlatform());
1436
    }
1437
1438
    /**
1439
     * Binds a set of parameters, some or all of which are typed with a PDO binding type
1440
     * or DBAL mapping type, to a given statement.
1441
     *
1442
     * @internal Duck-typing used on the $stmt parameter to support driver statements as well as
1443
     *           raw PDOStatement instances.
1444
     *
1445
     * @param DriverStatement $stmt   The statement to bind the values to.
1446
     * @param mixed[]         $params The map/list of named/positional parameters.
1447
     * @param int[]|string[]  $types  The parameter types.
1448
     */
1449 8247
    private function _bindTypedValues(DriverStatement $stmt, array $params, array $types) : void
1450
    {
1451
        // Check whether parameters are positional or named. Mixing is not allowed, just like in PDO.
1452 8247
        if (is_int(key($params))) {
1453
            // Positional parameters
1454 8247
            $typeOffset = array_key_exists(0, $types) ? -1 : 0;
1455 8247
            $bindIndex  = 1;
1456 8247
            foreach ($params as $value) {
1457 8247
                $typeIndex = $bindIndex + $typeOffset;
1458 8247
                if (isset($types[$typeIndex])) {
1459 8247
                    $type                  = $types[$typeIndex];
1460 8247
                    [$value, $bindingType] = $this->getBindingInfo($value, $type);
1461 8247
                    $stmt->bindValue($bindIndex, $value, $bindingType);
1462
                } else {
1463 8095
                    $stmt->bindValue($bindIndex, $value);
1464
                }
1465 8247
                ++$bindIndex;
1466
            }
1467
        } else {
1468
            // Named parameters
1469
            foreach ($params as $name => $value) {
1470
                if (isset($types[$name])) {
1471
                    $type                  = $types[$name];
1472
                    [$value, $bindingType] = $this->getBindingInfo($value, $type);
1473
                    $stmt->bindValue($name, $value, $bindingType);
1474
                } else {
1475
                    $stmt->bindValue($name, $value);
1476
                }
1477
            }
1478
        }
1479 8247
    }
1480
1481
    /**
1482
     * Gets the binding type of a given type. The given type can be a PDO or DBAL mapping type.
1483
     *
1484
     * @param mixed           $value The value to bind.
1485
     * @param int|string|null $type  The type to bind (PDO or DBAL).
1486
     *
1487
     * @return mixed[] [0] => the (escaped) value, [1] => the binding type.
1488
     */
1489 8247
    private function getBindingInfo($value, $type)
1490
    {
1491 8247
        if (is_string($type)) {
1492 7492
            $type = Type::getType($type);
1493
        }
1494 8247
        if ($type instanceof Type) {
1495 7492
            $value       = $type->convertToDatabaseValue($value, $this->getDatabasePlatform());
1496 7492
            $bindingType = $type->getBindingType();
1497
        } else {
1498 8235
            $bindingType = $type;
1499
        }
1500
1501 8247
        return [$value, $bindingType];
1502
    }
1503
1504
    /**
1505
     * Resolves the parameters to a format which can be displayed.
1506
     *
1507
     * @internal This is a purely internal method. If you rely on this method, you are advised to
1508
     *           copy/paste the code as this method may change, or be removed without prior notice.
1509
     *
1510
     * @param mixed[]        $params
1511
     * @param int[]|string[] $types
1512
     *
1513
     * @return mixed[]
1514
     */
1515 9929
    public function resolveParams(array $params, array $types)
1516
    {
1517 9929
        $resolvedParams = [];
1518
1519
        // Check whether parameters are positional or named. Mixing is not allowed, just like in PDO.
1520 9929
        if (is_int(key($params))) {
1521
            // Positional parameters
1522 6686
            $typeOffset = array_key_exists(0, $types) ? -1 : 0;
1523 6686
            $bindIndex  = 1;
1524 6686
            foreach ($params as $value) {
1525 6686
                $typeIndex = $bindIndex + $typeOffset;
1526 6686
                if (isset($types[$typeIndex])) {
1527
                    $type                       = $types[$typeIndex];
1528
                    [$value]                    = $this->getBindingInfo($value, $type);
1529
                    $resolvedParams[$bindIndex] = $value;
1530
                } else {
1531 6686
                    $resolvedParams[$bindIndex] = $value;
1532
                }
1533 6686
                ++$bindIndex;
1534
            }
1535
        } else {
1536
            // Named parameters
1537 9913
            foreach ($params as $name => $value) {
1538
                if (isset($types[$name])) {
1539
                    $type                  = $types[$name];
1540
                    [$value]               = $this->getBindingInfo($value, $type);
1541
                    $resolvedParams[$name] = $value;
1542
                } else {
1543
                    $resolvedParams[$name] = $value;
1544
                }
1545
            }
1546
        }
1547
1548 9929
        return $resolvedParams;
1549
    }
1550
1551
    /**
1552
     * Creates a new instance of a SQL query builder.
1553
     *
1554
     * @return QueryBuilder
1555
     */
1556
    public function createQueryBuilder()
1557
    {
1558
        return new Query\QueryBuilder($this);
1559
    }
1560
1561
    /**
1562
     * Ping the server
1563
     *
1564
     * When the server is not available the method returns FALSE.
1565
     * It is responsibility of the developer to handle this case
1566
     * and abort the request or reconnect manually:
1567
     *
1568
     * @return bool
1569
     *
1570
     * @example
1571
     *
1572
     *   if ($conn->ping() === false) {
1573
     *      $conn->close();
1574
     *      $conn->connect();
1575
     *   }
1576
     *
1577
     * It is undefined if the underlying driver attempts to reconnect
1578
     * or disconnect when the connection is not available anymore
1579
     * as long it returns TRUE when a reconnect succeeded and
1580
     * FALSE when the connection was dropped.
1581
     */
1582 7770
    public function ping()
1583
    {
1584 7770
        $connection = $this->getWrappedConnection();
1585
1586 7770
        if ($connection instanceof PingableConnection) {
1587 2763
            return $connection->ping();
1588
        }
1589
1590
        try {
1591 5007
            $this->query($this->getDatabasePlatform()->getDummySelectSQL());
1592
1593 5007
            return true;
1594
        } catch (DBALException $e) {
1595
            return false;
1596
        }
1597
    }
1598
}
1599