diff options
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 | } | ||