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

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