summaryrefslogtreecommitdiff
path: root/vendor/doctrine/dbal/src/Connection.php
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/doctrine/dbal/src/Connection.php')
-rw-r--r--vendor/doctrine/dbal/src/Connection.php1372
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
3declare(strict_types=1);
4
5namespace Doctrine\DBAL;
6
7use Closure;
8use Doctrine\DBAL\Cache\ArrayResult;
9use Doctrine\DBAL\Cache\CacheException;
10use Doctrine\DBAL\Cache\Exception\NoResultDriverConfigured;
11use Doctrine\DBAL\Cache\QueryCacheProfile;
12use Doctrine\DBAL\Connection\StaticServerVersionProvider;
13use Doctrine\DBAL\Driver\API\ExceptionConverter;
14use Doctrine\DBAL\Driver\Connection as DriverConnection;
15use Doctrine\DBAL\Driver\Statement as DriverStatement;
16use Doctrine\DBAL\Exception\CommitFailedRollbackOnly;
17use Doctrine\DBAL\Exception\ConnectionLost;
18use Doctrine\DBAL\Exception\DriverException;
19use Doctrine\DBAL\Exception\NoActiveTransaction;
20use Doctrine\DBAL\Exception\SavepointsNotSupported;
21use Doctrine\DBAL\Platforms\AbstractPlatform;
22use Doctrine\DBAL\Query\Expression\ExpressionBuilder;
23use Doctrine\DBAL\Query\QueryBuilder;
24use Doctrine\DBAL\Schema\AbstractSchemaManager;
25use Doctrine\DBAL\Schema\DefaultSchemaManagerFactory;
26use Doctrine\DBAL\Schema\SchemaManagerFactory;
27use Doctrine\DBAL\SQL\Parser;
28use Doctrine\DBAL\Types\Type;
29use Doctrine\Deprecations\Deprecation;
30use InvalidArgumentException;
31use SensitiveParameter;
32use Throwable;
33use Traversable;
34
35use function array_key_exists;
36use function array_merge;
37use function assert;
38use function count;
39use function implode;
40use function is_array;
41use function is_int;
42use function is_string;
43use function key;
44use 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 */
55class 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}