Subversion-Projekte lars-tiefland.laravel_shop

Revision

Revision 148 | Zur aktuellen Revision | Details | Vergleich mit vorheriger | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
148 lars 1
<?php
2
 
3
namespace Doctrine\DBAL;
4
 
5
use Closure;
6
use Doctrine\Common\EventManager;
7
use Doctrine\DBAL\Cache\ArrayResult;
8
use Doctrine\DBAL\Cache\CacheException;
9
use Doctrine\DBAL\Cache\QueryCacheProfile;
10
use Doctrine\DBAL\Driver\API\ExceptionConverter;
11
use Doctrine\DBAL\Driver\Connection as DriverConnection;
12
use Doctrine\DBAL\Driver\ServerInfoAwareConnection;
13
use Doctrine\DBAL\Driver\Statement as DriverStatement;
14
use Doctrine\DBAL\Event\TransactionBeginEventArgs;
15
use Doctrine\DBAL\Event\TransactionCommitEventArgs;
16
use Doctrine\DBAL\Event\TransactionRollBackEventArgs;
17
use Doctrine\DBAL\Exception\ConnectionLost;
18
use Doctrine\DBAL\Exception\DriverException;
19
use Doctrine\DBAL\Exception\InvalidArgumentException;
20
use Doctrine\DBAL\Platforms\AbstractPlatform;
21
use Doctrine\DBAL\Query\Expression\ExpressionBuilder;
22
use Doctrine\DBAL\Query\QueryBuilder;
23
use Doctrine\DBAL\Schema\AbstractSchemaManager;
621 lars 24
use Doctrine\DBAL\Schema\DefaultSchemaManagerFactory;
25
use Doctrine\DBAL\Schema\LegacySchemaManagerFactory;
26
use Doctrine\DBAL\Schema\SchemaManagerFactory;
148 lars 27
use Doctrine\DBAL\SQL\Parser;
28
use Doctrine\DBAL\Types\Type;
29
use Doctrine\Deprecations\Deprecation;
30
use LogicException;
621 lars 31
use SensitiveParameter;
148 lars 32
use Throwable;
33
use Traversable;
34
 
35
use function array_key_exists;
36
use function assert;
37
use function count;
38
use function get_class;
39
use function implode;
40
use function is_int;
41
use function is_string;
42
use function key;
43
use function method_exists;
44
use function sprintf;
45
 
46
/**
47
 * A database abstraction-level connection that implements features like events, transaction isolation levels,
48
 * configuration, emulated transaction nesting, lazy connecting and more.
49
 *
50
 * @psalm-import-type Params from DriverManager
51
 * @psalm-consistent-constructor
52
 */
