vendor/doctrine/dbal/lib/Doctrine/DBAL/Connection.php line 1038

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