vendor/doctrine/dbal/src/Connection.php line 1924

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