53
class Connection
54
{
55
    /**
56
     * Represents an array of ints to be expanded by Doctrine SQL parsing.
621 lars 57
     *
58
     * @deprecated Use {@see ArrayParameterType::INTEGER} instead.
148 lars 59
     */
621 lars 60
    public const PARAM_INT_ARRAY = ArrayParameterType::INTEGER;
148 lars 61
 
62
    /**
63
     * Represents an array of strings to be expanded by Doctrine SQL parsing.
621 lars 64
     *
65
     * @deprecated Use {@see ArrayParameterType::STRING} instead.
148 lars 66
     */
621 lars 67
    public const PARAM_STR_ARRAY = ArrayParameterType::STRING;
148 lars 68
 
69
    /**
70
     * Represents an array of ascii strings to be expanded by Doctrine SQL parsing.
621 lars 71
     *
72
     * @deprecated Use {@see ArrayParameterType::ASCII} instead.
148 lars 73
     */
621 lars 74
    public const PARAM_ASCII_STR_ARRAY = ArrayParameterType::ASCII;
148 lars 75
 
76
    /**
77
     * Offset by which PARAM_* constants are detected as arrays of the param type.
78
     *
79
     * @internal Should be used only within the wrapper layer.
80
     */
81
    public const ARRAY_PARAM_OFFSET = 100;
82
 
83
    /**
84
     * The wrapped driver connection.
85
     *
621 lars 86
     * @var DriverConnection|null
148 lars 87
     */
88
    protected $_conn;
89
 
90
    /** @var Configuration */
91
    protected $_config;
92
 
93
    /**
94
     * @deprecated
95
     *
96
     * @var EventManager
97
     */
98
    protected $_eventManager;
99
 
100
    /**
101
     * @deprecated Use {@see createExpressionBuilder()} instead.
102
     *
103
     * @var ExpressionBuilder
104
     */
105
    protected $_expr;
106
 
107
    /**
108
     * The current auto-commit mode of this connection.
109
     */
110
    private bool $autoCommit = true;
111
 
112
    /**
113
     * The transaction nesting level.
114
     */
115
    private int $transactionNestingLevel = 0;
116
 
117
    /**
118
     * The currently active transaction isolation level or NULL before it has been determined.
119
     *
120
     * @var TransactionIsolationLevel::*|null
121
     */
122
    private $transactionIsolationLevel;
123
 
124
    /**
125
     * If nested transactions should use savepoints.
126
     */
127
    private bool $nestTransactionsWithSavepoints = false;
128
 
129
    /**
130
     * The parameters used during creation of the Connection instance.
131
     *
132
     * @var array<string,mixed>
133
     * @psalm-var Params
134
     */
135
    private array $params;
136
 
137
    /**
138
     * The database platform object used by the connection or NULL before it's initialized.
139
     */
140
    private ?AbstractPlatform $platform = null;
141
 
142
    private ?ExceptionConverter $exceptionConverter = null;
143
    private ?Parser $parser                         = null;
144
 
145
    /**
146
     * The schema manager.
147
     *
148
     * @deprecated Use {@see createSchemaManager()} instead.
149
     *
150
     * @var AbstractSchemaManager|null
151
     */
152
    protected $_schemaManager;
153
 
154
    /**
155
     * The used DBAL driver.
156
     *
157
     * @var Driver
158
     */
159
    protected $_driver;
160
 
161
    /**
162
     * Flag that indicates whether the current transaction is marked for rollback only.
163
     */
164
    private bool $isRollbackOnly = false;
165
 
621 lars 166
    private SchemaManagerFactory $schemaManagerFactory;
167
 
148 lars 168
    /**
169
     * Initializes a new instance of the Connection class.
170
     *
171
     * @internal The connection can be only instantiated by the driver manager.
172
     *
173
     * @param array<string,mixed> $params       The connection parameters.
174
     * @param Driver              $driver       The driver to use.
175
     * @param Configuration|null  $config       The configuration, optional.
176
     * @param EventManager|null   $eventManager The event manager, optional.
177
     * @psalm-param Params $params
178
     *
179
     * @throws Exception
180
     */
181
    public function __construct(
621 lars 182
        #[SensitiveParameter]
148 lars 183
        array $params,
184
        Driver $driver,
185
        ?Configuration $config = null,
186
        ?EventManager $eventManager = null
187
    ) {
188
        $this->_driver = $driver;
189
        $this->params  = $params;
190
 
191
        // Create default config and event manager if none given
192
        $config       ??= new Configuration();
193
        $eventManager ??= new EventManager();
194
 
195
        $this->_config       = $config;
196
        $this->_eventManager = $eventManager;
197
 
198
        if (isset($params['platform'])) {
199
            if (! $params['platform'] instanceof Platforms\AbstractPlatform) {
200
                throw Exception::invalidPlatformType($params['platform']);
201
            }
202
 
203
            Deprecation::trigger(
204
                'doctrine/dbal',
205
                'https://github.com/doctrine/dbal/pull/5699',
206
                'The "platform" connection parameter is deprecated.'
207
                    . ' Use a driver middleware that would instantiate the platform instead.',
208
            );
209
 
210
            $this->platform = $params['platform'];
211
            $this->platform->setEventManager($this->_eventManager);
212
        }
213
 
214
        $this->_expr = $this->createExpressionBuilder();
215
 
216
        $this->autoCommit = $config->getAutoCommit();
621 lars 217
 
218
        $schemaManagerFactory = $config->getSchemaManagerFactory();
219
        if ($schemaManagerFactory === null) {
220
            Deprecation::trigger(
221
                'doctrine/dbal',
222
                'https://github.com/doctrine/dbal/issues/5812',
223
                'Not configuring a schema manager factory is deprecated.'
224
                    . ' Use %s which is going to be the default in DBAL 4.',
225
                DefaultSchemaManagerFactory::class,
226
            );
227
 
228
            $schemaManagerFactory = new LegacySchemaManagerFactory();
229
        }
230
 
231
        $this->schemaManagerFactory = $schemaManagerFactory;
148 lars 232
    }
233
 
234
    /**
235
     * Gets the parameters used during instantiation.
236
     *
237
     * @internal
238
     *
239
     * @return array<string,mixed>
240
     * @psalm-return Params
241
     */
242
    public function getParams()
243
    {
244
        return $this->params;
245
    }
246
 
247
    /**
248
     * Gets the name of the currently selected database.
249
     *
250
     * @return string|null The name of the database or NULL if a database is not selected.
251
     *                     The platforms which don't support the concept of a database (e.g. embedded databases)
252
     *                     must always return a string as an indicator of an implicitly selected database.
253
     *
254
     * @throws Exception
255
     */
256
    public function getDatabase()
257
    {
258
        $platform = $this->getDatabasePlatform();
259
        $query    = $platform->getDummySelectSQL($platform->getCurrentDatabaseExpression());
260
        $database = $this->fetchOne($query);
261
 
262
        assert(is_string($database) || $database === null);
263
 
264
        return $database;
265
    }
266
 
267
    /**
268
     * Gets the DBAL driver instance.
269
     *
270
     * @return Driver
271
     */
272
    public function getDriver()
273
    {
274
        return $this->_driver;
275
    }
276
 
277
    /**
278
     * Gets the Configuration used by the Connection.
279
     *
280
     * @return Configuration
281
     */
282
    public function getConfiguration()
283
    {
284
        return $this->_config;
285
    }
286
 
287
    /**
288
     * Gets the EventManager used by the Connection.
289
     *
290
     * @deprecated
291
     *
292
     * @return EventManager
293
     */
294
    public function getEventManager()
295
    {
296
        Deprecation::triggerIfCalledFromOutside(
297
            'doctrine/dbal',
298
            'https://github.com/doctrine/dbal/issues/5784',
299
            '%s is deprecated.',
300
            __METHOD__,
301
        );
302
 
303
        return $this->_eventManager;
304
    }
305
 
306
    /**
307
     * Gets the DatabasePlatform for the connection.
308
     *
309
     * @return AbstractPlatform
310
     *
311
     * @throws Exception
312
     */
313
    public function getDatabasePlatform()
314
    {
315
        if ($this->platform === null) {
316
            $this->platform = $this->detectDatabasePlatform();
317
            $this->platform->setEventManager($this->_eventManager);
318
        }
319
 
320
        return $this->platform;
321
    }
322
 
323
    /**
324
     * Creates an expression builder for the connection.
325
     */
326
    public function createExpressionBuilder(): ExpressionBuilder
327
    {
328
        return new ExpressionBuilder($this);
329
    }
330
 
331
    /**
332
     * Gets the ExpressionBuilder for the connection.
333
     *
334
     * @deprecated Use {@see createExpressionBuilder()} instead.
335
     *
336
     * @return ExpressionBuilder
337
     */
338
    public function getExpressionBuilder()
339
    {
340
        Deprecation::triggerIfCalledFromOutside(
341
            'doctrine/dbal',
342
            'https://github.com/doctrine/dbal/issues/4515',
343
            'Connection::getExpressionBuilder() is deprecated,'
344
                . ' use Connection::createExpressionBuilder() instead.',
345
        );
346
 
347
        return $this->_expr;
348
    }
349
 
350
    /**
351
     * Establishes the connection with the database.
352
     *
353
     * @internal This method will be made protected in DBAL 4.0.
354
     *
355
     * @return bool TRUE if the connection was successfully established, FALSE if
356
     *              the connection is already open.
357
     *
358
     * @throws Exception
621 lars 359
     *
360
     * @psalm-assert !null $this->_conn
148 lars 361
     */
362
    public function connect()
363
    {
364
        Deprecation::triggerIfCalledFromOutside(
365
            'doctrine/dbal',
366
            'https://github.com/doctrine/dbal/issues/4966',
367
            'Public access to Connection::connect() is deprecated.',
368
        );
369
 
370
        if ($this->_conn !== null) {
371
            return false;
372
        }
373
 
374
        try {
375
            $this->_conn = $this->_driver->connect($this->params);
376
        } catch (Driver\Exception $e) {
377
            throw $this->convertException($e);
378
        }
379
 
380
        if ($this->autoCommit === false) {
381
            $this->beginTransaction();
382
        }
383
 
384
        if ($this->_eventManager->hasListeners(Events::postConnect)) {
385
            Deprecation::trigger(
386
                'doctrine/dbal',
387
                'https://github.com/doctrine/dbal/issues/5784',
388
                'Subscribing to %s events is deprecated. Implement a middleware instead.',
389
                Events::postConnect,
390
            );
391
 
392
            $eventArgs = new Event\ConnectionEventArgs($this);
393
            $this->_eventManager->dispatchEvent(Events::postConnect, $eventArgs);
394
        }
395
 
396
        return true;
397
    }
398
 
399
    /**
400
     * Detects and sets the database platform.
401
     *
402
     * Evaluates custom platform class and version in order to set the correct platform.
403
     *
404
     * @throws Exception If an invalid platform was specified for this connection.
405
     */
406
    private function detectDatabasePlatform(): AbstractPlatform
407
    {
408
        $version = $this->getDatabasePlatformVersion();
409
 
410
        if ($version !== null) {
411
            assert($this->_driver instanceof VersionAwarePlatformDriver);
412
 
413
            return $this->_driver->createDatabasePlatformForVersion($version);
414
        }
415
 
416
        return $this->_driver->getDatabasePlatform();
417
    }
418
 
419
    /**
420
     * Returns the version of the related platform if applicable.
421
     *
422
     * Returns null if either the driver is not capable to create version
423
     * specific platform instances, no explicit server version was specified
424
     * or the underlying driver connection cannot determine the platform
425
     * version without having to query it (performance reasons).
426
     *
427
     * @return string|null
428
     *
429
     * @throws Throwable
430
     */
431
    private function getDatabasePlatformVersion()
432
    {
433
        // Driver does not support version specific platforms.
434
        if (! $this->_driver instanceof VersionAwarePlatformDriver) {
435
            return null;
436
        }
437
 
438
        // Explicit platform version requested (supersedes auto-detection).
439
        if (isset($this->params['serverVersion'])) {
440
            return $this->params['serverVersion'];
441
        }
442
 
443
        // If not connected, we need to connect now to determine the platform version.
444
        if ($this->_conn === null) {
445
            try {
446
                $this->connect();
447
            } catch (Exception $originalException) {
448
                if (! isset($this->params['dbname'])) {
449
                    throw $originalException;
450
                }
451
 
452
                Deprecation::trigger(
453
                    'doctrine/dbal',
454
                    'https://github.com/doctrine/dbal/pull/5707',
455
                    'Relying on a fallback connection used to determine the database platform while connecting'
456
                        . ' to a non-existing database is deprecated. Either use an existing database name in'
457
                        . ' connection parameters or omit the database name if the platform'
458
                        . ' and the server configuration allow that.',
459
                );
460
 
461
                // The database to connect to might not yet exist.
462
                // Retry detection without database name connection parameter.
463
                $params = $this->params;
464
 
465
                unset($this->params['dbname']);
466
 
467
                try {
468
                    $this->connect();
469
                } catch (Exception $fallbackException) {
470
                    // Either the platform does not support database-less connections
471
                    // or something else went wrong.
472
                    throw $originalException;
473
                } finally {
474
                    $this->params = $params;
475
                }
476
 
477
                $serverVersion = $this->getServerVersion();
478
 
479
                // Close "temporary" connection to allow connecting to the real database again.
480
                $this->close();
481
 
482
                return $serverVersion;
483
            }
484
        }
485
 
486
        return $this->getServerVersion();
487
    }
488
 
489
    /**
490
     * Returns the database server version if the underlying driver supports it.
491
     *
492
     * @return string|null
493
     *
494
     * @throws Exception
495
     */
496
    private function getServerVersion()
497
    {
498
        $connection = $this->getWrappedConnection();
499
 
500
        // Automatic platform version detection.
501
        if ($connection instanceof ServerInfoAwareConnection) {
502
            try {
503
                return $connection->getServerVersion();
504
            } catch (Driver\Exception $e) {
505
                throw $this->convertException($e);
506
            }
507
        }
508
 
509
        Deprecation::trigger(
510
            'doctrine/dbal',
511
            'https://github.com/doctrine/dbal/pull/4750',
512
            'Not implementing the ServerInfoAwareConnection interface in %s is deprecated',
513
            get_class($connection),
514
        );
515
 
516
        // Unable to detect platform version.
517
        return null;
518
    }
519
 
520
    /**
521
     * Returns the current auto-commit mode for this connection.
522
     *
523
     * @see    setAutoCommit
524
     *
525
     * @return bool True if auto-commit mode is currently enabled for this connection, false otherwise.
526
     */
527
    public function isAutoCommit()
528
    {
529
        return $this->autoCommit === true;
530
    }
531
 
532
    /**
533
     * Sets auto-commit mode for this connection.
534
     *
535
     * If a connection is in auto-commit mode, then all its SQL statements will be executed and committed as individual
536
     * transactions. Otherwise, its SQL statements are grouped into transactions that are terminated by a call to either
537
     * the method commit or the method rollback. By default, new connections are in auto-commit mode.
538
     *
539
     * NOTE: If this method is called during a transaction and the auto-commit mode is changed, the transaction is
540
     * committed. If this method is called and the auto-commit mode is not changed, the call is a no-op.
541
     *
542
     * @see   isAutoCommit
543
     *
544
     * @param bool $autoCommit True to enable auto-commit mode; false to disable it.
545
     *
546
     * @return void
547
     */
548
    public function setAutoCommit($autoCommit)
549
    {
550
        $autoCommit = (bool) $autoCommit;
551
 
552
        // Mode not changed, no-op.
553
        if ($autoCommit === $this->autoCommit) {
554
            return;
555
        }
556
 
557
        $this->autoCommit = $autoCommit;
558
 
559
        // Commit all currently active transactions if any when switching auto-commit mode.
560
        if ($this->_conn === null || $this->transactionNestingLevel === 0) {
561
            return;
562
        }
563
 
564
        $this->commitAll();
565
    }
566
 
567
    /**
568
     * Prepares and executes an SQL query and returns the first row of the result
569
     * as an associative array.
570
     *
571
     * @param string                                                               $query  SQL query
572
     * @param list<mixed>|array<string, mixed>                                     $params Query parameters
573
     * @param array<int, int|string|Type|null>|array<string, int|string|Type|null> $types  Parameter types
574
     *
575
     * @return array<string, mixed>|false False is returned if no rows are found.
576
     *
577
     * @throws Exception
578
     */
579
    public function fetchAssociative(string $query, array $params = [], array $types = [])
580
    {
581
        return $this->executeQuery($query, $params, $types)->fetchAssociative();
582
    }
583
 
584
    /**
585
     * Prepares and executes an SQL query and returns the first row of the result
586
     * as a numerically indexed array.
587
     *
588
     * @param string                                                               $query  SQL query
589
     * @param list<mixed>|array<string, mixed>                                     $params Query parameters
590
     * @param array<int, int|string|Type|null>|array<string, int|string|Type|null> $types  Parameter types
591
     *
592
     * @return list<mixed>|false False is returned if no rows are found.
593
     *
594
     * @throws Exception
595
     */
596
    public function fetchNumeric(string $query, array $params = [], array $types = [])
597
    {
598
        return $this->executeQuery($query, $params, $types)->fetchNumeric();
599
    }
600
 
601
    /**
602
     * Prepares and executes an SQL query and returns the value of a single column
603
     * of the first row of the result.
604
     *
605
     * @param string                                                               $query  SQL query
606
     * @param list<mixed>|array<string, mixed>                                     $params Query parameters
607
     * @param array<int, int|string|Type|null>|array<string, int|string|Type|null> $types  Parameter types
608
     *
609
     * @return mixed|false False is returned if no rows are found.
610
     *
611
     * @throws Exception
612
     */
613
    public function fetchOne(string $query, array $params = [], array $types = [])
614
    {
615
        return $this->executeQuery($query, $params, $types)->fetchOne();
616
    }
617
 
618
    /**
619
     * Whether an actual connection to the database is established.
620
     *
621
     * @return bool
622
     */
623
    public function isConnected()
624
    {
625
        return $this->_conn !== null;
626
    }
627
 
628
    /**
629
     * Checks whether a transaction is currently active.
630
     *
631
     * @return bool TRUE if a transaction is currently active, FALSE otherwise.
632
     */
633
    public function isTransactionActive()
634
    {
635
        return $this->transactionNestingLevel > 0;
636
    }
637
 
638
    /**
639
     * Adds condition based on the criteria to the query components
640
     *
641
     * @param array<string,mixed> $criteria   Map of key columns to their values
642
     * @param string[]            $columns    Column names
643
     * @param mixed[]             $values     Column values
644
     * @param string[]            $conditions Key conditions
645
     *
646
     * @throws Exception
647
     */
648
    private function addCriteriaCondition(
649
        array $criteria,
650
        array &$columns,
651
        array &$values,
652
        array &$conditions
653
    ): void {
654
        $platform = $this->getDatabasePlatform();
655
 
656
        foreach ($criteria as $columnName => $value) {
657
            if ($value === null) {
658
                $conditions[] = $platform->getIsNullExpression($columnName);
659
                continue;
660
            }
661
 
662
            $columns[]    = $columnName;
663
            $values[]     = $value;
664
            $conditions[] = $columnName . ' = ?';
665
        }
666
    }
667
 
668
    /**
669
     * Executes an SQL DELETE statement on a table.
670
     *
671
     * Table expression and columns are not escaped and are not safe for user-input.
672
     *
673
     * @param string                                                               $table    Table name
674
     * @param array<string, mixed>                                                 $criteria Deletion criteria
675
     * @param array<int, int|string|Type|null>|array<string, int|string|Type|null> $types    Parameter types
676
     *
677
     * @return int|string The number of affected rows.
678
     *
679
     * @throws Exception
680
     */
681
    public function delete($table, array $criteria, array $types = [])
682
    {
683
        if (count($criteria) === 0) {
684
            throw InvalidArgumentException::fromEmptyCriteria();
685
        }
686
 
687
        $columns = $values = $conditions = [];
688
 
689
        $this->addCriteriaCondition($criteria, $columns, $values, $conditions);
690
 
691
        return $this->executeStatement(
692
            'DELETE FROM ' . $table . ' WHERE ' . implode(' AND ', $conditions),
693
            $values,
694
            is_string(key($types)) ? $this->extractTypeValues($columns, $types) : $types,
695
        );
696
    }
697
 
698
    /**
699
     * Closes the connection.
700
     *
701
     * @return void
702
     */
703
    public function close()
704
    {
705
        $this->_conn                   = null;
706
        $this->transactionNestingLevel = 0;
707
    }
708
 
709
    /**
710
     * Sets the transaction isolation level.
711
     *
712
     * @param TransactionIsolationLevel::* $level The level to set.
713
     *
714
     * @return int|string
715
     *
716
     * @throws Exception
717
     */
718
    public function setTransactionIsolation($level)
719
    {
720
        $this->transactionIsolationLevel = $level;
721
 
722
        return $this->executeStatement($this->getDatabasePlatform()->getSetTransactionIsolationSQL($level));
723
    }
724
 
725
    /**
726
     * Gets the currently active transaction isolation level.
727
     *
728
     * @return TransactionIsolationLevel::* The current transaction isolation level.
729
     *
730
     * @throws Exception
731
     */
732
    public function getTransactionIsolation()
733
    {
734
        return $this->transactionIsolationLevel ??= $this->getDatabasePlatform()->getDefaultTransactionIsolationLevel();
735
    }
736
 
737
    /**
738
     * Executes an SQL UPDATE statement on a table.
739
     *
740
     * Table expression and columns are not escaped and are not safe for user-input.
741
     *
742
     * @param string                                                               $table    Table name
743
     * @param array<string, mixed>                                                 $data     Column-value pairs
744
     * @param array<string, mixed>                                                 $criteria Update criteria
745
     * @param array<int, int|string|Type|null>|array<string, int|string|Type|null> $types    Parameter types
746
     *
747
     * @return int|string The number of affected rows.
748
     *
749
     * @throws Exception
750
     */
751
    public function update($table, array $data, array $criteria, array $types = [])
752
    {
753
        $columns = $values = $conditions = $set = [];
754
 
755
        foreach ($data as $columnName => $value) {
756
            $columns[] = $columnName;
757
            $values[]  = $value;
758
            $set[]     = $columnName . ' = ?';
759
        }
760
 
761
        $this->addCriteriaCondition($criteria, $columns, $values, $conditions);
762
 
763
        if (is_string(key($types))) {
764
            $types = $this->extractTypeValues($columns, $types);
765
        }
766
 
767
        $sql = 'UPDATE ' . $table . ' SET ' . implode(', ', $set)
768
                . ' WHERE ' . implode(' AND ', $conditions);
769
 
770
        return $this->executeStatement($sql, $values, $types);
771
    }
772
 
773
    /**
774
     * Inserts a table row with specified data.
775
     *
776
     * Table expression and columns are not escaped and are not safe for user-input.
777
     *
778
     * @param string                                                               $table Table name
779
     * @param array<string, mixed>                                                 $data  Column-value pairs
780
     * @param array<int, int|string|Type|null>|array<string, int|string|Type|null> $types Parameter types
781
     *
782
     * @return int|string The number of affected rows.
783
     *
784
     * @throws Exception
785
     */
786
    public function insert($table, array $data, array $types = [])
787
    {
788
        if (count($data) === 0) {
789
            return $this->executeStatement('INSERT INTO ' . $table . ' () VALUES ()');
790
        }
791
 
792
        $columns = [];
793
        $values  = [];
794
        $set     = [];
795
 
796
        foreach ($data as $columnName => $value) {
797
            $columns[] = $columnName;
798
            $values[]  = $value;
799
            $set[]     = '?';
800
        }
801
 
802
        return $this->executeStatement(
803
            'INSERT INTO ' . $table . ' (' . implode(', ', $columns) . ')' .
804
            ' VALUES (' . implode(', ', $set) . ')',
805
            $values,
806
            is_string(key($types)) ? $this->extractTypeValues($columns, $types) : $types,
807
        );
808
    }
809
 
810
    /**
811
     * Extract ordered type list from an ordered column list and type map.
812
     *
813
     * @param array<int, string>                                                   $columnList
814
     * @param array<int, int|string|Type|null>|array<string, int|string|Type|null> $types
815
     *
816
     * @return array<int, int|string|Type|null>|array<string, int|string|Type|null>
817
     */
818
    private function extractTypeValues(array $columnList, array $types): array
819
    {
820
        $typeValues = [];
821
 
822
        foreach ($columnList as $columnName) {
823
            $typeValues[] = $types[$columnName] ?? ParameterType::STRING;
824
        }
825
 
826
        return $typeValues;
827
    }
828
 
829
    /**
830
     * Quotes a string so it can be safely used as a table or column name, even if
831
     * it is a reserved name.
832
     *
833
     * Delimiting style depends on the underlying database platform that is being used.
834
     *
835
     * NOTE: Just because you CAN use quoted identifiers does not mean
836
     * you SHOULD use them. In general, they end up causing way more
837
     * problems than they solve.
838
     *
839
     * @param string $str The name to be quoted.
840
     *
841
     * @return string The quoted name.
842
     */
843
    public function quoteIdentifier($str)
844
    {
845
        return $this->getDatabasePlatform()->quoteIdentifier($str);
846
    }
847
 
848
    /**
849
     * The usage of this method is discouraged. Use prepared statements
850
     * or {@see AbstractPlatform::quoteStringLiteral()} instead.
851
     *
852
     * @param mixed                $value
853
     * @param int|string|Type|null $type
854
     *
855
     * @return mixed
856
     */
857
    public function quote($value, $type = ParameterType::STRING)
858
    {
859
        $connection = $this->getWrappedConnection();
860
 
861
        [$value, $bindingType] = $this->getBindingInfo($value, $type);
862
 
863
        return $connection->quote($value, $bindingType);
864
    }
865
 
866
    /**
867
     * Prepares and executes an SQL query and returns the result as an array of numeric arrays.
868
     *
869
     * @param string                                                               $query  SQL query
870
     * @param list<mixed>|array<string, mixed>                                     $params Query parameters
871
     * @param array<int, int|string|Type|null>|array<string, int|string|Type|null> $types  Parameter types
872
     *
873
     * @return list<list<mixed>>
874
     *
875
     * @throws Exception
876
     */
877
    public function fetchAllNumeric(string $query, array $params = [], array $types = []): array
878
    {
879
        return $this->executeQuery($query, $params, $types)->fetchAllNumeric();
880
    }
881
 
882
    /**
883
     * Prepares and executes an SQL query and returns the result as an array of associative arrays.
884
     *
885
     * @param string                                                               $query  SQL query
886
     * @param list<mixed>|array<string, mixed>                                     $params Query parameters
887
     * @param array<int, int|string|Type|null>|array<string, int|string|Type|null> $types  Parameter types
888
     *
889
     * @return list<array<string,mixed>>
890
     *
891
     * @throws Exception
892
     */
893
    public function fetchAllAssociative(string $query, array $params = [], array $types = []): array
894
    {
895
        return $this->executeQuery($query, $params, $types)->fetchAllAssociative();
896
    }
897
 
898
    /**
899
     * Prepares and executes an SQL query and returns the result as an associative array with the keys
900
     * mapped to the first column and the values mapped to the second column.
901
     *
902
     * @param string                                                               $query  SQL query
903
     * @param list<mixed>|array<string, mixed>                                     $params Query parameters
904
     * @param array<int, int|string|Type|null>|array<string, int|string|Type|null> $types  Parameter types
905
     *
906
     * @return array<mixed,mixed>
907
     *
908
     * @throws Exception
909
     */
910
    public function fetchAllKeyValue(string $query, array $params = [], array $types = []): array
911
    {
912
        return $this->executeQuery($query, $params, $types)->fetchAllKeyValue();
913
    }
914
 
915
    /**
916
     * Prepares and executes an SQL query and returns the result as an associative array with the keys mapped
917
     * to the first column and the values being an associative array representing the rest of the columns
918
     * and their values.
919
     *
920
     * @param string                                                               $query  SQL query
921
     * @param list<mixed>|array<string, mixed>                                     $params Query parameters
922
     * @param array<int, int|string|Type|null>|array<string, int|string|Type|null> $types  Parameter types
923
     *
924
     * @return array<mixed,array<string,mixed>>
925
     *
926
     * @throws Exception
927
     */
928
    public function fetchAllAssociativeIndexed(string $query, array $params = [], array $types = []): array
929
    {
930
        return $this->executeQuery($query, $params, $types)->fetchAllAssociativeIndexed();
931
    }
932
 
933
    /**
934
     * Prepares and executes an SQL query and returns the result as an array of the first column values.
935
     *
936
     * @param string                                                               $query  SQL query
937
     * @param list<mixed>|array<string, mixed>                                     $params Query parameters
938
     * @param array<int, int|string|Type|null>|array<string, int|string|Type|null> $types  Parameter types
939
     *
940
     * @return list<mixed>
941
     *
942
     * @throws Exception
943
     */
944
    public function fetchFirstColumn(string $query, array $params = [], array $types = []): array
945
    {
946
        return $this->executeQuery($query, $params, $types)->fetchFirstColumn();
947
    }
948
 
949
    /**
950
     * Prepares and executes an SQL query and returns the result as an iterator over rows represented as numeric arrays.
951
     *
952
     * @param string                                                               $query  SQL query
953
     * @param list<mixed>|array<string, mixed>                                     $params Query parameters
954
     * @param array<int, int|string|Type|null>|array<string, int|string|Type|null> $types  Parameter types
955
     *
956
     * @return Traversable<int,list<mixed>>
957
     *
958
     * @throws Exception
959
     */
960
    public function iterateNumeric(string $query, array $params = [], array $types = []): Traversable
961
    {
962
        return $this->executeQuery($query, $params, $types)->iterateNumeric();
963
    }
964
 
965
    /**
966
     * Prepares and executes an SQL query and returns the result as an iterator over rows represented
967
     * as associative arrays.
968
     *
969
     * @param string                                                               $query  SQL query
970
     * @param list<mixed>|array<string, mixed>                                     $params Query parameters
971
     * @param array<int, int|string|Type|null>|array<string, int|string|Type|null> $types  Parameter types
972
     *
973
     * @return Traversable<int,array<string,mixed>>
974
     *
975
     * @throws Exception
976
     */
977
    public function iterateAssociative(string $query, array $params = [], array $types = []): Traversable
978
    {
979
        return $this->executeQuery($query, $params, $types)->iterateAssociative();
980
    }
981
 
982
    /**
983
     * Prepares and executes an SQL query and returns the result as an iterator with the keys
984
     * mapped to the first column and the values mapped to the second column.
985
     *
986
     * @param string                                                               $query  SQL query
987
     * @param list<mixed>|array<string, mixed>                                     $params Query parameters
988
     * @param array<int, int|string|Type|null>|array<string, int|string|Type|null> $types  Parameter types
989
     *
990
     * @return Traversable<mixed,mixed>
991
     *
992
     * @throws Exception
993
     */
994
    public function iterateKeyValue(string $query, array $params = [], array $types = []): Traversable
995
    {
996
        return $this->executeQuery($query, $params, $types)->iterateKeyValue();
997
    }
998
 
999
    /**
1000
     * Prepares and executes an SQL query and returns the result as an iterator with the keys mapped
1001
     * to the first column and the values being an associative array representing the rest of the columns
1002
     * and their values.
1003
     *
1004
     * @param string                                           $query  SQL query
1005
     * @param list<mixed>|array<string, mixed>                 $params Query parameters
1006
     * @param array<int, int|string>|array<string, int|string> $types  Parameter types
1007
     *
1008
     * @return Traversable<mixed,array<string,mixed>>
1009
     *
1010
     * @throws Exception
1011
     */
1012
    public function iterateAssociativeIndexed(string $query, array $params = [], array $types = []): Traversable
1013
    {
1014
        return $this->executeQuery($query, $params, $types)->iterateAssociativeIndexed();
1015
    }
1016
 
1017
    /**
1018
     * Prepares and executes an SQL query and returns the result as an iterator over the first column values.
1019
     *
1020
     * @param string                                                               $query  SQL query
1021
     * @param list<mixed>|array<string, mixed>                                     $params Query parameters
1022
     * @param array<int, int|string|Type|null>|array<string, int|string|Type|null> $types  Parameter types
1023
     *
1024
     * @return Traversable<int,mixed>
1025
     *
1026
     * @throws Exception
1027
     */
1028
    public function iterateColumn(string $query, array $params = [], array $types = []): Traversable
1029
    {
1030
        return $this->executeQuery($query, $params, $types)->iterateColumn();
1031
    }
1032
 
1033
    /**
1034
     * Prepares an SQL statement.
1035
     *
1036
     * @param string $sql The SQL statement to prepare.
1037
     *
1038
     * @throws Exception
1039
     */
1040
    public function prepare(string $sql): Statement
1041
    {
1042
        $connection = $this->getWrappedConnection();
1043
 
1044
        try {
1045
            $statement = $connection->prepare($sql);
1046
        } catch (Driver\Exception $e) {
1047
            throw $this->convertExceptionDuringQuery($e, $sql);
1048
        }
1049
 
1050
        return new Statement($this, $statement, $sql);
1051
    }
1052
 
1053
    /**
1054
     * Executes an, optionally parametrized, SQL query.
1055
     *
1056
     * If the query is parametrized, a prepared statement is used.
1057
     * If an SQLLogger is configured, the execution is logged.
1058
     *
1059
     * @param string                                                               $sql    SQL query
1060
     * @param list<mixed>|array<string, mixed>                                     $params Query parameters
1061
     * @param array<int, int|string|Type|null>|array<string, int|string|Type|null> $types  Parameter types
1062
     *
1063
     * @throws Exception
1064
     */
1065
    public function executeQuery(
1066
        string $sql,
1067
        array $params = [],
1068
        $types = [],
1069
        ?QueryCacheProfile $qcp = null
1070
    ): Result {
1071
        if ($qcp !== null) {
1072
            return $this->executeCacheQuery($sql, $params, $types, $qcp);
1073
        }
1074
 
1075
        $connection = $this->getWrappedConnection();
1076
 
1077
        $logger = $this->_config->getSQLLogger();
1078
        if ($logger !== null) {
1079
            $logger->startQuery($sql, $params, $types);
1080
        }
1081
 
1082
        try {
1083
            if (count($params) > 0) {
1084
                if ($this->needsArrayParameterConversion($params, $types)) {
1085
                    [$sql, $params, $types] = $this->expandArrayParameters($sql, $params, $types);
1086
                }
1087
 
1088
                $stmt = $connection->prepare($sql);
1089
 
1090
                $this->bindParameters($stmt, $params, $types);
1091
 
1092
                $result = $stmt->execute();
1093
            } else {
1094
                $result = $connection->query($sql);
1095
            }
1096
 
1097
            return new Result($result, $this);
1098
        } catch (Driver\Exception $e) {
1099
            throw $this->convertExceptionDuringQuery($e, $sql, $params, $types);
1100
        } finally {
1101
            if ($logger !== null) {
1102
                $logger->stopQuery();
1103
            }
1104
        }
1105
    }
1106
 
1107
    /**
1108
     * Executes a caching query.
1109
     *
1110
     * @param string                                                               $sql    SQL query
1111
     * @param list<mixed>|array<string, mixed>                                     $params Query parameters
1112
     * @param array<int, int|string|Type|null>|array<string, int|string|Type|null> $types  Parameter types
1113
     *
1114
     * @throws CacheException
1115
     * @throws Exception
1116
     */
1117
    public function executeCacheQuery($sql, $params, $types, QueryCacheProfile $qcp): Result
1118
    {
1119
        $resultCache = $qcp->getResultCache() ?? $this->_config->getResultCache();
1120
 
1121
        if ($resultCache === null) {
1122
            throw CacheException::noResultDriverConfigured();
1123
        }
1124
 
1125
        $connectionParams = $this->params;
621 lars 1126
        unset($connectionParams['platform'], $connectionParams['password'], $connectionParams['url']);
148 lars 1127
 
1128
        [$cacheKey, $realKey] = $qcp->generateCacheKeys($sql, $params, $types, $connectionParams);
1129
 
1130
        $item = $resultCache->getItem($cacheKey);
1131
 
1132
        if ($item->isHit()) {
1133
            $value = $item->get();
1134
            if (isset($value[$realKey])) {
1135
                return new Result(new ArrayResult($value[$realKey]), $this);
1136
            }
1137
        } else {
1138
            $value = [];
1139
        }
1140
 
1141
        $data = $this->fetchAllAssociative($sql, $params, $types);
1142
 
1143
        $value[$realKey] = $data;
1144
 
1145
        $item->set($value);
1146
 
1147
        $lifetime = $qcp->getLifetime();
1148
        if ($lifetime > 0) {
1149
            $item->expiresAfter($lifetime);
1150
        }
1151
 
1152
        $resultCache->save($item);
1153
 
1154
        return new Result(new ArrayResult($data), $this);
1155
    }
1156
 
1157
    /**
1158
     * Executes an SQL statement with the given parameters and returns the number of affected rows.
1159
     *
1160
     * Could be used for:
1161
     *  - DML statements: INSERT, UPDATE, DELETE, etc.
1162
     *  - DDL statements: CREATE, DROP, ALTER, etc.
1163
     *  - DCL statements: GRANT, REVOKE, etc.
1164
     *  - Session control statements: ALTER SESSION, SET, DECLARE, etc.
1165
     *  - Other statements that don't yield a row set.
1166
     *
1167
     * This method supports PDO binding types as well as DBAL mapping types.
1168
     *
1169
     * @param string                                                               $sql    SQL statement
1170
     * @param list<mixed>|array<string, mixed>                                     $params Statement parameters
1171
     * @param array<int, int|string|Type|null>|array<string, int|string|Type|null> $types  Parameter types
1172
     *
1173
     * @return int|string The number of affected rows.
1174
     *
1175
     * @throws Exception
1176
     */
1177
    public function executeStatement($sql, array $params = [], array $types = [])
1178
    {
1179
        $connection = $this->getWrappedConnection();
1180
 
1181
        $logger = $this->_config->getSQLLogger();
1182
        if ($logger !== null) {
1183
            $logger->startQuery($sql, $params, $types);
1184
        }
1185
 
1186
        try {
1187
            if (count($params) > 0) {
1188
                if ($this->needsArrayParameterConversion($params, $types)) {
1189
                    [$sql, $params, $types] = $this->expandArrayParameters($sql, $params, $types);
1190
                }
1191
 
1192
                $stmt = $connection->prepare($sql);
1193
 
1194
                $this->bindParameters($stmt, $params, $types);
1195
 
1196
                return $stmt->execute()
1197
                    ->rowCount();
1198
            }
1199
 
1200
            return $connection->exec($sql);
1201
        } catch (Driver\Exception $e) {
1202
            throw $this->convertExceptionDuringQuery($e, $sql, $params, $types);
1203
        } finally {
1204
            if ($logger !== null) {
1205
                $logger->stopQuery();
1206
            }
1207
        }
1208
    }
1209
 
1210
    /**
1211
     * Returns the current transaction nesting level.
1212
     *
1213
     * @return int The nesting level. A value of 0 means there's no active transaction.
1214
     */
1215
    public function getTransactionNestingLevel()
1216
    {
1217
        return $this->transactionNestingLevel;
1218
    }
1219
 
1220
    /**
1221
     * Returns the ID of the last inserted row, or the last value from a sequence object,
1222
     * depending on the underlying driver.
1223
     *
1224
     * Note: This method may not return a meaningful or consistent result across different drivers,
1225
     * because the underlying database may not even support the notion of AUTO_INCREMENT/IDENTITY
1226
     * columns or sequences.
1227
     *
1228
     * @param string|null $name Name of the sequence object from which the ID should be returned.
1229
     *
1230
     * @return string|int|false A string representation of the last inserted ID.
1231
     *
1232
     * @throws Exception
1233
     */
1234
    public function lastInsertId($name = null)
1235
    {
1236
        if ($name !== null) {
1237
            Deprecation::trigger(
1238
                'doctrine/dbal',
1239
                'https://github.com/doctrine/dbal/issues/4687',
1240
                'The usage of Connection::lastInsertId() with a sequence name is deprecated.',
1241
            );
1242
        }
1243
 
1244
        try {
1245
            return $this->getWrappedConnection()->lastInsertId($name);
1246
        } catch (Driver\Exception $e) {
1247
            throw $this->convertException($e);
1248
        }
1249
    }
1250
 
1251
    /**
1252
     * Executes a function in a transaction.
1253
     *
1254
     * The function gets passed this Connection instance as an (optional) parameter.
1255
     *
1256
     * If an exception occurs during execution of the function or transaction commit,
1257
     * the transaction is rolled back and the exception re-thrown.
1258
     *
1259
     * @param Closure(self):T $func The function to execute transactionally.
1260
     *
1261
     * @return T The value returned by $func
1262
     *
1263
     * @throws Throwable
1264
     *
1265
     * @template T
1266
     */
1267
    public function transactional(Closure $func)
1268
    {
1269
        $this->beginTransaction();
1270
        try {
1271
            $res = $func($this);
1272
            $this->commit();
1273
 
1274
            return $res;
1275
        } catch (Throwable $e) {
1276
            $this->rollBack();
1277
 
1278
            throw $e;
1279
        }
1280
    }
1281
 
1282
    /**
1283
     * Sets if nested transactions should use savepoints.
1284
     *
1285
     * @param bool $nestTransactionsWithSavepoints
1286
     *
1287
     * @return void
1288
     *
1289
     * @throws Exception
1290
     */
1291
    public function setNestTransactionsWithSavepoints($nestTransactionsWithSavepoints)
1292
    {
1293
        if (! $nestTransactionsWithSavepoints) {
1294
            Deprecation::trigger(
1295
                'doctrine/dbal',
1296
                'https://github.com/doctrine/dbal/pull/5383',
1297
                <<<'DEPRECATION'
1298
                Nesting transactions without enabling savepoints is deprecated.
1299
                Call %s::setNestTransactionsWithSavepoints(true) to enable savepoints.
1300
                DEPRECATION,
1301
                self::class,
1302
            );
1303
        }
1304
 
1305
        if ($this->transactionNestingLevel > 0) {
1306
            throw ConnectionException::mayNotAlterNestedTransactionWithSavepointsInTransaction();
1307
        }
1308
 
1309
        if (! $this->getDatabasePlatform()->supportsSavepoints()) {
1310
            throw ConnectionException::savepointsNotSupported();
1311
        }
1312
 
1313
        $this->nestTransactionsWithSavepoints = (bool) $nestTransactionsWithSavepoints;
1314
    }
1315
 
1316
    /**
1317
     * Gets if nested transactions should use savepoints.
1318
     *
1319
     * @return bool
1320
     */
1321
    public function getNestTransactionsWithSavepoints()
1322
    {
1323
        return $this->nestTransactionsWithSavepoints;
1324
    }
1325
 
1326
    /**
1327
     * Returns the savepoint name to use for nested transactions.
1328
     *
1329
     * @return string
1330
     */
1331
    protected function _getNestedTransactionSavePointName()
1332
    {
1333
        return 'DOCTRINE2_SAVEPOINT_' . $this->transactionNestingLevel;
1334
    }
1335
 
1336
    /**
1337
     * @return bool
1338
     *
1339
     * @throws Exception
1340
     */
1341
    public function beginTransaction()
1342
    {
1343
        $connection = $this->getWrappedConnection();
1344
 
1345
        ++$this->transactionNestingLevel;
1346
 
1347
        $logger = $this->_config->getSQLLogger();
1348
 
1349
        if ($this->transactionNestingLevel === 1) {
1350
            if ($logger !== null) {
1351
                $logger->startQuery('"START TRANSACTION"');
1352
            }
1353
 
1354
            $connection->beginTransaction();
1355
 
1356
            if ($logger !== null) {
1357
                $logger->stopQuery();
1358
            }
1359
        } elseif ($this->nestTransactionsWithSavepoints) {
1360
            if ($logger !== null) {
1361
                $logger->startQuery('"SAVEPOINT"');
1362
            }
1363
 
1364
            $this->createSavepoint($this->_getNestedTransactionSavePointName());
1365
            if ($logger !== null) {
1366
                $logger->stopQuery();
1367
            }
1368
        } else {
1369
            Deprecation::trigger(
1370
                'doctrine/dbal',
1371
                'https://github.com/doctrine/dbal/pull/5383',
1372
                <<<'DEPRECATION'
1373
                Nesting transactions without enabling savepoints is deprecated.
1374
                Call %s::setNestTransactionsWithSavepoints(true) to enable savepoints.
1375
                DEPRECATION,
1376
                self::class,
1377
            );
1378
        }
1379
 
1380
        $eventManager = $this->getEventManager();
1381
 
1382
        if ($eventManager->hasListeners(Events::onTransactionBegin)) {
1383
            Deprecation::trigger(
1384
                'doctrine/dbal',
1385
                'https://github.com/doctrine/dbal/issues/5784',
1386
                'Subscribing to %s events is deprecated.',
1387
                Events::onTransactionBegin,
1388
            );
1389
 
1390
            $eventManager->dispatchEvent(Events::onTransactionBegin, new TransactionBeginEventArgs($this));
1391
        }
1392
 
1393
        return true;
1394
    }
1395
 
1396
    /**
1397
     * @return bool
1398
     *
1399
     * @throws Exception
1400
     */
1401
    public function commit()
1402
    {
1403
        if ($this->transactionNestingLevel === 0) {
1404
            throw ConnectionException::noActiveTransaction();
1405
        }
1406
 
1407
        if ($this->isRollbackOnly) {
1408
            throw ConnectionException::commitFailedRollbackOnly();
1409
        }
1410
 
1411
        $result = true;
1412
 
1413
        $connection = $this->getWrappedConnection();
1414
 
1415
        if ($this->transactionNestingLevel === 1) {
1416
            $result = $this->doCommit($connection);
1417
        } elseif ($this->nestTransactionsWithSavepoints) {
1418
            $this->releaseSavepoint($this->_getNestedTransactionSavePointName());
1419
        }
1420
 
1421
        --$this->transactionNestingLevel;
1422
 
1423
        $eventManager = $this->getEventManager();
1424
 
1425
        if ($eventManager->hasListeners(Events::onTransactionCommit)) {
1426
            Deprecation::trigger(
1427
                'doctrine/dbal',
1428
                'https://github.com/doctrine/dbal/issues/5784',
1429
                'Subscribing to %s events is deprecated.',
1430
                Events::onTransactionCommit,
1431
            );
1432
 
1433
            $eventManager->dispatchEvent(Events::onTransactionCommit, new TransactionCommitEventArgs($this));
1434
        }
1435
 
1436
        if ($this->autoCommit !== false || $this->transactionNestingLevel !== 0) {
1437
            return $result;
1438
        }
1439
 
1440
        $this->beginTransaction();
1441
 
1442
        return $result;
1443
    }
1444
 
1445
    /**
1446
     * @return bool
1447
     *
1448
     * @throws DriverException
1449
     */
1450
    private function doCommit(DriverConnection $connection)
1451
    {
1452
        $logger = $this->_config->getSQLLogger();
1453
 
1454
        if ($logger !== null) {
1455
            $logger->startQuery('"COMMIT"');
1456
        }
1457
 
1458
        $result = $connection->commit();
1459
 
1460
        if ($logger !== null) {
1461
            $logger->stopQuery();
1462
        }
1463
 
1464
        return $result;
1465
    }
1466
 
1467
    /**
1468
     * Commits all current nesting transactions.
1469
     *
1470
     * @throws Exception
1471
     */
1472
    private function commitAll(): void
1473
    {
1474
        while ($this->transactionNestingLevel !== 0) {
1475
            if ($this->autoCommit === false && $this->transactionNestingLevel === 1) {
1476
                // When in no auto-commit mode, the last nesting commit immediately starts a new transaction.
1477
                // Therefore we need to do the final commit here and then leave to avoid an infinite loop.
1478
                $this->commit();
1479
 
1480
                return;
1481
            }
1482
 
1483
            $this->commit();
1484
        }
1485
    }
1486
 
1487
    /**
1488
     * Cancels any database changes done during the current transaction.
1489
     *
1490
     * @return bool
1491
     *
1492
     * @throws Exception
1493
     */
1494
    public function rollBack()
1495
    {
1496
        if ($this->transactionNestingLevel === 0) {
1497
            throw ConnectionException::noActiveTransaction();
1498
        }
1499
 
1500
        $connection = $this->getWrappedConnection();
1501
 
1502
        $logger = $this->_config->getSQLLogger();
1503
 
1504
        if ($this->transactionNestingLevel === 1) {
1505
            if ($logger !== null) {
1506
                $logger->startQuery('"ROLLBACK"');
1507
            }
1508
 
1509
            $this->transactionNestingLevel = 0;
1510
            $connection->rollBack();
1511
            $this->isRollbackOnly = false;
1512
            if ($logger !== null) {
1513
                $logger->stopQuery();
1514
            }
1515
 
1516
            if ($this->autoCommit === false) {
1517
                $this->beginTransaction();
1518
            }
1519
        } elseif ($this->nestTransactionsWithSavepoints) {
1520
            if ($logger !== null) {
1521
                $logger->startQuery('"ROLLBACK TO SAVEPOINT"');
1522
            }
1523
 
1524
            $this->rollbackSavepoint($this->_getNestedTransactionSavePointName());
1525
            --$this->transactionNestingLevel;
1526
            if ($logger !== null) {
1527
                $logger->stopQuery();
1528
            }
1529
        } else {
1530
            $this->isRollbackOnly = true;
1531
            --$this->transactionNestingLevel;
1532
        }
1533
 
1534
        $eventManager = $this->getEventManager();
1535
 
1536
        if ($eventManager->hasListeners(Events::onTransactionRollBack)) {
1537
            Deprecation::trigger(
1538
                'doctrine/dbal',
1539
                'https://github.com/doctrine/dbal/issues/5784',
1540
                'Subscribing to %s events is deprecated.',
1541
                Events::onTransactionRollBack,
1542
            );
1543
 
1544
            $eventManager->dispatchEvent(Events::onTransactionRollBack, new TransactionRollBackEventArgs($this));
1545
        }
1546
 
1547
        return true;
1548
    }
1549
 
1550
    /**
1551
     * Creates a new savepoint.
1552
     *
1553
     * @param string $savepoint The name of the savepoint to create.
1554
     *
1555
     * @return void
1556
     *
1557
     * @throws Exception
1558
     */
1559
    public function createSavepoint($savepoint)
1560
    {
1561
        $platform = $this->getDatabasePlatform();
1562
 
1563
        if (! $platform->supportsSavepoints()) {
1564
            throw ConnectionException::savepointsNotSupported();
1565
        }
1566
 
1567
        $this->executeStatement($platform->createSavePoint($savepoint));
1568
    }
1569
 
1570
    /**
1571
     * Releases the given savepoint.
1572
     *
1573
     * @param string $savepoint The name of the savepoint to release.
1574
     *
1575
     * @return void
1576
     *
1577
     * @throws Exception
1578
     */
1579
    public function releaseSavepoint($savepoint)
1580
    {
1581
        $logger = $this->_config->getSQLLogger();
1582
 
1583
        $platform = $this->getDatabasePlatform();
1584
 
1585
        if (! $platform->supportsSavepoints()) {
1586
            throw ConnectionException::savepointsNotSupported();
1587
        }
1588
 
1589
        if (! $platform->supportsReleaseSavepoints()) {
1590
            if ($logger !== null) {
1591
                $logger->stopQuery();
1592
            }
1593
 
1594
            return;
1595
        }
1596
 
1597
        if ($logger !== null) {
1598
            $logger->startQuery('"RELEASE SAVEPOINT"');
1599
        }
1600
 
1601
        $this->executeStatement($platform->releaseSavePoint($savepoint));
1602
 
1603
        if ($logger === null) {
1604
            return;
1605
        }
1606
 
1607
        $logger->stopQuery();
1608
    }
1609
 
1610
    /**
1611
     * Rolls back to the given savepoint.
1612
     *
1613
     * @param string $savepoint The name of the savepoint to rollback to.
1614
     *
1615
     * @return void
1616
     *
1617
     * @throws Exception
1618
     */
1619
    public function rollbackSavepoint($savepoint)
1620
    {
1621
        $platform = $this->getDatabasePlatform();
1622
 
1623
        if (! $platform->supportsSavepoints()) {
1624
            throw ConnectionException::savepointsNotSupported();
1625
        }
1626
 
1627
        $this->executeStatement($platform->rollbackSavePoint($savepoint));
1628
    }
1629
 
1630
    /**
1631
     * Gets the wrapped driver connection.
1632
     *
1633
     * @deprecated Use {@link getNativeConnection()} to access the native connection.
1634
     *
1635
     * @return DriverConnection
1636
     *
1637
     * @throws Exception
1638
     */
1639
    public function getWrappedConnection()
1640
    {
1641
        Deprecation::triggerIfCalledFromOutside(
1642
            'doctrine/dbal',
1643
            'https://github.com/doctrine/dbal/issues/4966',
1644
            'Connection::getWrappedConnection() is deprecated.'
1645
                . ' Use Connection::getNativeConnection() to access the native connection.',
1646
        );
1647
 
1648
        $this->connect();
1649
 
1650
        return $this->_conn;
1651
    }
1652
 
1653
    /** @return resource|object */
1654
    public function getNativeConnection()
1655
    {
1656
        $this->connect();
1657
 
1658
        if (! method_exists($this->_conn, 'getNativeConnection')) {
1659
            throw new LogicException(sprintf(
1660
                'The driver connection %s does not support accessing the native connection.',
1661
                get_class($this->_conn),
1662
            ));
1663
        }
1664
 
1665
        return $this->_conn->getNativeConnection();
1666
    }
1667
 
1668
    /**
1669
     * Creates a SchemaManager that can be used to inspect or change the
1670
     * database schema through the connection.
1671
     *
1672
     * @throws Exception
1673
     */
1674
    public function createSchemaManager(): AbstractSchemaManager
1675
    {
621 lars 1676
        return $this->schemaManagerFactory->createSchemaManager($this);
148 lars 1677
    }
1678
 
1679
    /**
1680
     * Gets the SchemaManager that can be used to inspect or change the
1681
     * database schema through the connection.
1682
     *
1683
     * @deprecated Use {@see createSchemaManager()} instead.
1684
     *
1685
     * @return AbstractSchemaManager
1686
     *
1687
     * @throws Exception
1688
     */
1689
    public function getSchemaManager()
1690
    {
1691
        Deprecation::triggerIfCalledFromOutside(
1692
            'doctrine/dbal',
1693
            'https://github.com/doctrine/dbal/issues/4515',
1694
            'Connection::getSchemaManager() is deprecated, use Connection::createSchemaManager() instead.',
1695
        );
1696
 
1697
        return $this->_schemaManager ??= $this->createSchemaManager();
1698
    }
1699
 
1700
    /**
1701
     * Marks the current transaction so that the only possible
1702
     * outcome for the transaction to be rolled back.
1703
     *
1704
     * @return void
1705
     *
1706
     * @throws ConnectionException If no transaction is active.
1707
     */
1708
    public function setRollbackOnly()
1709
    {
1710
        if ($this->transactionNestingLevel === 0) {
1711
            throw ConnectionException::noActiveTransaction();
1712
        }
1713
 
1714
        $this->isRollbackOnly = true;
1715
    }
1716
 
1717
    /**
1718
     * Checks whether the current transaction is marked for rollback only.
1719
     *
1720
     * @return bool
1721
     *
1722
     * @throws ConnectionException If no transaction is active.
1723
     */
1724
    public function isRollbackOnly()
1725
    {
1726
        if ($this->transactionNestingLevel === 0) {
1727
            throw ConnectionException::noActiveTransaction();
1728
        }
1729
 
1730
        return $this->isRollbackOnly;
1731
    }
1732
 
1733
    /**
1734
     * Converts a given value to its database representation according to the conversion
1735
     * rules of a specific DBAL mapping type.
1736
     *
1737
     * @param mixed  $value The value to convert.
1738
     * @param string $type  The name of the DBAL mapping type.
1739
     *
1740
     * @return mixed The converted value.
1741
     *
1742
     * @throws Exception
1743
     */
1744
    public function convertToDatabaseValue($value, $type)
1745
    {
1746
        return Type::getType($type)->convertToDatabaseValue($value, $this->getDatabasePlatform());
1747
    }
1748
 
1749
    /**
1750
     * Converts a given value to its PHP representation according to the conversion
1751
     * rules of a specific DBAL mapping type.
1752
     *
1753
     * @param mixed  $value The value to convert.
1754
     * @param string $type  The name of the DBAL mapping type.
1755
     *
1756
     * @return mixed The converted type.
1757
     *
1758
     * @throws Exception
1759
     */
1760
    public function convertToPHPValue($value, $type)
1761
    {
1762
        return Type::getType($type)->convertToPHPValue($value, $this->getDatabasePlatform());
1763
    }
1764
 
1765
    /**
1766
     * Binds a set of parameters, some or all of which are typed with a PDO binding type
1767
     * or DBAL mapping type, to a given statement.
1768
     *
1769
     * @param DriverStatement                                                      $stmt   Prepared statement
1770
     * @param list<mixed>|array<string, mixed>                                     $params Statement parameters
1771
     * @param array<int, int|string|Type|null>|array<string, int|string|Type|null> $types  Parameter types
1772
     *
1773
     * @throws Exception
1774
     */
1775
    private function bindParameters(DriverStatement $stmt, array $params, array $types): void
1776
    {
1777
        // Check whether parameters are positional or named. Mixing is not allowed.
1778
        if (is_int(key($params))) {
1779
            $bindIndex = 1;
1780
 
1781
            foreach ($params as $key => $value) {
1782
                if (isset($types[$key])) {
1783
                    $type                  = $types[$key];
1784
                    [$value, $bindingType] = $this->getBindingInfo($value, $type);
1785
                } else {
1786
                    if (array_key_exists($key, $types)) {
1787
                        Deprecation::trigger(
1788
                            'doctrine/dbal',
1789
                            'https://github.com/doctrine/dbal/pull/5550',
1790
                            'Using NULL as prepared statement parameter type is deprecated.'
1791
                                . 'Omit or use Parameter::STRING instead',
1792
                        );
1793
                    }
1794
 
1795
                    $bindingType = ParameterType::STRING;
1796
                }
1797
 
1798
                $stmt->bindValue($bindIndex, $value, $bindingType);
1799
 
1800
                ++$bindIndex;
1801
            }
1802
        } else {
1803
            // Named parameters
1804
            foreach ($params as $name => $value) {
1805
                if (isset($types[$name])) {
1806
                    $type                  = $types[$name];
1807
                    [$value, $bindingType] = $this->getBindingInfo($value, $type);
1808
                } else {
1809
                    if (array_key_exists($name, $types)) {
1810
                        Deprecation::trigger(
1811
                            'doctrine/dbal',
1812
                            'https://github.com/doctrine/dbal/pull/5550',
1813
                            'Using NULL as prepared statement parameter type is deprecated.'
1814
                                . 'Omit or use Parameter::STRING instead',
1815
                        );
1816
                    }
1817
 
1818
                    $bindingType = ParameterType::STRING;
1819
                }
1820
 
1821
                $stmt->bindValue($name, $value, $bindingType);
1822
            }
1823
        }
1824
    }
1825
 
1826
    /**
1827
     * Gets the binding type of a given type.
1828
     *
1829
     * @param mixed                $value The value to bind.
1830
     * @param int|string|Type|null $type  The type to bind (PDO or DBAL).
1831
     *
1832
     * @return array{mixed, int} [0] => the (escaped) value, [1] => the binding type.
1833
     *
1834
     * @throws Exception
1835
     */
1836
    private function getBindingInfo($value, $type): array
1837
    {
1838
        if (is_string($type)) {
1839
            $type = Type::getType($type);
1840
        }
1841
 
1842
        if ($type instanceof Type) {
1843
            $value       = $type->convertToDatabaseValue($value, $this->getDatabasePlatform());
1844
            $bindingType = $type->getBindingType();
1845
        } else {
1846
            $bindingType = $type ?? ParameterType::STRING;
1847
        }
1848
 
1849
        return [$value, $bindingType];
1850
    }
1851
 
1852
    /**
1853
     * Creates a new instance of a SQL query builder.
1854
     *
1855
     * @return QueryBuilder
1856
     */
1857
    public function createQueryBuilder()
1858
    {
1859
        return new Query\QueryBuilder($this);
1860
    }
1861
 
1862
    /**
1863
     * @internal
1864
     *
1865
     * @param list<mixed>|array<string, mixed>                                     $params
1866
     * @param array<int, int|string|Type|null>|array<string, int|string|Type|null> $types
1867
     */
1868
    final public function convertExceptionDuringQuery(
1869
        Driver\Exception $e,
1870
        string $sql,
1871
        array $params = [],
1872
        array $types = []
1873
    ): DriverException {
1874
        return $this->handleDriverException($e, new Query($sql, $params, $types));
1875
    }
1876
 
1877
    /** @internal */
1878
    final public function convertException(Driver\Exception $e): DriverException
1879
    {
1880
        return $this->handleDriverException($e, null);
1881
    }
1882
 
1883
    /**
1884
     * @param array<int, mixed>|array<string, mixed>                               $params
1885
     * @param array<int, int|string|Type|null>|array<string, int|string|Type|null> $types
1886
     *
1887
     * @return array{string, list<mixed>, array<int,Type|int|string|null>}
1888
     */
1889
    private function expandArrayParameters(string $sql, array $params, array $types): array
1890
    {
1891
        $this->parser ??= $this->getDatabasePlatform()->createSQLParser();
1892
        $visitor        = new ExpandArrayParameters($params, $types);
1893
 
1894
        $this->parser->parse($sql, $visitor);
1895
 
1896
        return [
1897
            $visitor->getSQL(),
1898
            $visitor->getParameters(),
1899
            $visitor->getTypes(),
1900
        ];
1901
    }
1902
 
1903
    /**
1904
     * @param array<int, mixed>|array<string, mixed>                               $params
1905
     * @param array<int, int|string|Type|null>|array<string, int|string|Type|null> $types
1906
     */
1907
    private function needsArrayParameterConversion(array $params, array $types): bool
1908
    {
1909
        if (is_string(key($params))) {
1910
            return true;
1911
        }
1912
 
1913
        foreach ($types as $type) {
1914
            if (
621 lars 1915
                $type === ArrayParameterType::INTEGER
1916
                || $type === ArrayParameterType::STRING
1917
                || $type === ArrayParameterType::ASCII
148 lars 1918
            ) {
1919
                return true;
1920
            }
1921
        }
1922
 
1923
        return false;
1924
    }
1925
 
1926
    private function handleDriverException(
1927
        Driver\Exception $driverException,
1928
        ?Query $query
1929
    ): DriverException {
1930
        $this->exceptionConverter ??= $this->_driver->getExceptionConverter();
1931
        $exception                  = $this->exceptionConverter->convert($driverException, $query);
1932
 
1933
        if ($exception instanceof ConnectionLost) {
1934
            $this->close();
1935
        }
1936
 
1937
        return $exception;
1938
    }
1939
 
1940
    /**
1941
     * BC layer for a wide-spread use-case of old DBAL APIs
1942
     *
1943
     * @deprecated This API is deprecated and will be removed after 2022
1944
     *
1945
     * @param array<mixed>           $params The query parameters
1946
     * @param array<int|string|null> $types  The parameter types
1947
     */
1948
    public function executeUpdate(string $sql, array $params = [], array $types = []): int
1949
    {
1950
        return $this->executeStatement($sql, $params, $types);
1951
    }
1952
 
1953
    /**
1954
     * BC layer for a wide-spread use-case of old DBAL APIs
1955
     *
1956
     * @deprecated This API is deprecated and will be removed after 2022
1957
     */
1958
    public function query(string $sql): Result
1959
    {
1960
        return $this->executeQuery($sql);
1961
    }
1962
 
1963
    /**
1964
     * BC layer for a wide-spread use-case of old DBAL APIs
1965
     *
1966
     * @deprecated This API is deprecated and will be removed after 2022
1967
     */
1968
    public function exec(string $sql): int
1969
    {
1970
        return $this->executeStatement($sql);
1971
    }
1972
}