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