diff options
| author | polo <ordipolo@gmx.fr> | 2024-08-13 23:45:21 +0200 |
|---|---|---|
| committer | polo <ordipolo@gmx.fr> | 2024-08-13 23:45:21 +0200 |
| commit | bf6655a534a6775d30cafa67bd801276bda1d98d (patch) | |
| tree | c6381e3f6c81c33eab72508f410b165ba05f7e9c /vendor/doctrine/dbal/src/Connection.php | |
| parent | 94d67a4b51f8e62e7d518cce26a526ae1ec48278 (diff) | |
| download | AppliGestionPHP-bf6655a534a6775d30cafa67bd801276bda1d98d.tar.gz AppliGestionPHP-bf6655a534a6775d30cafa67bd801276bda1d98d.tar.bz2 AppliGestionPHP-bf6655a534a6775d30cafa67bd801276bda1d98d.zip | |
VERSION 0.2 doctrine ORM et entités
Diffstat (limited to 'vendor/doctrine/dbal/src/Connection.php')
| -rw-r--r-- | vendor/doctrine/dbal/src/Connection.php | 1372 |
1 files changed, 1372 insertions, 0 deletions
diff --git a/vendor/doctrine/dbal/src/Connection.php b/vendor/doctrine/dbal/src/Connection.php new file mode 100644 index 0000000..184b01b --- /dev/null +++ b/vendor/doctrine/dbal/src/Connection.php | |||
| @@ -0,0 +1,1372 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\DBAL; | ||
| 6 | |||
| 7 | use Closure; | ||
| 8 | use Doctrine\DBAL\Cache\ArrayResult; | ||
| 9 | use Doctrine\DBAL\Cache\CacheException; | ||
| 10 | use Doctrine\DBAL\Cache\Exception\NoResultDriverConfigured; | ||
| 11 | use Doctrine\DBAL\Cache\QueryCacheProfile; | ||
| 12 | use Doctrine\DBAL\Connection\StaticServerVersionProvider; | ||
| 13 | use Doctrine\DBAL\Driver\API\ExceptionConverter; | ||
| 14 | use Doctrine\DBAL\Driver\Connection as DriverConnection; | ||
| 15 | use Doctrine\DBAL\Driver\Statement as DriverStatement; | ||
| 16 | use Doctrine\DBAL\Exception\CommitFailedRollbackOnly; | ||
| 17 | use Doctrine\DBAL\Exception\ConnectionLost; | ||
| 18 | use Doctrine\DBAL\Exception\DriverException; | ||
| 19 | use Doctrine\DBAL\Exception\NoActiveTransaction; | ||
| 20 | use Doctrine\DBAL\Exception\SavepointsNotSupported; | ||
| 21 | use Doctrine\DBAL\Platforms\AbstractPlatform; | ||
| 22 | use Doctrine\DBAL\Query\Expression\ExpressionBuilder; | ||
| 23 | use Doctrine\DBAL\Query\QueryBuilder; | ||
| 24 | use Doctrine\DBAL\Schema\AbstractSchemaManager; | ||
| 25 | use Doctrine\DBAL\Schema\DefaultSchemaManagerFactory; | ||
| 26 | use Doctrine\DBAL\Schema\SchemaManagerFactory; | ||
| 27 | use Doctrine\DBAL\SQL\Parser; | ||
| 28 | use Doctrine\DBAL\Types\Type; | ||
| 29 | use Doctrine\Deprecations\Deprecation; | ||
| 30 | use InvalidArgumentException; | ||
| 31 | use SensitiveParameter; | ||
| 32 | use Throwable; | ||
| 33 | use Traversable; | ||
| 34 | |||
| 35 | use function array_key_exists; | ||
| 36 | use function array_merge; | ||
| 37 | use function assert; | ||
| 38 | use function count; | ||
| 39 | use function implode; | ||
| 40 | use function is_array; | ||
| 41 | use function is_int; | ||
| 42 | use function is_string; | ||
| 43 | use function key; | ||
| 44 | use function sprintf; | ||
| 45 | |||
| 46 | /** | ||
| 47 | * A database abstraction-level connection that implements features like transaction isolation levels, | ||
| 48 | * configuration, emulated transaction nesting, lazy connecting and more. | ||
| 49 | * | ||
| 50 | * @psalm-import-type Params from DriverManager | ||
| 51 | * @psalm-type WrapperParameterType = string|Type|ParameterType|ArrayParameterType | ||
| 52 | * @psalm-type WrapperParameterTypeArray = array<int<0, max>, WrapperParameterType>|array<string, WrapperParameterType> | ||
| 53 | * @psalm-consistent-constructor | ||
| 54 | */ | ||
| 55 | class Connection implements ServerVersionProvider | ||
| 56 | { | ||
| 57 | /** | ||
| 58 | * The wrapped driver connection. | ||
| 59 | */ | ||
| 60 | protected ?DriverConnection $_conn = null; | ||
| 61 | |||
| 62 | protected Configuration $_config; | ||
| 63 | |||
| 64 | /** | ||
| 65 | * The current auto-commit mode of this connection. | ||
| 66 | */ | ||
| 67 | private bool $autoCommit = true; | ||
| 68 | |||
| 69 | /** | ||
| 70 | * The transaction nesting level. | ||
| 71 | */ | ||
| 72 | private int $transactionNestingLevel = 0; | ||
| 73 | |||
| 74 | /** | ||
| 75 | * The currently active transaction isolation level or NULL before it has been determined. | ||
| 76 | */ | ||
| 77 | private ?TransactionIsolationLevel $transactionIsolationLevel = null; | ||
| 78 | |||
| 79 | /** | ||
| 80 | * The parameters used during creation of the Connection instance. | ||
| 81 | * | ||
| 82 | * @var array<string,mixed> | ||
| 83 | * @psalm-var Params | ||
| 84 | */ | ||
| 85 | private array $params; | ||
| 86 | |||
| 87 | /** | ||
| 88 | * The database platform object used by the connection or NULL before it's initialized. | ||
| 89 | */ | ||
| 90 | private ?AbstractPlatform $platform = null; | ||
| 91 | |||
| 92 | private ?ExceptionConverter $exceptionConverter = null; | ||
| 93 | private ?Parser $parser = null; | ||
| 94 | |||
| 95 | /** | ||
| 96 | * Flag that indicates whether the current transaction is marked for rollback only. | ||
| 97 | */ | ||
| 98 | private bool $isRollbackOnly = false; | ||
| 99 | |||
| 100 | private SchemaManagerFactory $schemaManagerFactory; | ||
| 101 | |||
| 102 | /** | ||
| 103 | * Initializes a new instance of the Connection class. | ||
| 104 | * | ||
| 105 | * @internal The connection can be only instantiated by the driver manager. | ||
| 106 | * | ||
| 107 | * @param array<string, mixed> $params The connection parameters. | ||
| 108 | * @param Driver $driver The driver to use. | ||
| 109 | * @param Configuration|null $config The configuration, optional. | ||
| 110 | * @psalm-param Params $params | ||
| 111 | */ | ||
| 112 | public function __construct( | ||
| 113 | #[SensitiveParameter] | ||
| 114 | array $params, | ||
| 115 | protected Driver $driver, | ||
| 116 | ?Configuration $config = null, | ||
| 117 | ) { | ||
| 118 | $this->_config = $config ?? new Configuration(); | ||
| 119 | $this->params = $params; | ||
| 120 | $this->autoCommit = $this->_config->getAutoCommit(); | ||
| 121 | |||
| 122 | $this->schemaManagerFactory = $this->_config->getSchemaManagerFactory() | ||
| 123 | ?? new DefaultSchemaManagerFactory(); | ||
| 124 | } | ||
| 125 | |||
| 126 | /** | ||
| 127 | * Gets the parameters used during instantiation. | ||
| 128 | * | ||
| 129 | * @internal | ||
| 130 | * | ||
| 131 | * @return array<string,mixed> | ||
| 132 | * @psalm-return Params | ||
| 133 | */ | ||
| 134 | public function getParams(): array | ||
| 135 | { | ||
| 136 | return $this->params; | ||
| 137 | } | ||
| 138 | |||
| 139 | /** | ||
| 140 | * Gets the name of the currently selected database. | ||
| 141 | * | ||
| 142 | * @return string|null The name of the database or NULL if a database is not selected. | ||
| 143 | * The platforms which don't support the concept of a database (e.g. embedded databases) | ||
| 144 | * must always return a string as an indicator of an implicitly selected database. | ||
| 145 | * | ||
| 146 | * @throws Exception | ||
| 147 | */ | ||
| 148 | public function getDatabase(): ?string | ||
| 149 | { | ||
| 150 | $platform = $this->getDatabasePlatform(); | ||
| 151 | $query = $platform->getDummySelectSQL($platform->getCurrentDatabaseExpression()); | ||
| 152 | $database = $this->fetchOne($query); | ||
| 153 | |||
| 154 | assert(is_string($database) || $database === null); | ||
| 155 | |||
| 156 | return $database; | ||
| 157 | } | ||
| 158 | |||
| 159 | /** | ||
| 160 | * Gets the DBAL driver instance. | ||
| 161 | */ | ||
| 162 | public function getDriver(): Driver | ||
| 163 | { | ||
| 164 | return $this->driver; | ||
| 165 | } | ||
| 166 | |||
| 167 | /** | ||
| 168 | * Gets the Configuration used by the Connection. | ||
| 169 | */ | ||
| 170 | public function getConfiguration(): Configuration | ||
| 171 | { | ||
| 172 | return $this->_config; | ||
| 173 | } | ||
| 174 | |||
| 175 | /** | ||
| 176 | * Gets the DatabasePlatform for the connection. | ||
| 177 | * | ||
| 178 | * @throws Exception | ||
| 179 | */ | ||
| 180 | public function getDatabasePlatform(): AbstractPlatform | ||
| 181 | { | ||
| 182 | if ($this->platform === null) { | ||
| 183 | $versionProvider = $this; | ||
| 184 | |||
| 185 | if (isset($this->params['serverVersion'])) { | ||
| 186 | $versionProvider = new StaticServerVersionProvider($this->params['serverVersion']); | ||
| 187 | } elseif (isset($this->params['primary']['serverVersion'])) { | ||
| 188 | $versionProvider = new StaticServerVersionProvider($this->params['primary']['serverVersion']); | ||
| 189 | } | ||
| 190 | |||
| 191 | $this->platform = $this->driver->getDatabasePlatform($versionProvider); | ||
| 192 | } | ||
| 193 | |||
| 194 | return $this->platform; | ||
| 195 | } | ||
| 196 | |||
| 197 | /** | ||
| 198 | * Creates an expression builder for the connection. | ||
| 199 | */ | ||
| 200 | public function createExpressionBuilder(): ExpressionBuilder | ||
| 201 | { | ||
| 202 | return new ExpressionBuilder($this); | ||
| 203 | } | ||
| 204 | |||
| 205 | /** | ||
| 206 | * Establishes the connection with the database and returns the underlying connection. | ||
| 207 | * | ||
| 208 | * @throws Exception | ||
| 209 | */ | ||
| 210 | protected function connect(): DriverConnection | ||
| 211 | { | ||
| 212 | if ($this->_conn !== null) { | ||
| 213 | return $this->_conn; | ||
| 214 | } | ||
| 215 | |||
| 216 | try { | ||
| 217 | $connection = $this->_conn = $this->driver->connect($this->params); | ||
| 218 | } catch (Driver\Exception $e) { | ||
| 219 | throw $this->convertException($e); | ||
| 220 | } | ||
| 221 | |||
| 222 | if ($this->autoCommit === false) { | ||
| 223 | $this->beginTransaction(); | ||
| 224 | } | ||
| 225 | |||
| 226 | return $connection; | ||
| 227 | } | ||
| 228 | |||
| 229 | /** | ||
| 230 | * {@inheritDoc} | ||
| 231 | * | ||
| 232 | * @throws Exception | ||
| 233 | */ | ||
| 234 | public function getServerVersion(): string | ||
| 235 | { | ||
| 236 | return $this->connect()->getServerVersion(); | ||
| 237 | } | ||
| 238 | |||
| 239 | /** | ||
| 240 | * Returns the current auto-commit mode for this connection. | ||
| 241 | * | ||
| 242 | * @see setAutoCommit | ||
| 243 | * | ||
| 244 | * @return bool True if auto-commit mode is currently enabled for this connection, false otherwise. | ||
| 245 | */ | ||
| 246 | public function isAutoCommit(): bool | ||
| 247 | { | ||
| 248 | return $this->autoCommit; | ||
| 249 | } | ||
| 250 | |||
| 251 | /** | ||
| 252 | * Sets auto-commit mode for this connection. | ||
| 253 | * | ||
| 254 | * If a connection is in auto-commit mode, then all its SQL statements will be executed and committed as individual | ||
| 255 | * transactions. Otherwise, its SQL statements are grouped into transactions that are terminated by a call to either | ||
| 256 | * the method commit or the method rollback. By default, new connections are in auto-commit mode. | ||
| 257 | * | ||
| 258 | * NOTE: If this method is called during a transaction and the auto-commit mode is changed, the transaction is | ||
| 259 | * committed. If this method is called and the auto-commit mode is not changed, the call is a no-op. | ||
| 260 | * | ||
| 261 | * @see isAutoCommit | ||
| 262 | * | ||
| 263 | * @throws ConnectionException | ||
| 264 | * @throws DriverException | ||
| 265 | */ | ||
| 266 | public function setAutoCommit(bool $autoCommit): void | ||
| 267 | { | ||
| 268 | // Mode not changed, no-op. | ||
| 269 | if ($autoCommit === $this->autoCommit) { | ||
| 270 | return; | ||
| 271 | } | ||
| 272 | |||
| 273 | $this->autoCommit = $autoCommit; | ||
| 274 | |||
| 275 | // Commit all currently active transactions if any when switching auto-commit mode. | ||
| 276 | if ($this->_conn === null || $this->transactionNestingLevel === 0) { | ||
| 277 | return; | ||
| 278 | } | ||
| 279 | |||
| 280 | $this->commitAll(); | ||
| 281 | } | ||
| 282 | |||
| 283 | /** | ||
| 284 | * Prepares and executes an SQL query and returns the first row of the result | ||
| 285 | * as an associative array. | ||
| 286 | * | ||
| 287 | * @param list<mixed>|array<string, mixed> $params | ||
| 288 | * @psalm-param WrapperParameterTypeArray $types | ||
| 289 | * | ||
| 290 | * @return array<string, mixed>|false False is returned if no rows are found. | ||
| 291 | * | ||
| 292 | * @throws Exception | ||
| 293 | */ | ||
| 294 | public function fetchAssociative(string $query, array $params = [], array $types = []): array|false | ||
| 295 | { | ||
| 296 | return $this->executeQuery($query, $params, $types)->fetchAssociative(); | ||
| 297 | } | ||
| 298 | |||
| 299 | /** | ||
| 300 | * Prepares and executes an SQL query and returns the first row of the result | ||
| 301 | * as a numerically indexed array. | ||
| 302 | * | ||
| 303 | * @param list<mixed>|array<string, mixed> $params | ||
| 304 | * @psalm-param WrapperParameterTypeArray $types | ||
| 305 | * | ||
| 306 | * @return list<mixed>|false False is returned if no rows are found. | ||
| 307 | * | ||
| 308 | * @throws Exception | ||
| 309 | */ | ||
| 310 | public function fetchNumeric(string $query, array $params = [], array $types = []): array|false | ||
| 311 | { | ||
| 312 | return $this->executeQuery($query, $params, $types)->fetchNumeric(); | ||
| 313 | } | ||
| 314 | |||
| 315 | /** | ||
| 316 | * Prepares and executes an SQL query and returns the value of a single column | ||
| 317 | * of the first row of the result. | ||
| 318 | * | ||
| 319 | * @param list<mixed>|array<string, mixed> $params | ||
| 320 | * @psalm-param WrapperParameterTypeArray $types | ||
| 321 | * | ||
| 322 | * @return mixed|false False is returned if no rows are found. | ||
| 323 | * | ||
| 324 | * @throws Exception | ||
| 325 | */ | ||
| 326 | public function fetchOne(string $query, array $params = [], array $types = []): mixed | ||
| 327 | { | ||
| 328 | return $this->executeQuery($query, $params, $types)->fetchOne(); | ||
| 329 | } | ||
| 330 | |||
| 331 | /** | ||
| 332 | * Whether an actual connection to the database is established. | ||
| 333 | */ | ||
| 334 | public function isConnected(): bool | ||
| 335 | { | ||
| 336 | return $this->_conn !== null; | ||
| 337 | } | ||
| 338 | |||
| 339 | /** | ||
| 340 | * Checks whether a transaction is currently active. | ||
| 341 | * | ||
| 342 | * @return bool TRUE if a transaction is currently active, FALSE otherwise. | ||
| 343 | */ | ||
| 344 | public function isTransactionActive(): bool | ||
| 345 | { | ||
| 346 | return $this->transactionNestingLevel > 0; | ||
| 347 | } | ||
| 348 | |||
| 349 | /** | ||
| 350 | * Adds condition based on the criteria to the query components | ||
| 351 | * | ||
| 352 | * @param array<string, mixed> $criteria Map of key columns to their values | ||
| 353 | * | ||
| 354 | * @return array{list<string>, list<mixed>, list<string>} | ||
| 355 | */ | ||
| 356 | private function getCriteriaCondition(array $criteria): array | ||
| 357 | { | ||
| 358 | $columns = $values = $conditions = []; | ||
| 359 | |||
| 360 | foreach ($criteria as $columnName => $value) { | ||
| 361 | if ($value === null) { | ||
| 362 | $conditions[] = $columnName . ' IS NULL'; | ||
| 363 | continue; | ||
| 364 | } | ||
| 365 | |||
| 366 | $columns[] = $columnName; | ||
| 367 | $values[] = $value; | ||
| 368 | $conditions[] = $columnName . ' = ?'; | ||
| 369 | } | ||
| 370 | |||
| 371 | return [$columns, $values, $conditions]; | ||
| 372 | } | ||
| 373 | |||
| 374 | /** | ||
| 375 | * Executes an SQL DELETE statement on a table. | ||
| 376 | * | ||
| 377 | * Table expression and columns are not escaped and are not safe for user-input. | ||
| 378 | * | ||
| 379 | * @param array<string, mixed> $criteria | ||
| 380 | * @param array<int<0,max>, string|ParameterType|Type>|array<string, string|ParameterType|Type> $types | ||
| 381 | * | ||
| 382 | * @return int|numeric-string The number of affected rows. | ||
| 383 | * | ||
| 384 | * @throws Exception | ||
| 385 | */ | ||
| 386 | public function delete(string $table, array $criteria = [], array $types = []): int|string | ||
| 387 | { | ||
| 388 | [$columns, $values, $conditions] = $this->getCriteriaCondition($criteria); | ||
| 389 | |||
| 390 | $sql = 'DELETE FROM ' . $table; | ||
| 391 | |||
| 392 | if ($conditions !== []) { | ||
| 393 | $sql .= ' WHERE ' . implode(' AND ', $conditions); | ||
| 394 | } | ||
| 395 | |||
| 396 | return $this->executeStatement( | ||
| 397 | $sql, | ||
| 398 | $values, | ||
| 399 | is_string(key($types)) ? $this->extractTypeValues($columns, $types) : $types, | ||
| 400 | ); | ||
| 401 | } | ||
| 402 | |||
| 403 | /** | ||
| 404 | * Closes the connection. | ||
| 405 | */ | ||
| 406 | public function close(): void | ||
| 407 | { | ||
| 408 | $this->_conn = null; | ||
| 409 | $this->transactionNestingLevel = 0; | ||
| 410 | } | ||
| 411 | |||
| 412 | /** | ||
| 413 | * Sets the transaction isolation level. | ||
| 414 | * | ||
| 415 | * @param TransactionIsolationLevel $level The level to set. | ||
| 416 | * | ||
| 417 | * @throws Exception | ||
| 418 | */ | ||
| 419 | public function setTransactionIsolation(TransactionIsolationLevel $level): void | ||
| 420 | { | ||
| 421 | $this->transactionIsolationLevel = $level; | ||
| 422 | |||
| 423 | $this->executeStatement($this->getDatabasePlatform()->getSetTransactionIsolationSQL($level)); | ||
| 424 | } | ||
| 425 | |||
| 426 | /** | ||
| 427 | * Gets the currently active transaction isolation level. | ||
| 428 | * | ||
| 429 | * @return TransactionIsolationLevel The current transaction isolation level. | ||
| 430 | * | ||
| 431 | * @throws Exception | ||
| 432 | */ | ||
| 433 | public function getTransactionIsolation(): TransactionIsolationLevel | ||
| 434 | { | ||
| 435 | return $this->transactionIsolationLevel ??= $this->getDatabasePlatform()->getDefaultTransactionIsolationLevel(); | ||
| 436 | } | ||
| 437 | |||
| 438 | /** | ||
| 439 | * Executes an SQL UPDATE statement on a table. | ||
| 440 | * | ||
| 441 | * Table expression and columns are not escaped and are not safe for user-input. | ||
| 442 | * | ||
| 443 | * @param array<string, mixed> $data | ||
| 444 | * @param array<string, mixed> $criteria | ||
| 445 | * @param array<int<0,max>, string|ParameterType|Type>|array<string, string|ParameterType|Type> $types | ||
| 446 | * | ||
| 447 | * @return int|numeric-string The number of affected rows. | ||
| 448 | * | ||
| 449 | * @throws Exception | ||
| 450 | */ | ||
| 451 | public function update(string $table, array $data, array $criteria = [], array $types = []): int|string | ||
| 452 | { | ||
| 453 | $columns = $values = $conditions = $set = []; | ||
| 454 | |||
| 455 | foreach ($data as $columnName => $value) { | ||
| 456 | $columns[] = $columnName; | ||
| 457 | $values[] = $value; | ||
| 458 | $set[] = $columnName . ' = ?'; | ||
| 459 | } | ||
| 460 | |||
| 461 | [$criteriaColumns, $criteriaValues, $criteriaConditions] = $this->getCriteriaCondition($criteria); | ||
| 462 | |||
| 463 | $columns = array_merge($columns, $criteriaColumns); | ||
| 464 | $values = array_merge($values, $criteriaValues); | ||
| 465 | $conditions = array_merge($conditions, $criteriaConditions); | ||
| 466 | |||
| 467 | if (is_string(key($types))) { | ||
| 468 | $types = $this->extractTypeValues($columns, $types); | ||
| 469 | } | ||
| 470 | |||
| 471 | $sql = 'UPDATE ' . $table . ' SET ' . implode(', ', $set); | ||
| 472 | |||
| 473 | if ($conditions !== []) { | ||
| 474 | $sql .= ' WHERE ' . implode(' AND ', $conditions); | ||
| 475 | } | ||
| 476 | |||
| 477 | return $this->executeStatement($sql, $values, $types); | ||
| 478 | } | ||
| 479 | |||
| 480 | /** | ||
| 481 | * Inserts a table row with specified data. | ||
| 482 | * | ||
| 483 | * Table expression and columns are not escaped and are not safe for user-input. | ||
| 484 | * | ||
| 485 | * @param array<string, mixed> $data | ||
| 486 | * @param array<int<0,max>, string|ParameterType|Type>|array<string, string|ParameterType|Type> $types | ||
| 487 | * | ||
| 488 | * @return int|numeric-string The number of affected rows. | ||
| 489 | * | ||
| 490 | * @throws Exception | ||
| 491 | */ | ||
| 492 | public function insert(string $table, array $data, array $types = []): int|string | ||
| 493 | { | ||
| 494 | if (count($data) === 0) { | ||
| 495 | return $this->executeStatement('INSERT INTO ' . $table . ' () VALUES ()'); | ||
| 496 | } | ||
| 497 | |||
| 498 | $columns = []; | ||
| 499 | $values = []; | ||
| 500 | $set = []; | ||
| 501 | |||
| 502 | foreach ($data as $columnName => $value) { | ||
| 503 | $columns[] = $columnName; | ||
| 504 | $values[] = $value; | ||
| 505 | $set[] = '?'; | ||
| 506 | } | ||
| 507 | |||
| 508 | return $this->executeStatement( | ||
| 509 | 'INSERT INTO ' . $table . ' (' . implode(', ', $columns) . ')' . | ||
| 510 | ' VALUES (' . implode(', ', $set) . ')', | ||
| 511 | $values, | ||
| 512 | is_string(key($types)) ? $this->extractTypeValues($columns, $types) : $types, | ||
| 513 | ); | ||
| 514 | } | ||
| 515 | |||
| 516 | /** | ||
| 517 | * Extract ordered type list from an ordered column list and type map. | ||
| 518 | * | ||
| 519 | * @param array<int, string> $columns | ||
| 520 | * @param array<int, string|ParameterType|Type>|array<string, string|ParameterType|Type> $types | ||
| 521 | * | ||
| 522 | * @return array<int<0, max>, string|ParameterType|Type> | ||
| 523 | */ | ||
| 524 | private function extractTypeValues(array $columns, array $types): array | ||
| 525 | { | ||
| 526 | $typeValues = []; | ||
| 527 | |||
| 528 | foreach ($columns as $columnName) { | ||
| 529 | $typeValues[] = $types[$columnName] ?? ParameterType::STRING; | ||
| 530 | } | ||
| 531 | |||
| 532 | return $typeValues; | ||
| 533 | } | ||
| 534 | |||
| 535 | /** | ||
| 536 | * Quotes a string so it can be safely used as a table or column name, even if | ||
| 537 | * it is a reserved name. | ||
| 538 | * | ||
| 539 | * Delimiting style depends on the underlying database platform that is being used. | ||
| 540 | * | ||
| 541 | * NOTE: Just because you CAN use quoted identifiers does not mean | ||
| 542 | * you SHOULD use them. In general, they end up causing way more | ||
| 543 | * problems than they solve. | ||
| 544 | * | ||
| 545 | * @param string $identifier The identifier to be quoted. | ||
| 546 | * | ||
| 547 | * @return string The quoted identifier. | ||
| 548 | */ | ||
| 549 | public function quoteIdentifier(string $identifier): string | ||
| 550 | { | ||
| 551 | return $this->getDatabasePlatform()->quoteIdentifier($identifier); | ||
| 552 | } | ||
| 553 | |||
| 554 | /** | ||
| 555 | * The usage of this method is discouraged. Use prepared statements | ||
| 556 | * or {@see AbstractPlatform::quoteStringLiteral()} instead. | ||
| 557 | */ | ||
| 558 | public function quote(string $value): string | ||
| 559 | { | ||
| 560 | return $this->connect()->quote($value); | ||
| 561 | } | ||
| 562 | |||
| 563 | /** | ||
| 564 | * Prepares and executes an SQL query and returns the result as an array of numeric arrays. | ||
| 565 | * | ||
| 566 | * @param list<mixed>|array<string, mixed> $params | ||
| 567 | * @psalm-param WrapperParameterTypeArray $types | ||
| 568 | * | ||
| 569 | * @return list<list<mixed>> | ||
| 570 | * | ||
| 571 | * @throws Exception | ||
| 572 | */ | ||
| 573 | public function fetchAllNumeric(string $query, array $params = [], array $types = []): array | ||
| 574 | { | ||
| 575 | return $this->executeQuery($query, $params, $types)->fetchAllNumeric(); | ||
| 576 | } | ||
| 577 | |||
| 578 | /** | ||
| 579 | * Prepares and executes an SQL query and returns the result as an array of associative arrays. | ||
| 580 | * | ||
| 581 | * @param list<mixed>|array<string, mixed> $params | ||
| 582 | * @psalm-param WrapperParameterTypeArray $types | ||
| 583 | * | ||
| 584 | * @return list<array<string,mixed>> | ||
| 585 | * | ||
| 586 | * @throws Exception | ||
| 587 | */ | ||
| 588 | public function fetchAllAssociative(string $query, array $params = [], array $types = []): array | ||
| 589 | { | ||
| 590 | return $this->executeQuery($query, $params, $types)->fetchAllAssociative(); | ||
| 591 | } | ||
| 592 | |||
| 593 | /** | ||
| 594 | * Prepares and executes an SQL query and returns the result as an associative array with the keys | ||
| 595 | * mapped to the first column and the values mapped to the second column. | ||
| 596 | * | ||
| 597 | * @param list<mixed>|array<string, mixed> $params | ||
| 598 | * @psalm-param WrapperParameterTypeArray $types | ||
| 599 | * | ||
| 600 | * @return array<mixed,mixed> | ||
| 601 | * | ||
| 602 | * @throws Exception | ||
| 603 | */ | ||
| 604 | public function fetchAllKeyValue(string $query, array $params = [], array $types = []): array | ||
| 605 | { | ||
| 606 | return $this->executeQuery($query, $params, $types)->fetchAllKeyValue(); | ||
| 607 | } | ||
| 608 | |||
| 609 | /** | ||
| 610 | * Prepares and executes an SQL query and returns the result as an associative array with the keys mapped | ||
| 611 | * to the first column and the values being an associative array representing the rest of the columns | ||
| 612 | * and their values. | ||
| 613 | * | ||
| 614 | * @param list<mixed>|array<string, mixed> $params | ||
| 615 | * @psalm-param WrapperParameterTypeArray $types | ||
| 616 | * | ||
| 617 | * @return array<mixed,array<string,mixed>> | ||
| 618 | * | ||
| 619 | * @throws Exception | ||
| 620 | */ | ||
| 621 | public function fetchAllAssociativeIndexed(string $query, array $params = [], array $types = []): array | ||
| 622 | { | ||
| 623 | return $this->executeQuery($query, $params, $types)->fetchAllAssociativeIndexed(); | ||
| 624 | } | ||
| 625 | |||
| 626 | /** | ||
| 627 | * Prepares and executes an SQL query and returns the result as an array of the first column values. | ||
| 628 | * | ||
| 629 | * @param list<mixed>|array<string, mixed> $params | ||
| 630 | * @psalm-param WrapperParameterTypeArray $types | ||
| 631 | * | ||
| 632 | * @return list<mixed> | ||
| 633 | * | ||
| 634 | * @throws Exception | ||
| 635 | */ | ||
| 636 | public function fetchFirstColumn(string $query, array $params = [], array $types = []): array | ||
| 637 | { | ||
| 638 | return $this->executeQuery($query, $params, $types)->fetchFirstColumn(); | ||
| 639 | } | ||
| 640 | |||
| 641 | /** | ||
| 642 | * Prepares and executes an SQL query and returns the result as an iterator over rows represented as numeric arrays. | ||
| 643 | * | ||
| 644 | * @param list<mixed>|array<string, mixed> $params | ||
| 645 | * @psalm-param WrapperParameterTypeArray $types | ||
| 646 | * | ||
| 647 | * @return Traversable<int,list<mixed>> | ||
| 648 | * | ||
| 649 | * @throws Exception | ||
| 650 | */ | ||
| 651 | public function iterateNumeric(string $query, array $params = [], array $types = []): Traversable | ||
| 652 | { | ||
| 653 | return $this->executeQuery($query, $params, $types)->iterateNumeric(); | ||
| 654 | } | ||
| 655 | |||
| 656 | /** | ||
| 657 | * Prepares and executes an SQL query and returns the result as an iterator over rows represented | ||
| 658 | * as associative arrays. | ||
| 659 | * | ||
| 660 | * @param list<mixed>|array<string, mixed> $params | ||
| 661 | * @psalm-param WrapperParameterTypeArray $types | ||
| 662 | * | ||
| 663 | * @return Traversable<int,array<string,mixed>> | ||
| 664 | * | ||
| 665 | * @throws Exception | ||
| 666 | */ | ||
| 667 | public function iterateAssociative(string $query, array $params = [], array $types = []): Traversable | ||
| 668 | { | ||
| 669 | return $this->executeQuery($query, $params, $types)->iterateAssociative(); | ||
| 670 | } | ||
| 671 | |||
| 672 | /** | ||
| 673 | * Prepares and executes an SQL query and returns the result as an iterator with the keys | ||
| 674 | * mapped to the first column and the values mapped to the second column. | ||
| 675 | * | ||
| 676 | * @param list<mixed>|array<string, mixed> $params | ||
| 677 | * @psalm-param WrapperParameterTypeArray $types | ||
| 678 | * | ||
| 679 | * @return Traversable<mixed,mixed> | ||
| 680 | * | ||
| 681 | * @throws Exception | ||
| 682 | */ | ||
| 683 | public function iterateKeyValue(string $query, array $params = [], array $types = []): Traversable | ||
| 684 | { | ||
| 685 | return $this->executeQuery($query, $params, $types)->iterateKeyValue(); | ||
| 686 | } | ||
| 687 | |||
| 688 | /** | ||
| 689 | * Prepares and executes an SQL query and returns the result as an iterator with the keys mapped | ||
| 690 | * to the first column and the values being an associative array representing the rest of the columns | ||
| 691 | * and their values. | ||
| 692 | * | ||
| 693 | * @param list<mixed>|array<string, mixed> $params | ||
| 694 | * @psalm-param WrapperParameterTypeArray $types | ||
| 695 | * | ||
| 696 | * @return Traversable<mixed,array<string,mixed>> | ||
| 697 | * | ||
| 698 | * @throws Exception | ||
| 699 | */ | ||
| 700 | public function iterateAssociativeIndexed(string $query, array $params = [], array $types = []): Traversable | ||
| 701 | { | ||
| 702 | return $this->executeQuery($query, $params, $types)->iterateAssociativeIndexed(); | ||
| 703 | } | ||
| 704 | |||
| 705 | /** | ||
| 706 | * Prepares and executes an SQL query and returns the result as an iterator over the first column values. | ||
| 707 | * | ||
| 708 | * @param list<mixed>|array<string, mixed> $params | ||
| 709 | * @psalm-param WrapperParameterTypeArray $types | ||
| 710 | * | ||
| 711 | * @return Traversable<int,mixed> | ||
| 712 | * | ||
| 713 | * @throws Exception | ||
| 714 | */ | ||
| 715 | public function iterateColumn(string $query, array $params = [], array $types = []): Traversable | ||
| 716 | { | ||
| 717 | return $this->executeQuery($query, $params, $types)->iterateColumn(); | ||
| 718 | } | ||
| 719 | |||
| 720 | /** | ||
| 721 | * Prepares an SQL statement. | ||
| 722 | * | ||
| 723 | * @param string $sql The SQL statement to prepare. | ||
| 724 | * | ||
| 725 | * @throws Exception | ||
| 726 | */ | ||
| 727 | public function prepare(string $sql): Statement | ||
| 728 | { | ||
| 729 | $connection = $this->connect(); | ||
| 730 | |||
| 731 | try { | ||
| 732 | $statement = $connection->prepare($sql); | ||
| 733 | } catch (Driver\Exception $e) { | ||
| 734 | throw $this->convertExceptionDuringQuery($e, $sql); | ||
| 735 | } | ||
| 736 | |||
| 737 | return new Statement($this, $statement, $sql); | ||
| 738 | } | ||
| 739 | |||
| 740 | /** | ||
| 741 | * Executes an, optionally parameterized, SQL query. | ||
| 742 | * | ||
| 743 | * If the query is parametrized, a prepared statement is used. | ||
| 744 | * | ||
| 745 | * @param list<mixed>|array<string, mixed> $params | ||
| 746 | * @psalm-param WrapperParameterTypeArray $types | ||
| 747 | * | ||
| 748 | * @throws Exception | ||
| 749 | */ | ||
| 750 | public function executeQuery( | ||
| 751 | string $sql, | ||
| 752 | array $params = [], | ||
| 753 | array $types = [], | ||
| 754 | ?QueryCacheProfile $qcp = null, | ||
| 755 | ): Result { | ||
| 756 | if ($qcp !== null) { | ||
| 757 | return $this->executeCacheQuery($sql, $params, $types, $qcp); | ||
| 758 | } | ||
| 759 | |||
| 760 | $connection = $this->connect(); | ||
| 761 | |||
| 762 | try { | ||
| 763 | if (count($params) > 0) { | ||
| 764 | [$sql, $params, $types] = $this->expandArrayParameters($sql, $params, $types); | ||
| 765 | |||
| 766 | $stmt = $connection->prepare($sql); | ||
| 767 | |||
| 768 | $this->bindParameters($stmt, $params, $types); | ||
| 769 | |||
| 770 | $result = $stmt->execute(); | ||
| 771 | } else { | ||
| 772 | $result = $connection->query($sql); | ||
| 773 | } | ||
| 774 | |||
| 775 | return new Result($result, $this); | ||
| 776 | } catch (Driver\Exception $e) { | ||
| 777 | throw $this->convertExceptionDuringQuery($e, $sql, $params, $types); | ||
| 778 | } | ||
| 779 | } | ||
| 780 | |||
| 781 | /** | ||
| 782 | * Executes a caching query. | ||
| 783 | * | ||
| 784 | * @param list<mixed>|array<string, mixed> $params | ||
| 785 | * @psalm-param WrapperParameterTypeArray $types | ||
| 786 | * | ||
| 787 | * @throws CacheException | ||
| 788 | * @throws Exception | ||
| 789 | */ | ||
| 790 | public function executeCacheQuery(string $sql, array $params, array $types, QueryCacheProfile $qcp): Result | ||
| 791 | { | ||
| 792 | $resultCache = $qcp->getResultCache() ?? $this->_config->getResultCache(); | ||
| 793 | |||
| 794 | if ($resultCache === null) { | ||
| 795 | throw NoResultDriverConfigured::new(); | ||
| 796 | } | ||
| 797 | |||
| 798 | $connectionParams = $this->params; | ||
| 799 | unset($connectionParams['password']); | ||
| 800 | |||
| 801 | [$cacheKey, $realKey] = $qcp->generateCacheKeys($sql, $params, $types, $connectionParams); | ||
| 802 | |||
| 803 | $item = $resultCache->getItem($cacheKey); | ||
| 804 | |||
| 805 | if ($item->isHit()) { | ||
| 806 | $value = $item->get(); | ||
| 807 | if (! is_array($value)) { | ||
| 808 | $value = []; | ||
| 809 | } | ||
| 810 | |||
| 811 | if (isset($value[$realKey])) { | ||
| 812 | return new Result(new ArrayResult($value[$realKey]), $this); | ||
| 813 | } | ||
| 814 | } else { | ||
| 815 | $value = []; | ||
| 816 | } | ||
| 817 | |||
| 818 | $data = $this->fetchAllAssociative($sql, $params, $types); | ||
| 819 | |||
| 820 | $value[$realKey] = $data; | ||
| 821 | |||
| 822 | $item->set($value); | ||
| 823 | |||
| 824 | $lifetime = $qcp->getLifetime(); | ||
| 825 | if ($lifetime > 0) { | ||
| 826 | $item->expiresAfter($lifetime); | ||
| 827 | } | ||
| 828 | |||
| 829 | $resultCache->save($item); | ||
| 830 | |||
| 831 | return new Result(new ArrayResult($data), $this); | ||
| 832 | } | ||
| 833 | |||
| 834 | /** | ||
| 835 | * Executes an SQL statement with the given parameters and returns the number of affected rows. | ||
| 836 | * | ||
| 837 | * Could be used for: | ||
| 838 | * - DML statements: INSERT, UPDATE, DELETE, etc. | ||
| 839 | * - DDL statements: CREATE, DROP, ALTER, etc. | ||
| 840 | * - DCL statements: GRANT, REVOKE, etc. | ||
| 841 | * - Session control statements: ALTER SESSION, SET, DECLARE, etc. | ||
| 842 | * - Other statements that don't yield a row set. | ||
| 843 | * | ||
| 844 | * This method supports PDO binding types as well as DBAL mapping types. | ||
| 845 | * | ||
| 846 | * @param list<mixed>|array<string, mixed> $params | ||
| 847 | * @psalm-param WrapperParameterTypeArray $types | ||
| 848 | * | ||
| 849 | * @return int|numeric-string | ||
| 850 | * | ||
| 851 | * @throws Exception | ||
| 852 | */ | ||
| 853 | public function executeStatement(string $sql, array $params = [], array $types = []): int|string | ||
| 854 | { | ||
| 855 | $connection = $this->connect(); | ||
| 856 | |||
| 857 | try { | ||
| 858 | if (count($params) > 0) { | ||
| 859 | [$sql, $params, $types] = $this->expandArrayParameters($sql, $params, $types); | ||
| 860 | |||
| 861 | $stmt = $connection->prepare($sql); | ||
| 862 | |||
| 863 | $this->bindParameters($stmt, $params, $types); | ||
| 864 | |||
| 865 | return $stmt->execute() | ||
| 866 | ->rowCount(); | ||
| 867 | } | ||
| 868 | |||
| 869 | return $connection->exec($sql); | ||
| 870 | } catch (Driver\Exception $e) { | ||
| 871 | throw $this->convertExceptionDuringQuery($e, $sql, $params, $types); | ||
| 872 | } | ||
| 873 | } | ||
| 874 | |||
| 875 | /** | ||
| 876 | * Returns the current transaction nesting level. | ||
| 877 | * | ||
| 878 | * @return int The nesting level. A value of 0 means there's no active transaction. | ||
| 879 | */ | ||
| 880 | public function getTransactionNestingLevel(): int | ||
| 881 | { | ||
| 882 | return $this->transactionNestingLevel; | ||
| 883 | } | ||
| 884 | |||
| 885 | /** | ||
| 886 | * Returns the ID of the last inserted row. | ||
| 887 | * | ||
| 888 | * If the underlying driver does not support identity columns, an exception is thrown. | ||
| 889 | * | ||
| 890 | * @throws Exception | ||
| 891 | */ | ||
| 892 | public function lastInsertId(): int|string | ||
| 893 | { | ||
| 894 | try { | ||
| 895 | return $this->connect()->lastInsertId(); | ||
| 896 | } catch (Driver\Exception $e) { | ||
| 897 | throw $this->convertException($e); | ||
| 898 | } | ||
| 899 | } | ||
| 900 | |||
| 901 | /** | ||
| 902 | * Executes a function in a transaction. | ||
| 903 | * | ||
| 904 | * The function gets passed this Connection instance as an (optional) parameter. | ||
| 905 | * | ||
| 906 | * If an exception occurs during execution of the function or transaction commit, | ||
| 907 | * the transaction is rolled back and the exception re-thrown. | ||
| 908 | * | ||
| 909 | * @param Closure(self):T $func The function to execute transactionally. | ||
| 910 | * | ||
| 911 | * @return T The value returned by $func | ||
| 912 | * | ||
| 913 | * @throws Throwable | ||
| 914 | * | ||
| 915 | * @template T | ||
| 916 | */ | ||
| 917 | public function transactional(Closure $func): mixed | ||
| 918 | { | ||
| 919 | $this->beginTransaction(); | ||
| 920 | try { | ||
| 921 | $res = $func($this); | ||
| 922 | $this->commit(); | ||
| 923 | |||
| 924 | return $res; | ||
| 925 | } catch (Throwable $e) { | ||
| 926 | $this->rollBack(); | ||
| 927 | |||
| 928 | throw $e; | ||
| 929 | } | ||
| 930 | } | ||
| 931 | |||
| 932 | /** | ||
| 933 | * Sets if nested transactions should use savepoints. | ||
| 934 | * | ||
| 935 | * @deprecated No replacement planned | ||
| 936 | * | ||
| 937 | * @throws Exception | ||
| 938 | */ | ||
| 939 | public function setNestTransactionsWithSavepoints(bool $nestTransactionsWithSavepoints): void | ||
| 940 | { | ||
| 941 | if (! $nestTransactionsWithSavepoints) { | ||
| 942 | throw new InvalidArgumentException(sprintf( | ||
| 943 | 'Calling %s with false to enable nesting transactions without savepoints is no longer supported.', | ||
| 944 | __METHOD__, | ||
| 945 | )); | ||
| 946 | } | ||
| 947 | |||
| 948 | Deprecation::trigger( | ||
| 949 | 'doctrine/dbal', | ||
| 950 | 'https://github.com/doctrine/dbal/pull/5383', | ||
| 951 | '%s is deprecated and will be removed in 5.0', | ||
| 952 | __METHOD__, | ||
| 953 | ); | ||
| 954 | } | ||
| 955 | |||
| 956 | /** | ||
| 957 | * Gets if nested transactions should use savepoints. | ||
| 958 | * | ||
| 959 | * @deprecated No replacement planned | ||
| 960 | */ | ||
| 961 | public function getNestTransactionsWithSavepoints(): bool | ||
| 962 | { | ||
| 963 | Deprecation::trigger( | ||
| 964 | 'doctrine/dbal', | ||
| 965 | 'https://github.com/doctrine/dbal/pull/5383', | ||
| 966 | '%s is deprecated and will be removed in 5.0', | ||
| 967 | __METHOD__, | ||
| 968 | ); | ||
| 969 | |||
| 970 | return true; | ||
| 971 | } | ||
| 972 | |||
| 973 | /** | ||
| 974 | * Returns the savepoint name to use for nested transactions. | ||
| 975 | */ | ||
| 976 | protected function _getNestedTransactionSavePointName(): string | ||
| 977 | { | ||
| 978 | return 'DOCTRINE_' . $this->transactionNestingLevel; | ||
| 979 | } | ||
| 980 | |||
| 981 | /** @throws Exception */ | ||
| 982 | public function beginTransaction(): void | ||
| 983 | { | ||
| 984 | $connection = $this->connect(); | ||
| 985 | |||
| 986 | ++$this->transactionNestingLevel; | ||
| 987 | |||
| 988 | if ($this->transactionNestingLevel === 1) { | ||
| 989 | $connection->beginTransaction(); | ||
| 990 | } else { | ||
| 991 | $this->createSavepoint($this->_getNestedTransactionSavePointName()); | ||
| 992 | } | ||
| 993 | } | ||
| 994 | |||
| 995 | /** @throws Exception */ | ||
| 996 | public function commit(): void | ||
| 997 | { | ||
| 998 | if ($this->transactionNestingLevel === 0) { | ||
| 999 | throw NoActiveTransaction::new(); | ||
| 1000 | } | ||
| 1001 | |||
| 1002 | if ($this->isRollbackOnly) { | ||
| 1003 | throw CommitFailedRollbackOnly::new(); | ||
| 1004 | } | ||
| 1005 | |||
| 1006 | $connection = $this->connect(); | ||
| 1007 | |||
| 1008 | if ($this->transactionNestingLevel === 1) { | ||
| 1009 | try { | ||
| 1010 | $connection->commit(); | ||
| 1011 | } catch (Driver\Exception $e) { | ||
| 1012 | throw $this->convertException($e); | ||
| 1013 | } | ||
| 1014 | } else { | ||
| 1015 | $this->releaseSavepoint($this->_getNestedTransactionSavePointName()); | ||
| 1016 | } | ||
| 1017 | |||
| 1018 | --$this->transactionNestingLevel; | ||
| 1019 | |||
| 1020 | if ($this->autoCommit !== false || $this->transactionNestingLevel !== 0) { | ||
| 1021 | return; | ||
| 1022 | } | ||
| 1023 | |||
| 1024 | $this->beginTransaction(); | ||
| 1025 | } | ||
| 1026 | |||
| 1027 | /** | ||
| 1028 | * Commits all current nesting transactions. | ||
| 1029 | * | ||
| 1030 | * @throws Exception | ||
| 1031 | */ | ||
| 1032 | private function commitAll(): void | ||
| 1033 | { | ||
| 1034 | while ($this->transactionNestingLevel !== 0) { | ||
| 1035 | if ($this->autoCommit === false && $this->transactionNestingLevel === 1) { | ||
| 1036 | // When in no auto-commit mode, the last nesting commit immediately starts a new transaction. | ||
| 1037 | // Therefore we need to do the final commit here and then leave to avoid an infinite loop. | ||
| 1038 | $this->commit(); | ||
| 1039 | |||
| 1040 | return; | ||
| 1041 | } | ||
| 1042 | |||
| 1043 | $this->commit(); | ||
| 1044 | } | ||
| 1045 | } | ||
| 1046 | |||
| 1047 | /** @throws Exception */ | ||
| 1048 | public function rollBack(): void | ||
| 1049 | { | ||
| 1050 | if ($this->transactionNestingLevel === 0) { | ||
| 1051 | throw NoActiveTransaction::new(); | ||
| 1052 | } | ||
| 1053 | |||
| 1054 | $connection = $this->connect(); | ||
| 1055 | |||
| 1056 | if ($this->transactionNestingLevel === 1) { | ||
| 1057 | $this->transactionNestingLevel = 0; | ||
| 1058 | |||
| 1059 | try { | ||
| 1060 | $connection->rollBack(); | ||
| 1061 | } catch (Driver\Exception $e) { | ||
| 1062 | throw $this->convertException($e); | ||
| 1063 | } finally { | ||
| 1064 | $this->isRollbackOnly = false; | ||
| 1065 | |||
| 1066 | if ($this->autoCommit === false) { | ||
| 1067 | $this->beginTransaction(); | ||
| 1068 | } | ||
| 1069 | } | ||
| 1070 | } else { | ||
| 1071 | $this->rollbackSavepoint($this->_getNestedTransactionSavePointName()); | ||
| 1072 | --$this->transactionNestingLevel; | ||
| 1073 | } | ||
| 1074 | } | ||
| 1075 | |||
| 1076 | /** | ||
| 1077 | * Creates a new savepoint. | ||
| 1078 | * | ||
| 1079 | * @param string $savepoint The name of the savepoint to create. | ||
| 1080 | * | ||
| 1081 | * @throws Exception | ||
| 1082 | */ | ||
| 1083 | public function createSavepoint(string $savepoint): void | ||
| 1084 | { | ||
| 1085 | $platform = $this->getDatabasePlatform(); | ||
| 1086 | |||
| 1087 | if (! $platform->supportsSavepoints()) { | ||
| 1088 | throw SavepointsNotSupported::new(); | ||
| 1089 | } | ||
| 1090 | |||
| 1091 | $this->executeStatement($platform->createSavePoint($savepoint)); | ||
| 1092 | } | ||
| 1093 | |||
| 1094 | /** | ||
| 1095 | * Releases the given savepoint. | ||
| 1096 | * | ||
| 1097 | * @param string $savepoint The name of the savepoint to release. | ||
| 1098 | * | ||
| 1099 | * @throws Exception | ||
| 1100 | */ | ||
| 1101 | public function releaseSavepoint(string $savepoint): void | ||
| 1102 | { | ||
| 1103 | $platform = $this->getDatabasePlatform(); | ||
| 1104 | |||
| 1105 | if (! $platform->supportsSavepoints()) { | ||
| 1106 | throw SavepointsNotSupported::new(); | ||
| 1107 | } | ||
| 1108 | |||
| 1109 | if (! $platform->supportsReleaseSavepoints()) { | ||
| 1110 | return; | ||
| 1111 | } | ||
| 1112 | |||
| 1113 | $this->executeStatement($platform->releaseSavePoint($savepoint)); | ||
| 1114 | } | ||
| 1115 | |||
| 1116 | /** | ||
| 1117 | * Rolls back to the given savepoint. | ||
| 1118 | * | ||
| 1119 | * @param string $savepoint The name of the savepoint to rollback to. | ||
| 1120 | * | ||
| 1121 | * @throws Exception | ||
| 1122 | */ | ||
| 1123 | public function rollbackSavepoint(string $savepoint): void | ||
| 1124 | { | ||
| 1125 | $platform = $this->getDatabasePlatform(); | ||
| 1126 | |||
| 1127 | if (! $platform->supportsSavepoints()) { | ||
| 1128 | throw SavepointsNotSupported::new(); | ||
| 1129 | } | ||
| 1130 | |||
| 1131 | $this->executeStatement($platform->rollbackSavePoint($savepoint)); | ||
| 1132 | } | ||
| 1133 | |||
| 1134 | /** | ||
| 1135 | * Provides access to the native database connection. | ||
| 1136 | * | ||
| 1137 | * @return resource|object | ||
| 1138 | * | ||
| 1139 | * @throws Exception | ||
| 1140 | */ | ||
| 1141 | public function getNativeConnection() | ||
| 1142 | { | ||
| 1143 | return $this->connect()->getNativeConnection(); | ||
| 1144 | } | ||
| 1145 | |||
| 1146 | /** | ||
| 1147 | * Creates a SchemaManager that can be used to inspect or change the | ||
| 1148 | * database schema through the connection. | ||
| 1149 | * | ||
| 1150 | * @throws Exception | ||
| 1151 | */ | ||
| 1152 | public function createSchemaManager(): AbstractSchemaManager | ||
| 1153 | { | ||
| 1154 | return $this->schemaManagerFactory->createSchemaManager($this); | ||
| 1155 | } | ||
| 1156 | |||
| 1157 | /** | ||
| 1158 | * Marks the current transaction so that the only possible | ||
| 1159 | * outcome for the transaction to be rolled back. | ||
| 1160 | * | ||
| 1161 | * @throws ConnectionException If no transaction is active. | ||
| 1162 | */ | ||
| 1163 | public function setRollbackOnly(): void | ||
| 1164 | { | ||
| 1165 | if ($this->transactionNestingLevel === 0) { | ||
| 1166 | throw NoActiveTransaction::new(); | ||
| 1167 | } | ||
| 1168 | |||
| 1169 | $this->isRollbackOnly = true; | ||
| 1170 | } | ||
| 1171 | |||
| 1172 | /** | ||
| 1173 | * Checks whether the current transaction is marked for rollback only. | ||
| 1174 | * | ||
| 1175 | * @throws ConnectionException If no transaction is active. | ||
| 1176 | */ | ||
| 1177 | public function isRollbackOnly(): bool | ||
| 1178 | { | ||
| 1179 | if ($this->transactionNestingLevel === 0) { | ||
| 1180 | throw NoActiveTransaction::new(); | ||
| 1181 | } | ||
| 1182 | |||
| 1183 | return $this->isRollbackOnly; | ||
| 1184 | } | ||
| 1185 | |||
| 1186 | /** | ||
| 1187 | * Converts a given value to its database representation according to the conversion | ||
| 1188 | * rules of a specific DBAL mapping type. | ||
| 1189 | * | ||
| 1190 | * @param mixed $value The value to convert. | ||
| 1191 | * @param string $type The name of the DBAL mapping type. | ||
| 1192 | * | ||
| 1193 | * @return mixed The converted value. | ||
| 1194 | * | ||
| 1195 | * @throws Exception | ||
| 1196 | */ | ||
| 1197 | public function convertToDatabaseValue(mixed $value, string $type): mixed | ||
| 1198 | { | ||
| 1199 | return Type::getType($type)->convertToDatabaseValue($value, $this->getDatabasePlatform()); | ||
| 1200 | } | ||
| 1201 | |||
| 1202 | /** | ||
| 1203 | * Converts a given value to its PHP representation according to the conversion | ||
| 1204 | * rules of a specific DBAL mapping type. | ||
| 1205 | * | ||
| 1206 | * @param mixed $value The value to convert. | ||
| 1207 | * @param string $type The name of the DBAL mapping type. | ||
| 1208 | * | ||
| 1209 | * @return mixed The converted type. | ||
| 1210 | * | ||
| 1211 | * @throws Exception | ||
| 1212 | */ | ||
| 1213 | public function convertToPHPValue(mixed $value, string $type): mixed | ||
| 1214 | { | ||
| 1215 | return Type::getType($type)->convertToPHPValue($value, $this->getDatabasePlatform()); | ||
| 1216 | } | ||
| 1217 | |||
| 1218 | /** | ||
| 1219 | * Binds a set of parameters, some or all of which are typed with a PDO binding type | ||
| 1220 | * or DBAL mapping type, to a given statement. | ||
| 1221 | * | ||
| 1222 | * @param list<mixed>|array<string, mixed> $params | ||
| 1223 | * @param array<int, string|ParameterType|Type>|array<string, string|ParameterType|Type> $types | ||
| 1224 | * | ||
| 1225 | * @throws Exception | ||
| 1226 | */ | ||
| 1227 | private function bindParameters(DriverStatement $stmt, array $params, array $types): void | ||
| 1228 | { | ||
| 1229 | // Check whether parameters are positional or named. Mixing is not allowed. | ||
| 1230 | if (is_int(key($params))) { | ||
| 1231 | $bindIndex = 1; | ||
| 1232 | |||
| 1233 | foreach ($params as $key => $value) { | ||
| 1234 | if (array_key_exists($key, $types)) { | ||
| 1235 | $type = $types[$key]; | ||
| 1236 | [$value, $bindingType] = $this->getBindingInfo($value, $type); | ||
| 1237 | } else { | ||
| 1238 | $bindingType = ParameterType::STRING; | ||
| 1239 | } | ||
| 1240 | |||
| 1241 | $stmt->bindValue($bindIndex, $value, $bindingType); | ||
| 1242 | |||
| 1243 | ++$bindIndex; | ||
| 1244 | } | ||
| 1245 | } else { | ||
| 1246 | // Named parameters | ||
| 1247 | foreach ($params as $name => $value) { | ||
| 1248 | if (array_key_exists($name, $types)) { | ||
| 1249 | $type = $types[$name]; | ||
| 1250 | [$value, $bindingType] = $this->getBindingInfo($value, $type); | ||
| 1251 | } else { | ||
| 1252 | $bindingType = ParameterType::STRING; | ||
| 1253 | } | ||
| 1254 | |||
| 1255 | $stmt->bindValue($name, $value, $bindingType); | ||
| 1256 | } | ||
| 1257 | } | ||
| 1258 | } | ||
| 1259 | |||
| 1260 | /** | ||
| 1261 | * Gets the binding type of a given type. | ||
| 1262 | * | ||
| 1263 | * @param mixed $value The value to bind. | ||
| 1264 | * @param string|ParameterType|Type $type The type to bind. | ||
| 1265 | * | ||
| 1266 | * @return array{mixed, ParameterType} [0] => the (escaped) value, [1] => the binding type. | ||
| 1267 | * | ||
| 1268 | * @throws Exception | ||
| 1269 | */ | ||
| 1270 | private function getBindingInfo(mixed $value, string|ParameterType|Type $type): array | ||
| 1271 | { | ||
| 1272 | if (is_string($type)) { | ||
| 1273 | $type = Type::getType($type); | ||
| 1274 | } | ||
| 1275 | |||
| 1276 | if ($type instanceof Type) { | ||
| 1277 | $value = $type->convertToDatabaseValue($value, $this->getDatabasePlatform()); | ||
| 1278 | $bindingType = $type->getBindingType(); | ||
| 1279 | } else { | ||
| 1280 | $bindingType = $type; | ||
| 1281 | } | ||
| 1282 | |||
| 1283 | return [$value, $bindingType]; | ||
| 1284 | } | ||
| 1285 | |||
| 1286 | /** | ||
| 1287 | * Creates a new instance of a SQL query builder. | ||
| 1288 | */ | ||
| 1289 | public function createQueryBuilder(): QueryBuilder | ||
| 1290 | { | ||
| 1291 | return new Query\QueryBuilder($this); | ||
| 1292 | } | ||
| 1293 | |||
| 1294 | /** | ||
| 1295 | * @internal | ||
| 1296 | * | ||
| 1297 | * @param list<mixed>|array<string,mixed> $params | ||
| 1298 | * @psalm-param WrapperParameterTypeArray $types | ||
| 1299 | */ | ||
| 1300 | final public function convertExceptionDuringQuery( | ||
| 1301 | Driver\Exception $e, | ||
| 1302 | string $sql, | ||
| 1303 | array $params = [], | ||
| 1304 | array $types = [], | ||
| 1305 | ): DriverException { | ||
| 1306 | return $this->handleDriverException($e, new Query($sql, $params, $types)); | ||
| 1307 | } | ||
| 1308 | |||
| 1309 | /** @internal */ | ||
| 1310 | final public function convertException(Driver\Exception $e): DriverException | ||
| 1311 | { | ||
| 1312 | return $this->handleDriverException($e, null); | ||
| 1313 | } | ||
| 1314 | |||
| 1315 | /** | ||
| 1316 | * @param list<mixed>|array<string, mixed> $params | ||
| 1317 | * @psalm-param WrapperParameterTypeArray $types | ||
| 1318 | * | ||
| 1319 | * @return array{ | ||
| 1320 | * string, | ||
| 1321 | * list<mixed>|array<string, mixed>, | ||
| 1322 | * array<int<0, max>, string|ParameterType|Type>|array<string, string|ParameterType|Type> | ||
| 1323 | * } | ||
| 1324 | */ | ||
| 1325 | private function expandArrayParameters(string $sql, array $params, array $types): array | ||
| 1326 | { | ||
| 1327 | $needsConversion = false; | ||
| 1328 | $nonArrayTypes = []; | ||
| 1329 | |||
| 1330 | if (is_string(key($params))) { | ||
| 1331 | $needsConversion = true; | ||
| 1332 | } else { | ||
| 1333 | foreach ($types as $key => $type) { | ||
| 1334 | if ($type instanceof ArrayParameterType) { | ||
| 1335 | $needsConversion = true; | ||
| 1336 | break; | ||
| 1337 | } | ||
| 1338 | |||
| 1339 | $nonArrayTypes[$key] = $type; | ||
| 1340 | } | ||
| 1341 | } | ||
| 1342 | |||
| 1343 | if (! $needsConversion) { | ||
| 1344 | return [$sql, $params, $nonArrayTypes]; | ||
| 1345 | } | ||
| 1346 | |||
| 1347 | $this->parser ??= $this->getDatabasePlatform()->createSQLParser(); | ||
| 1348 | $visitor = new ExpandArrayParameters($params, $types); | ||
| 1349 | |||
| 1350 | $this->parser->parse($sql, $visitor); | ||
| 1351 | |||
| 1352 | return [ | ||
| 1353 | $visitor->getSQL(), | ||
| 1354 | $visitor->getParameters(), | ||
| 1355 | $visitor->getTypes(), | ||
| 1356 | ]; | ||
| 1357 | } | ||
| 1358 | |||
| 1359 | private function handleDriverException( | ||
| 1360 | Driver\Exception $driverException, | ||
| 1361 | ?Query $query, | ||
| 1362 | ): DriverException { | ||
| 1363 | $this->exceptionConverter ??= $this->driver->getExceptionConverter(); | ||
| 1364 | $exception = $this->exceptionConverter->convert($driverException, $query); | ||
| 1365 | |||
| 1366 | if ($exception instanceof ConnectionLost) { | ||
| 1367 | $this->close(); | ||
| 1368 | } | ||
| 1369 | |||
| 1370 | return $exception; | ||
| 1371 | } | ||
| 1372 | } | ||
