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