Primo rilascio

This commit is contained in:
2026-03-07 00:15:59 +01:00
commit dd5282dd69
609 changed files with 75246 additions and 0 deletions

View File

@@ -0,0 +1,6 @@
This repository has [guidelines specific to testing][testing guidelines], and
Doctrine has [general contributing guidelines][contributor workflow], make
sure you follow both.
[contributor workflow]: https://www.doctrine-project.org/contribute/index.html
[testing guidelines]: https://www.doctrine-project.org/projects/doctrine-dbal/en/stable/reference/testing.html

19
backend/vendor/doctrine/dbal/LICENSE vendored Normal file
View File

@@ -0,0 +1,19 @@
Copyright (c) 2006-2018 Doctrine Project
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

55
backend/vendor/doctrine/dbal/README.md vendored Normal file
View File

@@ -0,0 +1,55 @@
# Doctrine DBAL
| [5.0-dev][5.0] | [4.3-dev][4.3] | [4.2][4.2] | [3.10][3.10] | [3.9][3.9] |
|:---------------------------------------------------:|:---------------------------------------------------:|:---------------------------------------------------:|:-----------------------------------------------------:|:---------------------------------------------------:|
| [![GitHub Actions][GA 5.0 image]][GA 5.0] | [![GitHub Actions][GA 4.3 image]][GA 4.3] | [![GitHub Actions][GA 4.2 image]][GA 4.2] | [![GitHub Actions][GA 3.10 image]][GA 3.10] | [![GitHub Actions][GA 3.9 image]][GA 3.9] |
| [![AppVeyor][AppVeyor 5.0 image]][AppVeyor 5.0] | [![AppVeyor][AppVeyor 4.3 image]][AppVeyor 4.3] | [![AppVeyor][AppVeyor 4.2 image]][AppVeyor 4.2] | [![AppVeyor][AppVeyor 3.10 image]][AppVeyor 3.10] | [![AppVeyor][AppVeyor 3.9 image]][AppVeyor 3.9] |
| [![Code Coverage][Coverage 5.0 image]][CodeCov 5.0] | [![Code Coverage][Coverage 4.3 image]][CodeCov 4.3] | [![Code Coverage][Coverage 4.2 image]][CodeCov 4.2] | [![Code Coverage][Coverage 3.10 image]][CodeCov 3.10] | [![Code Coverage][Coverage 3.9 image]][CodeCov 3.9] |
Powerful ***D***ata***B***ase ***A***bstraction ***L***ayer with many features for database schema introspection and schema management.
## More resources:
* [Website](http://www.doctrine-project.org/projects/dbal.html)
* [Documentation](http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/)
* [Issue Tracker](https://github.com/doctrine/dbal/issues)
[Coverage 5.0 image]: https://codecov.io/gh/doctrine/dbal/branch/5.0.x/graph/badge.svg
[5.0]: https://github.com/doctrine/dbal/tree/5.0.x
[CodeCov 5.0]: https://codecov.io/gh/doctrine/dbal/branch/5.0.x
[AppVeyor 5.0]: https://ci.appveyor.com/project/doctrine/dbal/branch/5.0.x
[AppVeyor 5.0 image]: https://ci.appveyor.com/api/projects/status/i88kitq8qpbm0vie/branch/5.0.x?svg=true
[GA 5.0]: https://github.com/doctrine/dbal/actions?query=workflow%3A%22Continuous+Integration%22+branch%3A5.0.x
[GA 5.0 image]: https://github.com/doctrine/dbal/actions/workflows/continuous-integration.yml/badge.svg?branch=5.0.x
[Coverage 4.3 image]: https://codecov.io/gh/doctrine/dbal/branch/4.3.x/graph/badge.svg
[4.3]: https://github.com/doctrine/dbal/tree/4.3.x
[CodeCov 4.3]: https://codecov.io/gh/doctrine/dbal/branch/4.3.x
[AppVeyor 4.3]: https://ci.appveyor.com/project/doctrine/dbal/branch/4.3.x
[AppVeyor 4.3 image]: https://ci.appveyor.com/api/projects/status/i88kitq8qpbm0vie/branch/4.3.x?svg=true
[GA 4.3]: https://github.com/doctrine/dbal/actions?query=workflow%3A%22Continuous+Integration%22+branch%3A4.3.x
[GA 4.3 image]: https://github.com/doctrine/dbal/actions/workflows/continuous-integration.yml/badge.svg?branch=4.3.x
[Coverage 4.2 image]: https://codecov.io/gh/doctrine/dbal/branch/4.2.x/graph/badge.svg
[4.2]: https://github.com/doctrine/dbal/tree/4.2.x
[CodeCov 4.2]: https://codecov.io/gh/doctrine/dbal/branch/4.2.x
[AppVeyor 4.2]: https://ci.appveyor.com/project/doctrine/dbal/branch/4.2.x
[AppVeyor 4.2 image]: https://ci.appveyor.com/api/projects/status/i88kitq8qpbm0vie/branch/4.2.x?svg=true
[GA 4.2]: https://github.com/doctrine/dbal/actions?query=workflow%3A%22Continuous+Integration%22+branch%3A4.2.x
[GA 4.2 image]: https://github.com/doctrine/dbal/actions/workflows/continuous-integration.yml/badge.svg?branch=4.2.x
[Coverage 3.10 image]: https://codecov.io/gh/doctrine/dbal/branch/3.10.x/graph/badge.svg
[3.10]: https://github.com/doctrine/dbal/tree/3.10.x
[CodeCov 3.10]: https://codecov.io/gh/doctrine/dbal/branch/3.10.x
[AppVeyor 3.10]: https://ci.appveyor.com/project/doctrine/dbal/branch/3.10.x
[AppVeyor 3.10 image]: https://ci.appveyor.com/api/projects/status/i88kitq8qpbm0vie/branch/3.10.x?svg=true
[GA 3.10]: https://github.com/doctrine/dbal/actions?query=workflow%3A%22Continuous+Integration%22+branch%3A3.10.x
[GA 3.10 image]: https://github.com/doctrine/dbal/actions/workflows/continuous-integration.yml/badge.svg?branch=3.10.x
[Coverage 3.9 image]: https://codecov.io/gh/doctrine/dbal/branch/3.9.x/graph/badge.svg
[3.9]: https://github.com/doctrine/dbal/tree/3.9.x
[CodeCov 3.9]: https://codecov.io/gh/doctrine/dbal/branch/3.9.x
[AppVeyor 3.9]: https://ci.appveyor.com/project/doctrine/dbal/branch/3.9.x
[AppVeyor 3.9 image]: https://ci.appveyor.com/api/projects/status/i88kitq8qpbm0vie/branch/3.9.x?svg=true
[GA 3.9]: https://github.com/doctrine/dbal/actions?query=workflow%3A%22Continuous+Integration%22+branch%3A3.9.x
[GA 3.9 image]: https://github.com/doctrine/dbal/actions/workflows/continuous-integration.yml/badge.svg?branch=3.9.x

View File

@@ -0,0 +1,68 @@
{
"name": "doctrine/dbal",
"type": "library",
"description": "Powerful PHP database abstraction layer (DBAL) with many features for database schema introspection and management.",
"keywords": [
"abstraction",
"database",
"dbal",
"db2",
"mariadb",
"mssql",
"mysql",
"pgsql",
"postgresql",
"oci8",
"oracle",
"pdo",
"queryobject",
"sasql",
"sql",
"sqlite",
"sqlserver",
"sqlsrv"
],
"homepage": "https://www.doctrine-project.org/projects/dbal.html",
"license": "MIT",
"authors": [
{"name": "Guilherme Blanco", "email": "guilhermeblanco@gmail.com"},
{"name": "Roman Borschel", "email": "roman@code-factory.org"},
{"name": "Benjamin Eberlei", "email": "kontakt@beberlei.de"},
{"name": "Jonathan Wage", "email": "jonwage@gmail.com"}
],
"require": {
"php": "^8.1",
"doctrine/deprecations": "^0.5.3|^1",
"psr/cache": "^1|^2|^3",
"psr/log": "^1|^2|^3"
},
"require-dev": {
"doctrine/coding-standard": "12.0.0",
"fig/log-test": "^1",
"jetbrains/phpstorm-stubs": "2023.2",
"phpstan/phpstan": "2.1.1",
"phpstan/phpstan-phpunit": "2.0.3",
"phpstan/phpstan-strict-rules": "^2",
"phpunit/phpunit": "10.5.39",
"slevomat/coding-standard": "8.13.1",
"squizlabs/php_codesniffer": "3.10.2",
"symfony/cache": "^6.3.8|^7.0",
"symfony/console": "^5.4|^6.3|^7.0"
},
"suggest": {
"symfony/console": "For helpful console commands such as SQL execution and import of files."
},
"config": {
"sort-packages": true,
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true,
"composer/package-versions-deprecated": true
}
},
"autoload": {
"psr-4": { "Doctrine\\DBAL\\": "src" }
},
"autoload-dev": {
"psr-4": { "Doctrine\\DBAL\\Tests\\": "tests" }
}
}

View File

@@ -0,0 +1,91 @@
parameters:
ignoreErrors:
-
message: '#^Method Doctrine\\DBAL\\Driver\\IBMDB2\\Connection\:\:exec\(\) never returns numeric\-string so it can be removed from the return type\.$#'
identifier: return.unusedType
count: 1
path: src/Driver/IBMDB2/Connection.php
-
message: '#^Method Doctrine\\DBAL\\Driver\\OCI8\\Connection\:\:exec\(\) never returns numeric\-string so it can be removed from the return type\.$#'
identifier: return.unusedType
count: 1
path: src/Driver/OCI8/Connection.php
-
message: '#^Method Doctrine\\DBAL\\Driver\\OCI8\\Result\:\:fetchAllAssociative\(\) should return list\<array\<string, mixed\>\> but returns array\<mixed\>\.$#'
identifier: return.type
count: 1
path: src/Driver/OCI8/Result.php
-
message: '#^Method Doctrine\\DBAL\\Driver\\OCI8\\Result\:\:fetchAllNumeric\(\) should return list\<list\<mixed\>\> but returns array\<mixed\>\.$#'
identifier: return.type
count: 1
path: src/Driver/OCI8/Result.php
-
message: '#^Method Doctrine\\DBAL\\Driver\\PDO\\Result\:\:fetchAll\(\) should return list\<mixed\> but returns array\.$#'
identifier: return.type
count: 1
path: src/Driver/PDO/Result.php
-
message: '#^Method Doctrine\\DBAL\\Driver\\PgSQL\\Result\:\:fetchAllAssociative\(\) should return list\<array\<string, mixed\>\> but returns array\<int, array\<string, mixed\>\>\.$#'
identifier: return.type
count: 1
path: src/Driver/PgSQL/Result.php
-
message: '#^Method Doctrine\\DBAL\\Driver\\PgSQL\\Result\:\:fetchAllNumeric\(\) should return list\<list\<mixed\>\> but returns array\<int, list\<mixed\>\>\.$#'
identifier: return.type
count: 1
path: src/Driver/PgSQL/Result.php
-
message: '#^Method Doctrine\\DBAL\\Driver\\PgSQL\\Result\:\:fetchFirstColumn\(\) should return list\<mixed\> but returns array\<int, bool\|float\|int\|string\|null\>\.$#'
identifier: return.type
count: 1
path: src/Driver/PgSQL/Result.php
-
message: '#^Method Doctrine\\DBAL\\Driver\\SQLite3\\Result\:\:fetchNumeric\(\) should return list\<mixed\>\|false but returns array\|false\.$#'
identifier: return.type
count: 1
path: src/Driver/SQLite3/Result.php
-
message: '#^Template type T is declared as covariant, but occurs in invariant position in property Doctrine\\DBAL\\Schema\\AbstractSchemaManager\:\:\$platform\.$#'
identifier: generics.variance
count: 1
path: src/Schema/AbstractSchemaManager.php
-
message: '#^Loose comparison via "\!\=" is not allowed\.$#'
identifier: notEqual.notAllowed
count: 1
path: src/Schema/ColumnDiff.php
-
message: '#^Method Doctrine\\DBAL\\Schema\\SQLiteSchemaManager\:\:addDetailsToTableForeignKeyColumns\(\) should return list\<array\<string, mixed\>\> but returns array\<int\<0, max\>, array\<string, mixed\>\>\.$#'
identifier: return.type
count: 1
path: src/Schema/SQLiteSchemaManager.php
-
message: '#^Offset string might not exist on array\{application_name\?\: string, charset\?\: string, dbname\?\: string, defaultTableOptions\?\: array\<string, mixed\>, driver\?\: ''ibm_db2''\|''mysqli''\|''oci8''\|''pdo_mysql''\|''pdo_oci''\|''pdo_pgsql''\|''pdo_sqlite''\|''pdo_sqlsrv''\|''pgsql''\|''sqlite3''\|''sqlsrv'', driverClass\?\: class\-string\<Doctrine\\DBAL\\Driver\>, driverOptions\?\: array\<mixed\>, host\?\: string, \.\.\.\}\.$#'
identifier: offsetAccess.notFound
count: 1
path: tests/DriverManagerTest.php
-
message: '#^Call to new Doctrine\\DBAL\\Driver\\PgSQL\\Result\(\) on a separate line has no effect\.$#'
identifier: new.resultUnused
count: 1
path: tests/Functional/Driver/PgSQL/ResultTest.php
-
message: '#^Call to function array_filter\(\) requires parameter \#2 to be passed to avoid loose comparison semantics\.$#'
identifier: arrayFilter.strict
count: 1
path: tests/Functional/Schema/MySQL/JsonCollationTest.php

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL;
enum ArrayParameterType
{
/**
* Represents an array of ints to be expanded by Doctrine SQL parsing.
*/
case INTEGER;
/**
* Represents an array of strings to be expanded by Doctrine SQL parsing.
*/
case STRING;
/**
* Represents an array of ascii strings to be expanded by Doctrine SQL parsing.
*/
case ASCII;
/**
* Represents an array of ascii strings to be expanded by Doctrine SQL parsing.
*/
case BINARY;
/** @internal */
public static function toElementParameterType(self $type): ParameterType
{
return match ($type) {
self::INTEGER => ParameterType::INTEGER,
self::STRING => ParameterType::STRING,
self::ASCII => ParameterType::ASCII,
self::BINARY => ParameterType::BINARY,
};
}
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\ArrayParameters;
use Throwable;
/** @internal */
interface Exception extends Throwable
{
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\ArrayParameters\Exception;
use Doctrine\DBAL\ArrayParameters\Exception;
use LogicException;
use function sprintf;
class MissingNamedParameter extends LogicException implements Exception
{
public static function new(string $name): self
{
return new self(
sprintf('Named parameter "%s" does not have a bound value.', $name),
);
}
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\ArrayParameters\Exception;
use Doctrine\DBAL\ArrayParameters\Exception;
use LogicException;
use function sprintf;
/** @internal */
class MissingPositionalParameter extends LogicException implements Exception
{
public static function new(int $index): self
{
return new self(
sprintf('Positional parameter at index %d does not have a bound value.', $index),
);
}
}

View File

@@ -0,0 +1,136 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Cache;
use Doctrine\DBAL\Driver\FetchUtils;
use Doctrine\DBAL\Driver\Result;
use Doctrine\DBAL\Exception\InvalidColumnIndex;
use function array_combine;
use function array_keys;
use function array_map;
use function array_values;
use function count;
/** @internal The class is internal to the caching layer implementation. */
final class ArrayResult implements Result
{
private int $num = 0;
/**
* @param list<string> $columnNames The names of the result columns. Must be non-empty.
* @param list<list<mixed>> $rows The rows of the result. Each row must have the same number of columns
* as the number of column names.
*/
public function __construct(
private readonly array $columnNames,
private array $rows,
) {
}
public function fetchNumeric(): array|false
{
return $this->fetch();
}
public function fetchAssociative(): array|false
{
$row = $this->fetch();
if ($row === false) {
return false;
}
return array_combine($this->columnNames, $row);
}
public function fetchOne(): mixed
{
$row = $this->fetch();
if ($row === false) {
return false;
}
return $row[0];
}
/**
* {@inheritDoc}
*/
public function fetchAllNumeric(): array
{
return FetchUtils::fetchAllNumeric($this);
}
/**
* {@inheritDoc}
*/
public function fetchAllAssociative(): array
{
return FetchUtils::fetchAllAssociative($this);
}
/**
* {@inheritDoc}
*/
public function fetchFirstColumn(): array
{
return FetchUtils::fetchFirstColumn($this);
}
public function rowCount(): int
{
return count($this->rows);
}
public function columnCount(): int
{
return count($this->columnNames);
}
public function getColumnName(int $index): string
{
return $this->columnNames[$index] ?? throw InvalidColumnIndex::new($index);
}
public function free(): void
{
$this->rows = [];
}
/** @return array{list<string>, list<list<mixed>>} */
public function __serialize(): array
{
return [$this->columnNames, $this->rows];
}
/** @param mixed[] $data */
public function __unserialize(array $data): void
{
// Handle objects serialized with DBAL 4.1 and earlier.
if (isset($data["\0" . self::class . "\0data"])) {
/** @var list<array<string, mixed>> $legacyData */
$legacyData = $data["\0" . self::class . "\0data"];
$this->columnNames = array_keys($legacyData[0] ?? []);
$this->rows = array_map(array_values(...), $legacyData);
return;
}
[$this->columnNames, $this->rows] = $data;
}
/** @return list<mixed>|false */
private function fetch(): array|false
{
if (! isset($this->rows[$this->num])) {
return false;
}
return $this->rows[$this->num++];
}
}

View File

@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Cache;
use Doctrine\DBAL\Exception;
class CacheException extends \Exception implements Exception
{
}

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Cache\Exception;
use Doctrine\DBAL\Cache\CacheException;
final class NoCacheKey extends CacheException
{
public static function new(): self
{
return new self('No cache key was set.');
}
}

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Cache\Exception;
use Doctrine\DBAL\Cache\CacheException;
final class NoResultDriverConfigured extends CacheException
{
public static function new(): self
{
return new self('Trying to cache a query but no result driver is configured.');
}
}

View File

@@ -0,0 +1,91 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Cache;
use Doctrine\DBAL\Cache\Exception\NoCacheKey;
use Doctrine\DBAL\Connection;
use Psr\Cache\CacheItemPoolInterface;
use function hash;
use function serialize;
use function sha1;
/**
* Query Cache Profile handles the data relevant for query caching.
*
* It is a value object, setter methods return NEW instances.
*
* @phpstan-import-type WrapperParameterType from Connection
*/
class QueryCacheProfile
{
public function __construct(
private readonly int $lifetime = 0,
private readonly ?string $cacheKey = null,
private readonly ?CacheItemPoolInterface $resultCache = null,
) {
}
public function getResultCache(): ?CacheItemPoolInterface
{
return $this->resultCache;
}
public function getLifetime(): int
{
return $this->lifetime;
}
/** @throws CacheException */
public function getCacheKey(): string
{
if ($this->cacheKey === null) {
throw NoCacheKey::new();
}
return $this->cacheKey;
}
/**
* Generates the real cache key from query, params, types and connection parameters.
*
* @param list<mixed>|array<string, mixed> $params
* @param array<string, mixed> $connectionParams
* @phpstan-param array<int, WrapperParameterType>|array<string, WrapperParameterType> $types
*
* @return array{string, string}
*/
public function generateCacheKeys(string $sql, array $params, array $types, array $connectionParams = []): array
{
if (isset($connectionParams['password'])) {
unset($connectionParams['password']);
}
$realCacheKey = 'query=' . $sql .
'&params=' . serialize($params) .
'&types=' . serialize($types) .
'&connectionParams=' . hash('sha256', serialize($connectionParams));
// should the key be automatically generated using the inputs or is the cache key set?
$cacheKey = $this->cacheKey ?? sha1($realCacheKey);
return [$cacheKey, $realCacheKey];
}
public function setResultCache(CacheItemPoolInterface $cache): QueryCacheProfile
{
return new QueryCacheProfile($this->lifetime, $this->cacheKey, $cache);
}
public function setCacheKey(?string $cacheKey): self
{
return new QueryCacheProfile($this->lifetime, $cacheKey, $this->resultCache);
}
public function setLifetime(int $lifetime): self
{
return new QueryCacheProfile($lifetime, $this->cacheKey, $this->resultCache);
}
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL;
/**
* Contains portable column case conversions.
*/
enum ColumnCase
{
/**
* Convert column names to upper case.
*/
case UPPER;
/**
* Convert column names to lower case.
*/
case LOWER;
}

View File

@@ -0,0 +1,156 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL;
use Doctrine\DBAL\Driver\Middleware;
use Doctrine\DBAL\Exception\InvalidArgumentException;
use Doctrine\DBAL\Schema\SchemaManagerFactory;
use Psr\Cache\CacheItemPoolInterface;
/**
* Configuration container for the Doctrine DBAL.
*/
class Configuration
{
/** @var Middleware[] */
private array $middlewares = [];
/**
* The cache driver implementation that is used for query result caching.
*/
private ?CacheItemPoolInterface $resultCache = null;
/**
* The callable to use to filter schema assets.
*
* @var callable
*/
protected $schemaAssetsFilter;
/**
* The default auto-commit mode for connections.
*/
protected bool $autoCommit = true;
private ?SchemaManagerFactory $schemaManagerFactory = null;
public function __construct()
{
$this->schemaAssetsFilter = static function (): bool {
return true;
};
}
/**
* Gets the cache driver implementation that is used for query result caching.
*/
public function getResultCache(): ?CacheItemPoolInterface
{
return $this->resultCache;
}
/**
* Sets the cache driver implementation that is used for query result caching.
*/
public function setResultCache(CacheItemPoolInterface $cache): void
{
$this->resultCache = $cache;
}
/**
* Sets the callable to use to filter schema assets.
*/
public function setSchemaAssetsFilter(callable $schemaAssetsFilter): void
{
$this->schemaAssetsFilter = $schemaAssetsFilter;
}
/**
* Returns the callable to use to filter schema assets.
*/
public function getSchemaAssetsFilter(): callable
{
return $this->schemaAssetsFilter;
}
/**
* Sets the default auto-commit mode for connections.
*
* If a connection is in auto-commit mode, then all its SQL statements will be executed and committed as individual
* transactions. Otherwise, its SQL statements are grouped into transactions that are terminated by a call to either
* the method commit or the method rollback. By default, new connections are in auto-commit mode.
*
* @see getAutoCommit
*
* @param bool $autoCommit True to enable auto-commit mode; false to disable it
*/
public function setAutoCommit(bool $autoCommit): void
{
$this->autoCommit = $autoCommit;
}
/**
* Returns the default auto-commit mode for connections.
*
* @see setAutoCommit
*
* @return bool True if auto-commit mode is enabled by default for connections, false otherwise.
*/
public function getAutoCommit(): bool
{
return $this->autoCommit;
}
/**
* @param Middleware[] $middlewares
*
* @return $this
*/
public function setMiddlewares(array $middlewares): self
{
$this->middlewares = $middlewares;
return $this;
}
/** @return Middleware[] */
public function getMiddlewares(): array
{
return $this->middlewares;
}
public function getSchemaManagerFactory(): ?SchemaManagerFactory
{
return $this->schemaManagerFactory;
}
/** @return $this */
public function setSchemaManagerFactory(SchemaManagerFactory $schemaManagerFactory): self
{
$this->schemaManagerFactory = $schemaManagerFactory;
return $this;
}
/** @return true */
public function getDisableTypeComments(): bool
{
return true;
}
/**
* @param true $disableTypeComments
*
* @return $this
*/
public function setDisableTypeComments(bool $disableTypeComments): self
{
if (! $disableTypeComments) {
throw new InvalidArgumentException('Column comments cannot be enabled anymore.');
}
return $this;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Connection;
use Doctrine\DBAL\ServerVersionProvider;
class StaticServerVersionProvider implements ServerVersionProvider
{
public function __construct(private readonly string $version)
{
}
public function getServerVersion(): string
{
return $this->version;
}
}

View File

@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL;
class ConnectionException extends \Exception implements Exception
{
}

View File

@@ -0,0 +1,327 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Connections;
use Doctrine\DBAL\Configuration;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Driver;
use Doctrine\DBAL\Driver\Connection as DriverConnection;
use Doctrine\DBAL\Driver\Exception as DriverException;
use Doctrine\DBAL\DriverManager;
use Doctrine\DBAL\Exception;
use Doctrine\DBAL\Statement;
use InvalidArgumentException;
use SensitiveParameter;
use function array_rand;
use function assert;
use function count;
/**
* Primary-Replica Connection
*
* Connection can be used with primary-replica setups.
*
* Important for the understanding of this connection should be how and when
* it picks the replica or primary.
*
* 1. Replica if primary was never picked before and ONLY if 'getWrappedConnection'
* or 'executeQuery' is used.
* 2. Primary picked when 'executeStatement', 'insert', 'delete', 'update', 'createSavepoint',
* 'releaseSavepoint', 'beginTransaction', 'rollback', 'commit' or 'prepare' is called.
* 3. If Primary was picked once during the lifetime of the connection it will always get picked afterwards.
* 4. One replica connection is randomly picked ONCE during a request.
*
* ATTENTION: You can write to the replica with this connection if you execute a write query without
* opening up a transaction. For example:
*
* $conn = DriverManager::getConnection(...);
* $conn->executeQuery("DELETE FROM table");
*
* Be aware that Connection#executeQuery is a method specifically for READ
* operations only.
*
* Use Connection#executeStatement for any SQL statement that changes/updates
* state in the database (UPDATE, INSERT, DELETE or DDL statements).
*
* This connection is limited to replica operations using the
* Connection#executeQuery operation only, because it wouldn't be compatible
* with the ORM or SchemaManager code otherwise. Both use all the other
* operations in a context where writes could happen to a replica, which makes
* this restricted approach necessary.
*
* You can manually connect to the primary at any time by calling:
*
* $conn->ensureConnectedToPrimary();
*
* Instantiation through the DriverManager looks like:
*
* @phpstan-import-type Params from DriverManager
* @phpstan-import-type OverrideParams from DriverManager
* @example
*
* $conn = DriverManager::getConnection(array(
* 'wrapperClass' => 'Doctrine\DBAL\Connections\PrimaryReadReplicaConnection',
* 'driver' => 'pdo_mysql',
* 'primary' => array('user' => '', 'password' => '', 'host' => '', 'dbname' => ''),
* 'replica' => array(
* array('user' => 'replica1', 'password' => '', 'host' => '', 'dbname' => ''),
* array('user' => 'replica2', 'password' => '', 'host' => '', 'dbname' => ''),
* )
* ));
*
* You can also pass 'driverOptions' and any other documented option to each of this drivers
* to pass additional information.
*/
class PrimaryReadReplicaConnection extends Connection
{
/**
* Primary and Replica connection (one of the randomly picked replicas).
*
* @var array<string, DriverConnection|null>
*/
protected array $connections = ['primary' => null, 'replica' => null];
/**
* You can keep the replica connection and then switch back to it
* during the request if you know what you are doing.
*/
protected bool $keepReplica = false;
/**
* Creates Primary Replica Connection.
*
* @internal The connection can be only instantiated by the driver manager.
*
* @param array<string, mixed> $params
* @phpstan-param Params $params
*/
public function __construct(array $params, Driver $driver, ?Configuration $config = null)
{
if (! isset($params['replica'], $params['primary'])) {
throw new InvalidArgumentException('primary or replica configuration missing');
}
if (count($params['replica']) === 0) {
throw new InvalidArgumentException('You have to configure at least one replica.');
}
if (isset($params['driver'])) {
$params['primary']['driver'] = $params['driver'];
foreach ($params['replica'] as $replicaKey => $replica) {
$params['replica'][$replicaKey]['driver'] = $params['driver'];
}
}
$this->keepReplica = ! empty($params['keepReplica']);
parent::__construct($params, $driver, $config);
}
/**
* Checks if the connection is currently towards the primary or not.
*/
public function isConnectedToPrimary(): bool
{
return $this->_conn !== null && $this->_conn === $this->connections['primary'];
}
public function connect(?string $connectionName = null): DriverConnection
{
if ($connectionName !== null) {
throw new InvalidArgumentException(
'Passing a connection name as first argument is not supported anymore.'
. ' Use ensureConnectedToPrimary()/ensureConnectedToReplica() instead.',
);
}
return $this->performConnect();
}
protected function performConnect(?string $connectionName = null): DriverConnection
{
$requestedConnectionChange = ($connectionName !== null);
$connectionName ??= 'replica';
if ($connectionName !== 'replica' && $connectionName !== 'primary') {
throw new InvalidArgumentException('Invalid option to connect(), only primary or replica allowed.');
}
// If we have a connection open, and this is not an explicit connection
// change request, then abort right here, because we are already done.
// This prevents writes to the replica in case of "keepReplica" option enabled.
if ($this->_conn !== null && ! $requestedConnectionChange) {
return $this->_conn;
}
$forcePrimaryAsReplica = false;
if ($this->getTransactionNestingLevel() > 0) {
$connectionName = 'primary';
$forcePrimaryAsReplica = true;
}
if (isset($this->connections[$connectionName])) {
$this->_conn = $this->connections[$connectionName];
if ($forcePrimaryAsReplica && ! $this->keepReplica) {
$this->connections['replica'] = $this->_conn;
}
return $this->_conn;
}
if ($connectionName === 'primary') {
$this->connections['primary'] = $this->_conn = $this->connectTo($connectionName);
// Set replica connection to primary to avoid invalid reads
if (! $this->keepReplica) {
$this->connections['replica'] = $this->connections['primary'];
}
} else {
$this->connections['replica'] = $this->_conn = $this->connectTo($connectionName);
}
return $this->_conn;
}
/**
* Connects to the primary node of the database cluster.
*
* All following statements after this will be executed against the primary node.
*/
public function ensureConnectedToPrimary(): void
{
$this->performConnect('primary');
}
/**
* Connects to a replica node of the database cluster.
*
* All following statements after this will be executed against the replica node,
* unless the keepReplica option is set to false and a primary connection
* was already opened.
*/
public function ensureConnectedToReplica(): void
{
$this->performConnect('replica');
}
/**
* Connects to a specific connection.
*
* @throws Exception
*/
protected function connectTo(string $connectionName): DriverConnection
{
$params = $this->getParams();
assert(isset($params['primary']));
if ($connectionName === 'primary') {
$connectionParams = $params['primary'];
} else {
assert(isset($params['replica']));
$connectionParams = $this->chooseReplicaConnectionParameters($params['primary'], $params['replica']);
}
try {
return $this->driver->connect($connectionParams);
} catch (DriverException $e) {
throw $this->convertException($e);
}
}
/**
* @param OverrideParams $primary
* @param array<OverrideParams> $replicas
*
* @return array<string, mixed>
* @phpstan-return OverrideParams
*/
protected function chooseReplicaConnectionParameters(
#[SensitiveParameter]
array $primary,
#[SensitiveParameter]
array $replicas,
): array {
$params = $replicas[array_rand($replicas)];
if (! isset($params['charset']) && isset($primary['charset'])) {
$params['charset'] = $primary['charset'];
}
return $params;
}
/**
* {@inheritDoc}
*/
public function executeStatement(string $sql, array $params = [], array $types = []): int|string
{
$this->ensureConnectedToPrimary();
return parent::executeStatement($sql, $params, $types);
}
public function beginTransaction(): void
{
$this->ensureConnectedToPrimary();
parent::beginTransaction();
}
public function commit(): void
{
$this->ensureConnectedToPrimary();
parent::commit();
}
public function rollBack(): void
{
$this->ensureConnectedToPrimary();
parent::rollBack();
}
public function close(): void
{
unset($this->connections['primary'], $this->connections['replica']);
parent::close();
$this->_conn = null;
$this->connections = ['primary' => null, 'replica' => null];
}
public function createSavepoint(string $savepoint): void
{
$this->ensureConnectedToPrimary();
parent::createSavepoint($savepoint);
}
public function releaseSavepoint(string $savepoint): void
{
$this->ensureConnectedToPrimary();
parent::releaseSavepoint($savepoint);
}
public function rollbackSavepoint(string $savepoint): void
{
$this->ensureConnectedToPrimary();
parent::rollbackSavepoint($savepoint);
}
public function prepare(string $sql): Statement
{
$this->ensureConnectedToPrimary();
return parent::prepare($sql);
}
}

View File

@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL;
use Doctrine\DBAL\Driver\API\ExceptionConverter;
use Doctrine\DBAL\Driver\Connection as DriverConnection;
use Doctrine\DBAL\Driver\Exception;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use SensitiveParameter;
/**
* Driver interface.
* Interface that all DBAL drivers must implement.
*
* @phpstan-import-type Params from DriverManager
*/
interface Driver
{
/**
* Attempts to create a connection with the database.
*
* @param array<string, mixed> $params All connection parameters.
* @phpstan-param Params $params All connection parameters.
*
* @return DriverConnection The database connection.
*
* @throws Exception
*/
public function connect(
#[SensitiveParameter]
array $params,
): DriverConnection;
/**
* Gets the DatabasePlatform instance that provides all the metadata about
* the platform this driver connects to.
*
* @return AbstractPlatform The database platform.
*/
public function getDatabasePlatform(ServerVersionProvider $versionProvider): AbstractPlatform;
/**
* Gets the ExceptionConverter that can be used to convert driver-level exceptions into DBAL exceptions.
*/
public function getExceptionConverter(): ExceptionConverter;
}

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\API;
use Doctrine\DBAL\Driver\Exception;
use Doctrine\DBAL\Exception\DriverException;
use Doctrine\DBAL\Query;
interface ExceptionConverter
{
/**
* Converts a given driver-level exception into a DBAL-level driver exception.
*
* Implementors should use the vendor-specific error code and SQLSTATE of the exception
* and instantiate the most appropriate specialized {@see DriverException} subclass.
*
* @param Exception $exception The driver exception to convert.
* @param Query|null $query The SQL query that triggered the exception, if any.
*
* @return DriverException An instance of {@see DriverException} or one of its subclasses.
*/
public function convert(Exception $exception, ?Query $query): DriverException;
}

View File

@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\API\IBMDB2;
use Doctrine\DBAL\Driver\API\ExceptionConverter as ExceptionConverterInterface;
use Doctrine\DBAL\Driver\Exception;
use Doctrine\DBAL\Exception\ConnectionException;
use Doctrine\DBAL\Exception\DriverException;
use Doctrine\DBAL\Exception\ForeignKeyConstraintViolationException;
use Doctrine\DBAL\Exception\InvalidFieldNameException;
use Doctrine\DBAL\Exception\NonUniqueFieldNameException;
use Doctrine\DBAL\Exception\NotNullConstraintViolationException;
use Doctrine\DBAL\Exception\SyntaxErrorException;
use Doctrine\DBAL\Exception\TableExistsException;
use Doctrine\DBAL\Exception\TableNotFoundException;
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
use Doctrine\DBAL\Query;
/**
* @internal
*
* @link https://www.ibm.com/docs/en/db2/11.5?topic=messages-sql
*/
final class ExceptionConverter implements ExceptionConverterInterface
{
public function convert(Exception $exception, ?Query $query): DriverException
{
return match ($exception->getCode()) {
-104 => new SyntaxErrorException($exception, $query),
-203 => new NonUniqueFieldNameException($exception, $query),
-204 => new TableNotFoundException($exception, $query),
-206 => new InvalidFieldNameException($exception, $query),
-407 => new NotNullConstraintViolationException($exception, $query),
-530,
-531,
-532,
-20356 => new ForeignKeyConstraintViolationException($exception, $query),
-601 => new TableExistsException($exception, $query),
-803 => new UniqueConstraintViolationException($exception, $query),
-1336,
-30082 => new ConnectionException($exception, $query),
default => new DriverException($exception, $query),
};
}
}

View File

@@ -0,0 +1,94 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\API\MySQL;
use Doctrine\DBAL\Driver\API\ExceptionConverter as ExceptionConverterInterface;
use Doctrine\DBAL\Driver\Exception;
use Doctrine\DBAL\Exception\ConnectionException;
use Doctrine\DBAL\Exception\ConnectionLost;
use Doctrine\DBAL\Exception\DatabaseDoesNotExist;
use Doctrine\DBAL\Exception\DeadlockException;
use Doctrine\DBAL\Exception\DriverException;
use Doctrine\DBAL\Exception\ForeignKeyConstraintViolationException;
use Doctrine\DBAL\Exception\InvalidFieldNameException;
use Doctrine\DBAL\Exception\LockWaitTimeoutException;
use Doctrine\DBAL\Exception\NonUniqueFieldNameException;
use Doctrine\DBAL\Exception\NotNullConstraintViolationException;
use Doctrine\DBAL\Exception\SyntaxErrorException;
use Doctrine\DBAL\Exception\TableExistsException;
use Doctrine\DBAL\Exception\TableNotFoundException;
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
use Doctrine\DBAL\Query;
/** @internal */
final class ExceptionConverter implements ExceptionConverterInterface
{
/**
* @link https://dev.mysql.com/doc/mysql-errors/8.0/en/client-error-reference.html
* @link https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html
*/
public function convert(Exception $exception, ?Query $query): DriverException
{
return match ($exception->getCode()) {
1008 => new DatabaseDoesNotExist($exception, $query),
1213 => new DeadlockException($exception, $query),
1205 => new LockWaitTimeoutException($exception, $query),
1050 => new TableExistsException($exception, $query),
1051,
1146 => new TableNotFoundException($exception, $query),
1216,
1217,
1451,
1452,
1701 => new ForeignKeyConstraintViolationException($exception, $query),
1062,
1557,
1569,
1586 => new UniqueConstraintViolationException($exception, $query),
1054,
1166,
1611 => new InvalidFieldNameException($exception, $query),
1052,
1060,
1110 => new NonUniqueFieldNameException($exception, $query),
1064,
1149,
1287,
1341,
1342,
1343,
1344,
1382,
1479,
1541,
1554,
1626 => new SyntaxErrorException($exception, $query),
1044,
1045,
1046,
1049,
1095,
1142,
1143,
1227,
1370,
1429,
2002,
2005,
2054 => new ConnectionException($exception, $query),
2006,
4031 => new ConnectionLost($exception, $query),
1048,
1121,
1138,
1171,
1252,
1263,
1364,
1566 => new NotNullConstraintViolationException($exception, $query),
default => new DriverException($exception, $query),
};
}
}

View File

@@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\API\OCI;
use Doctrine\DBAL\Driver\API\ExceptionConverter as ExceptionConverterInterface;
use Doctrine\DBAL\Driver\Exception;
use Doctrine\DBAL\Driver\OCI8\Exception\Error;
use Doctrine\DBAL\Driver\PDO\Exception as DriverPDOException;
use Doctrine\DBAL\Exception\ConnectionException;
use Doctrine\DBAL\Exception\DatabaseDoesNotExist;
use Doctrine\DBAL\Exception\DatabaseObjectNotFoundException;
use Doctrine\DBAL\Exception\DriverException;
use Doctrine\DBAL\Exception\ForeignKeyConstraintViolationException;
use Doctrine\DBAL\Exception\InvalidFieldNameException;
use Doctrine\DBAL\Exception\NonUniqueFieldNameException;
use Doctrine\DBAL\Exception\NotNullConstraintViolationException;
use Doctrine\DBAL\Exception\SyntaxErrorException;
use Doctrine\DBAL\Exception\TableExistsException;
use Doctrine\DBAL\Exception\TableNotFoundException;
use Doctrine\DBAL\Exception\TransactionRolledBack;
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
use Doctrine\DBAL\Query;
use function assert;
use function count;
use function explode;
use function str_replace;
/** @internal */
final class ExceptionConverter implements ExceptionConverterInterface
{
/** @link http://www.dba-oracle.com/t_error_code_list.htm */
public function convert(Exception $exception, ?Query $query): DriverException
{
return match ($exception->getCode()) {
1,
2299,
38911 => new UniqueConstraintViolationException($exception, $query),
904 => new InvalidFieldNameException($exception, $query),
918,
960 => new NonUniqueFieldNameException($exception, $query),
923 => new SyntaxErrorException($exception, $query),
942 => new TableNotFoundException($exception, $query),
955 => new TableExistsException($exception, $query),
1017,
12545 => new ConnectionException($exception, $query),
1400 => new NotNullConstraintViolationException($exception, $query),
1918 => new DatabaseDoesNotExist($exception, $query),
2091 => (function () use ($exception, $query) {
//SQLSTATE[HY000]: General error: 2091 OCITransCommit: ORA-02091: transaction rolled back
//ORA-00001: unique constraint (DOCTRINE.GH3423_UNIQUE) violated
$lines = explode("\n", $exception->getMessage(), 2);
assert(count($lines) >= 2);
[, $causeError] = $lines;
[$causeCode] = explode(': ', $causeError, 2);
$code = (int) str_replace('ORA-', '', $causeCode);
$sqlState = $exception->getSQLState();
if ($exception instanceof DriverPDOException) {
$why = $this->convert(new DriverPDOException($causeError, $sqlState, $code, $exception), $query);
} else {
$why = $this->convert(new Error($causeError, $sqlState, $code, $exception), $query);
}
return new TransactionRolledBack($why, $query);
})(),
2289,
2443,
4080 => new DatabaseObjectNotFoundException($exception, $query),
2266,
2291,
2292 => new ForeignKeyConstraintViolationException($exception, $query),
default => new DriverException($exception, $query),
};
}
}

View File

@@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\API\PostgreSQL;
use Doctrine\DBAL\Driver\API\ExceptionConverter as ExceptionConverterInterface;
use Doctrine\DBAL\Driver\Exception;
use Doctrine\DBAL\Exception\ConnectionException;
use Doctrine\DBAL\Exception\ConnectionLost;
use Doctrine\DBAL\Exception\DatabaseDoesNotExist;
use Doctrine\DBAL\Exception\DeadlockException;
use Doctrine\DBAL\Exception\DriverException;
use Doctrine\DBAL\Exception\ForeignKeyConstraintViolationException;
use Doctrine\DBAL\Exception\InvalidFieldNameException;
use Doctrine\DBAL\Exception\NonUniqueFieldNameException;
use Doctrine\DBAL\Exception\NotNullConstraintViolationException;
use Doctrine\DBAL\Exception\SchemaDoesNotExist;
use Doctrine\DBAL\Exception\SyntaxErrorException;
use Doctrine\DBAL\Exception\TableExistsException;
use Doctrine\DBAL\Exception\TableNotFoundException;
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
use Doctrine\DBAL\Query;
use function str_contains;
/** @internal */
final class ExceptionConverter implements ExceptionConverterInterface
{
/** @link http://www.postgresql.org/docs/9.4/static/errcodes-appendix.html */
public function convert(Exception $exception, ?Query $query): DriverException
{
switch ($exception->getSQLState()) {
case '40001':
case '40P01':
return new DeadlockException($exception, $query);
case '0A000':
// Foreign key constraint violations during a TRUNCATE operation
// are considered "feature not supported" in PostgreSQL.
if (str_contains($exception->getMessage(), 'truncate')) {
return new ForeignKeyConstraintViolationException($exception, $query);
}
break;
case '23502':
return new NotNullConstraintViolationException($exception, $query);
case '23503':
return new ForeignKeyConstraintViolationException($exception, $query);
case '23505':
return new UniqueConstraintViolationException($exception, $query);
case '3D000':
return new DatabaseDoesNotExist($exception, $query);
case '3F000':
return new SchemaDoesNotExist($exception, $query);
case '42601':
return new SyntaxErrorException($exception, $query);
case '42702':
return new NonUniqueFieldNameException($exception, $query);
case '42703':
return new InvalidFieldNameException($exception, $query);
case '42P01':
return new TableNotFoundException($exception, $query);
case '42P07':
return new TableExistsException($exception, $query);
case '08006':
return new ConnectionException($exception, $query);
}
if (str_contains($exception->getMessage(), 'terminating connection')) {
return new ConnectionLost($exception, $query);
}
return new DriverException($exception, $query);
}
}

View File

@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\API\SQLSrv;
use Doctrine\DBAL\Driver\API\ExceptionConverter as ExceptionConverterInterface;
use Doctrine\DBAL\Driver\Exception;
use Doctrine\DBAL\Exception\ConnectionException;
use Doctrine\DBAL\Exception\DatabaseObjectNotFoundException;
use Doctrine\DBAL\Exception\DriverException;
use Doctrine\DBAL\Exception\ForeignKeyConstraintViolationException;
use Doctrine\DBAL\Exception\InvalidFieldNameException;
use Doctrine\DBAL\Exception\NonUniqueFieldNameException;
use Doctrine\DBAL\Exception\NotNullConstraintViolationException;
use Doctrine\DBAL\Exception\SyntaxErrorException;
use Doctrine\DBAL\Exception\TableExistsException;
use Doctrine\DBAL\Exception\TableNotFoundException;
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
use Doctrine\DBAL\Query;
/**
* @internal
*
* @link https://docs.microsoft.com/en-us/sql/relational-databases/errors-events/database-engine-events-and-errors
*/
final class ExceptionConverter implements ExceptionConverterInterface
{
public function convert(Exception $exception, ?Query $query): DriverException
{
return match ($exception->getCode()) {
102 => new SyntaxErrorException($exception, $query),
207 => new InvalidFieldNameException($exception, $query),
208 => new TableNotFoundException($exception, $query),
209 => new NonUniqueFieldNameException($exception, $query),
515 => new NotNullConstraintViolationException($exception, $query),
547,
4712 => new ForeignKeyConstraintViolationException($exception, $query),
2601,
2627 => new UniqueConstraintViolationException($exception, $query),
2714 => new TableExistsException($exception, $query),
3701,
15151 => new DatabaseObjectNotFoundException($exception, $query),
11001,
18456 => new ConnectionException($exception, $query),
default => new DriverException($exception, $query),
};
}
}

View File

@@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\API\SQLite;
use Doctrine\DBAL\Driver\API\ExceptionConverter as ExceptionConverterInterface;
use Doctrine\DBAL\Driver\Exception;
use Doctrine\DBAL\Exception\ConnectionException;
use Doctrine\DBAL\Exception\DriverException;
use Doctrine\DBAL\Exception\ForeignKeyConstraintViolationException;
use Doctrine\DBAL\Exception\InvalidFieldNameException;
use Doctrine\DBAL\Exception\LockWaitTimeoutException;
use Doctrine\DBAL\Exception\NonUniqueFieldNameException;
use Doctrine\DBAL\Exception\NotNullConstraintViolationException;
use Doctrine\DBAL\Exception\ReadOnlyException;
use Doctrine\DBAL\Exception\SyntaxErrorException;
use Doctrine\DBAL\Exception\TableExistsException;
use Doctrine\DBAL\Exception\TableNotFoundException;
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
use Doctrine\DBAL\Query;
use function str_contains;
/** @internal */
final class ExceptionConverter implements ExceptionConverterInterface
{
/** @link http://www.sqlite.org/c3ref/c_abort.html */
public function convert(Exception $exception, ?Query $query): DriverException
{
if (str_contains($exception->getMessage(), 'database is locked')) {
return new LockWaitTimeoutException($exception, $query);
}
if (
str_contains($exception->getMessage(), 'must be unique') ||
str_contains($exception->getMessage(), 'is not unique') ||
str_contains($exception->getMessage(), 'are not unique') ||
str_contains($exception->getMessage(), 'UNIQUE constraint failed')
) {
return new UniqueConstraintViolationException($exception, $query);
}
if (
str_contains($exception->getMessage(), 'may not be NULL') ||
str_contains($exception->getMessage(), 'NOT NULL constraint failed')
) {
return new NotNullConstraintViolationException($exception, $query);
}
if (str_contains($exception->getMessage(), 'no such table:')) {
return new TableNotFoundException($exception, $query);
}
if (str_contains($exception->getMessage(), 'already exists')) {
return new TableExistsException($exception, $query);
}
if (str_contains($exception->getMessage(), 'has no column named')) {
return new InvalidFieldNameException($exception, $query);
}
if (str_contains($exception->getMessage(), 'ambiguous column name')) {
return new NonUniqueFieldNameException($exception, $query);
}
if (str_contains($exception->getMessage(), 'syntax error')) {
return new SyntaxErrorException($exception, $query);
}
if (str_contains($exception->getMessage(), 'attempt to write a readonly database')) {
return new ReadOnlyException($exception, $query);
}
if (str_contains($exception->getMessage(), 'unable to open database file')) {
return new ConnectionException($exception, $query);
}
if (str_contains($exception->getMessage(), 'FOREIGN KEY constraint failed')) {
return new ForeignKeyConstraintViolationException($exception, $query);
}
return new DriverException($exception, $query);
}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver;
use Doctrine\DBAL\Driver;
use Doctrine\DBAL\Driver\API\ExceptionConverter as ExceptionConverterInterface;
use Doctrine\DBAL\Driver\API\IBMDB2\ExceptionConverter;
use Doctrine\DBAL\Platforms\DB2Platform;
use Doctrine\DBAL\ServerVersionProvider;
/**
* Abstract base implementation of the {@see Driver} interface for IBM DB2 based drivers.
*/
abstract class AbstractDB2Driver implements Driver
{
public function getDatabasePlatform(ServerVersionProvider $versionProvider): DB2Platform
{
return new DB2Platform();
}
public function getExceptionConverter(): ExceptionConverterInterface
{
return new ExceptionConverter();
}
}

View File

@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver;
use Exception as BaseException;
use Throwable;
/**
* Abstract base implementation of the {@see DriverException} interface.
*/
abstract class AbstractException extends BaseException implements Exception
{
/**
* @param string $message The driver error message.
* @param string|null $sqlState The SQLSTATE the driver is in at the time the error occurred, if any.
* @param int $code The driver specific error code if any.
* @param Throwable|null $previous The previous throwable used for the exception chaining.
*/
public function __construct(
string $message,
private readonly ?string $sqlState = null,
int $code = 0,
?Throwable $previous = null,
) {
parent::__construct($message, $code, $previous);
}
public function getSQLState(): ?string
{
return $this->sqlState;
}
}

View File

@@ -0,0 +1,109 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver;
use Doctrine\DBAL\Driver;
use Doctrine\DBAL\Driver\API\ExceptionConverter as ExceptionConverterInterface;
use Doctrine\DBAL\Driver\API\MySQL\ExceptionConverter;
use Doctrine\DBAL\Platforms\AbstractMySQLPlatform;
use Doctrine\DBAL\Platforms\Exception\InvalidPlatformVersion;
use Doctrine\DBAL\Platforms\MariaDB1010Platform;
use Doctrine\DBAL\Platforms\MariaDB1052Platform;
use Doctrine\DBAL\Platforms\MariaDB1060Platform;
use Doctrine\DBAL\Platforms\MariaDBPlatform;
use Doctrine\DBAL\Platforms\MySQL80Platform;
use Doctrine\DBAL\Platforms\MySQL84Platform;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\ServerVersionProvider;
use Doctrine\Deprecations\Deprecation;
use function preg_match;
use function stripos;
use function version_compare;
/**
* Abstract base implementation of the {@see Driver} interface for MySQL based drivers.
*/
abstract class AbstractMySQLDriver implements Driver
{
/**
* {@inheritDoc}
*
* @throws InvalidPlatformVersion
*/
public function getDatabasePlatform(ServerVersionProvider $versionProvider): AbstractMySQLPlatform
{
$version = $versionProvider->getServerVersion();
if (stripos($version, 'mariadb') !== false) {
$mariaDbVersion = $this->getMariaDbMysqlVersionNumber($version);
if (version_compare($mariaDbVersion, '10.10.0', '>=')) {
return new MariaDB1010Platform();
}
if (version_compare($mariaDbVersion, '10.6.0', '>=')) {
return new MariaDB1060Platform();
}
if (version_compare($mariaDbVersion, '10.5.2', '>=')) {
return new MariaDB1052Platform();
}
Deprecation::trigger(
'doctrine/dbal',
'https://github.com/doctrine/dbal/pull/6343',
'Support for MariaDB < 10.5.2 is deprecated and will be removed in DBAL 5',
);
return new MariaDBPlatform();
}
if (version_compare($version, '8.4.0', '>=')) {
return new MySQL84Platform();
}
if (version_compare($version, '8.0.0', '>=')) {
return new MySQL80Platform();
}
Deprecation::trigger(
'doctrine/dbal',
'https://github.com/doctrine/dbal/pull/6343',
'Support for MySQL < 8 is deprecated and will be removed in DBAL 5',
);
return new MySQLPlatform();
}
public function getExceptionConverter(): ExceptionConverterInterface
{
return new ExceptionConverter();
}
/**
* Detect MariaDB server version, including hack for some mariadb distributions
* that starts with the prefix '5.5.5-'
*
* @param string $versionString Version string as returned by mariadb server, i.e. '5.5.5-Mariadb-10.0.8-xenial'
*
* @throws InvalidPlatformVersion
*/
private function getMariaDbMysqlVersionNumber(string $versionString): string
{
if (
preg_match(
'/^(?:5\.5\.5-)?(mariadb-)?(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)/i',
$versionString,
$versionParts,
) !== 1
) {
throw InvalidPlatformVersion::new(
$versionString,
'^(?:5\.5\.5-)?(mariadb-)?<major_version>.<minor_version>.<patch_version>',
);
}
return $versionParts['major'] . '.' . $versionParts['minor'] . '.' . $versionParts['patch'];
}
}

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver;
use Doctrine\DBAL\Driver;
use Doctrine\DBAL\Driver\AbstractOracleDriver\EasyConnectString;
use Doctrine\DBAL\Driver\API\ExceptionConverter as ExceptionConverterInterface;
use Doctrine\DBAL\Driver\API\OCI\ExceptionConverter;
use Doctrine\DBAL\Platforms\OraclePlatform;
use Doctrine\DBAL\ServerVersionProvider;
/**
* Abstract base implementation of the {@see Driver} interface for Oracle based drivers.
*/
abstract class AbstractOracleDriver implements Driver
{
public function getDatabasePlatform(ServerVersionProvider $versionProvider): OraclePlatform
{
return new OraclePlatform();
}
public function getExceptionConverter(): ExceptionConverterInterface
{
return new ExceptionConverter();
}
/**
* Returns an appropriate Easy Connect String for the given parameters.
*
* @param array<string, mixed> $params The connection parameters to return the Easy Connect String for.
*/
protected function getEasyConnectString(array $params): string
{
return (string) EasyConnectString::fromConnectionParameters($params);
}
}

View File

@@ -0,0 +1,112 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\AbstractOracleDriver;
use function implode;
use function is_array;
use function sprintf;
/**
* Represents an Oracle Easy Connect string
*
* @link https://docs.oracle.com/database/121/NETAG/naming.htm
*/
final class EasyConnectString
{
private function __construct(private readonly string $string)
{
}
public function __toString(): string
{
return $this->string;
}
/**
* Creates the object from an array representation
*
* @param mixed[] $params
*/
public static function fromArray(array $params): self
{
return new self(self::renderParams($params));
}
/**
* Creates the object from the given DBAL connection parameters.
*
* @param mixed[] $params
*/
public static function fromConnectionParameters(array $params): self
{
if (isset($params['connectstring'])) {
return new self($params['connectstring']);
}
if (! isset($params['host'])) {
return new self($params['dbname'] ?? '');
}
$connectData = [];
if (isset($params['servicename']) || isset($params['dbname'])) {
$serviceKey = 'SID';
if (isset($params['service'])) {
$serviceKey = 'SERVICE_NAME';
}
$serviceName = $params['servicename'] ?? $params['dbname'];
$connectData[$serviceKey] = $serviceName;
}
if (isset($params['instancename'])) {
$connectData['INSTANCE_NAME'] = $params['instancename'];
}
if (! empty($params['pooled'])) {
$connectData['SERVER'] = 'POOLED';
}
return self::fromArray([
'DESCRIPTION' => [
'ADDRESS' => [
'PROTOCOL' => $params['driverOptions']['protocol'] ?? 'TCP',
'HOST' => $params['host'],
'PORT' => $params['port'] ?? 1521,
],
'CONNECT_DATA' => $connectData,
],
]);
}
/** @param mixed[] $params */
private static function renderParams(array $params): string
{
$chunks = [];
foreach ($params as $key => $value) {
$string = self::renderValue($value);
if ($string === '') {
continue;
}
$chunks[] = sprintf('(%s=%s)', $key, $string);
}
return implode('', $chunks);
}
private static function renderValue(mixed $value): string
{
if (is_array($value)) {
return self::renderParams($value);
}
return (string) $value;
}
}

View File

@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver;
use Doctrine\DBAL\Driver;
use Doctrine\DBAL\Driver\API\ExceptionConverter as ExceptionConverterInterface;
use Doctrine\DBAL\Driver\API\PostgreSQL\ExceptionConverter;
use Doctrine\DBAL\Platforms\Exception\InvalidPlatformVersion;
use Doctrine\DBAL\Platforms\PostgreSQL120Platform;
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
use Doctrine\DBAL\ServerVersionProvider;
use Doctrine\Deprecations\Deprecation;
use function preg_match;
use function version_compare;
/**
* Abstract base implementation of the {@see Driver} interface for PostgreSQL based drivers.
*/
abstract class AbstractPostgreSQLDriver implements Driver
{
public function getDatabasePlatform(ServerVersionProvider $versionProvider): PostgreSQLPlatform
{
$version = $versionProvider->getServerVersion();
if (preg_match('/^(?P<major>\d+)(?:\.(?P<minor>\d+)(?:\.(?P<patch>\d+))?)?/', $version, $versionParts) !== 1) {
throw InvalidPlatformVersion::new(
$version,
'<major_version>.<minor_version>.<patch_version>',
);
}
$majorVersion = $versionParts['major'];
$minorVersion = $versionParts['minor'] ?? 0;
$patchVersion = $versionParts['patch'] ?? 0;
$version = $majorVersion . '.' . $minorVersion . '.' . $patchVersion;
if (version_compare($version, '12.0', '>=')) {
return new PostgreSQL120Platform();
}
Deprecation::trigger(
'doctrine/dbal',
'https://github.com/doctrine/dbal/pull/6495',
'Support for Postgres < 12 is deprecated and will be removed in DBAL 5',
);
return new PostgreSQLPlatform();
}
public function getExceptionConverter(): ExceptionConverterInterface
{
return new ExceptionConverter();
}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver;
use Doctrine\DBAL\Driver;
use Doctrine\DBAL\Driver\API\ExceptionConverter as ExceptionConverterInterface;
use Doctrine\DBAL\Driver\API\SQLSrv\ExceptionConverter;
use Doctrine\DBAL\Platforms\SQLServerPlatform;
use Doctrine\DBAL\ServerVersionProvider;
/**
* Abstract base implementation of the {@see Driver} interface for Microsoft SQL Server based drivers.
*/
abstract class AbstractSQLServerDriver implements Driver
{
public function getDatabasePlatform(ServerVersionProvider $versionProvider): SQLServerPlatform
{
return new SQLServerPlatform();
}
public function getExceptionConverter(): ExceptionConverterInterface
{
return new ExceptionConverter();
}
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\AbstractSQLServerDriver\Exception;
use Doctrine\DBAL\Driver\AbstractException;
/** @internal */
final class PortWithoutHost extends AbstractException
{
public static function new(): self
{
return new self('Connection port specified without the host');
}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver;
use Doctrine\DBAL\Driver;
use Doctrine\DBAL\Driver\API\ExceptionConverter as ExceptionConverterInterface;
use Doctrine\DBAL\Driver\API\SQLite\ExceptionConverter;
use Doctrine\DBAL\Platforms\SQLitePlatform;
use Doctrine\DBAL\ServerVersionProvider;
/**
* Abstract base implementation of the {@see Driver} interface for SQLite based drivers.
*/
abstract class AbstractSQLiteDriver implements Driver
{
public function getDatabasePlatform(ServerVersionProvider $versionProvider): SQLitePlatform
{
return new SQLitePlatform();
}
public function getExceptionConverter(): ExceptionConverterInterface
{
return new ExceptionConverter();
}
}

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\AbstractSQLiteDriver\Middleware;
use Doctrine\DBAL\Driver;
use Doctrine\DBAL\Driver\Connection;
use Doctrine\DBAL\Driver\Middleware;
use Doctrine\DBAL\Driver\Middleware\AbstractDriverMiddleware;
use SensitiveParameter;
final class EnableForeignKeys implements Middleware
{
public function wrap(Driver $driver): Driver
{
return new class ($driver) extends AbstractDriverMiddleware {
/**
* {@inheritDoc}
*/
public function connect(
#[SensitiveParameter]
array $params,
): Connection {
$connection = parent::connect($params);
$connection->exec('PRAGMA foreign_keys=ON');
return $connection;
}
};
}
}

View File

@@ -0,0 +1,93 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver;
use Doctrine\DBAL\ServerVersionProvider;
/**
* Connection interface.
* Driver connections must implement this interface.
*/
interface Connection extends ServerVersionProvider
{
/**
* Prepares a statement for execution and returns a Statement object.
*
* @throws Exception
*/
public function prepare(string $sql): Statement;
/**
* Executes an SQL statement, returning a result set as a Statement object.
*
* @throws Exception
*/
public function query(string $sql): Result;
/**
* Quotes a string for use in a query.
*
* The usage of this method is discouraged. Use prepared statements
* or {@see AbstractPlatform::quoteStringLiteral()} instead.
*/
public function quote(string $value): string;
/**
* Executes an SQL statement and return the number of affected rows.
* If the number of affected rows is greater than the maximum int value (PHP_INT_MAX),
* the number of affected rows may be returned as a string.
*
* @return int|numeric-string
*
* @throws Exception
*/
public function exec(string $sql): int|string;
/**
* Returns the ID of the last inserted row.
*
* This method returns an integer or a string representing the value of the auto-increment column
* from the last row inserted into the database, if any, or throws an exception if a value cannot be returned,
* in particular when:
*
* - the driver does not support identity columns;
* - the last statement dit not return an identity (caution: see note below).
*
* Note: if the last statement was not an INSERT to an autoincrement column, this method MAY return an ID from a
* previous statement. DO NOT RELY ON THIS BEHAVIOR which is driver-dependent: always call this method right after
* executing an INSERT statement.
*
* @throws Exception
*/
public function lastInsertId(): int|string;
/**
* Initiates a transaction.
*
* @throws Exception
*/
public function beginTransaction(): void;
/**
* Commits a transaction.
*
* @throws Exception
*/
public function commit(): void;
/**
* Rolls back the current transaction, as initiated by beginTransaction().
*
* @throws Exception
*/
public function rollBack(): void;
/**
* Provides access to the native database connection.
*
* @return resource|object
*/
public function getNativeConnection();
}

View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver;
use Throwable;
/**
* Contract for a driver exception.
*
* Driver exceptions provide the SQLSTATE of the driver
* and the driver specific error code at the time the error occurred.
*/
interface Exception extends Throwable
{
/**
* Returns the SQLSTATE the driver was in at the time the error occurred.
*
* Returns null if the driver does not provide a SQLSTATE for the error occurred.
*/
public function getSQLState(): ?string;
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\Exception;
use Doctrine\DBAL\Driver\AbstractException;
use Throwable;
/** @internal */
final class IdentityColumnsNotSupported extends AbstractException
{
public static function new(?Throwable $previous = null): self
{
return new self('The driver does not support identity columns.', null, 0, $previous);
}
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\Exception;
use Doctrine\DBAL\Driver\AbstractException;
use Throwable;
/** @internal */
final class NoIdentityValue extends AbstractException
{
public static function new(?Throwable $previous = null): self
{
return new self('No identity value was generated by the last statement.', null, 0, $previous);
}
}

View File

@@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver;
/** @internal */
final class FetchUtils
{
/** @throws Exception */
public static function fetchOne(Result $result): mixed
{
$row = $result->fetchNumeric();
if ($row === false) {
return false;
}
return $row[0];
}
/**
* @return list<list<mixed>>
*
* @throws Exception
*/
public static function fetchAllNumeric(Result $result): array
{
$rows = [];
while (($row = $result->fetchNumeric()) !== false) {
$rows[] = $row;
}
return $rows;
}
/**
* @return list<array<string,mixed>>
*
* @throws Exception
*/
public static function fetchAllAssociative(Result $result): array
{
$rows = [];
while (($row = $result->fetchAssociative()) !== false) {
$rows[] = $row;
}
return $rows;
}
/**
* @return list<mixed>
*
* @throws Exception
*/
public static function fetchFirstColumn(Result $result): array
{
$rows = [];
while (($row = $result->fetchOne()) !== false) {
$rows[] = $row;
}
return $rows;
}
}

View File

@@ -0,0 +1,131 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\IBMDB2;
use Doctrine\DBAL\Driver\Connection as ConnectionInterface;
use Doctrine\DBAL\Driver\Exception\NoIdentityValue;
use Doctrine\DBAL\Driver\IBMDB2\Exception\ConnectionError;
use Doctrine\DBAL\Driver\IBMDB2\Exception\PrepareFailed;
use Doctrine\DBAL\Driver\IBMDB2\Exception\StatementError;
use stdClass;
use function assert;
use function db2_autocommit;
use function db2_commit;
use function db2_escape_string;
use function db2_exec;
use function db2_last_insert_id;
use function db2_num_rows;
use function db2_prepare;
use function db2_rollback;
use function db2_server_info;
use function error_get_last;
use const DB2_AUTOCOMMIT_OFF;
use const DB2_AUTOCOMMIT_ON;
final class Connection implements ConnectionInterface
{
/**
* @internal The connection can be only instantiated by its driver.
*
* @param resource $connection
*/
public function __construct(private readonly mixed $connection)
{
}
public function getServerVersion(): string
{
$serverInfo = db2_server_info($this->connection);
assert($serverInfo instanceof stdClass);
return $serverInfo->DBMS_VER;
}
public function prepare(string $sql): Statement
{
$stmt = @db2_prepare($this->connection, $sql);
if ($stmt === false) {
throw PrepareFailed::new(error_get_last());
}
return new Statement($stmt);
}
public function query(string $sql): Result
{
return $this->prepare($sql)->execute();
}
public function quote(string $value): string
{
return "'" . db2_escape_string($value) . "'";
}
public function exec(string $sql): int|string
{
$stmt = @db2_exec($this->connection, $sql);
if ($stmt === false) {
throw StatementError::new();
}
$numRows = db2_num_rows($stmt);
if ($numRows === false) {
throw StatementError::new();
}
return $numRows;
}
public function lastInsertId(): string
{
$lastInsertId = db2_last_insert_id($this->connection);
if ($lastInsertId === null) {
throw NoIdentityValue::new();
}
return $lastInsertId;
}
public function beginTransaction(): void
{
if (db2_autocommit($this->connection, DB2_AUTOCOMMIT_OFF) !== true) {
throw ConnectionError::new($this->connection);
}
}
public function commit(): void
{
if (! db2_commit($this->connection)) {
throw ConnectionError::new($this->connection);
}
if (db2_autocommit($this->connection, DB2_AUTOCOMMIT_ON) !== true) {
throw ConnectionError::new($this->connection);
}
}
public function rollBack(): void
{
if (! db2_rollback($this->connection)) {
throw ConnectionError::new($this->connection);
}
if (db2_autocommit($this->connection, DB2_AUTOCOMMIT_ON) !== true) {
throw ConnectionError::new($this->connection);
}
}
/** @return resource */
public function getNativeConnection()
{
return $this->connection;
}
}

View File

@@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\IBMDB2;
use SensitiveParameter;
use function implode;
use function sprintf;
use function str_contains;
/**
* IBM DB2 DSN
*/
final class DataSourceName
{
private function __construct(
#[SensitiveParameter]
private readonly string $string,
) {
}
public function toString(): string
{
return $this->string;
}
/**
* Creates the object from an array representation
*
* @param array<string,mixed> $params
*/
public static function fromArray(
#[SensitiveParameter]
array $params,
): self {
$chunks = [];
foreach ($params as $key => $value) {
$chunks[] = sprintf('%s=%s', $key, $value);
}
return new self(implode(';', $chunks));
}
/**
* Creates the object from the given DBAL connection parameters.
*
* @param array<string,mixed> $params
*/
public static function fromConnectionParameters(#[SensitiveParameter]
array $params,): self
{
if (isset($params['dbname']) && str_contains($params['dbname'], '=')) {
return new self($params['dbname']);
}
$dsnParams = [];
foreach (
[
'host' => 'HOSTNAME',
'port' => 'PORT',
'protocol' => 'PROTOCOL',
'dbname' => 'DATABASE',
'user' => 'UID',
'password' => 'PWD',
] as $dbalParam => $dsnParam
) {
if (! isset($params[$dbalParam])) {
continue;
}
$dsnParams[$dsnParam] = $params[$dbalParam];
}
return self::fromArray($dsnParams);
}
}

View File

@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\IBMDB2;
use Doctrine\DBAL\Driver\AbstractDB2Driver;
use Doctrine\DBAL\Driver\IBMDB2\Exception\ConnectionFailed;
use SensitiveParameter;
use function db2_connect;
use function db2_pconnect;
final class Driver extends AbstractDB2Driver
{
/**
* {@inheritDoc}
*/
public function connect(
#[SensitiveParameter]
array $params,
): Connection {
$dataSourceName = DataSourceName::fromConnectionParameters($params)->toString();
$username = $params['user'] ?? '';
$password = $params['password'] ?? '';
$driverOptions = $params['driverOptions'] ?? [];
if (! empty($params['persistent'])) {
$connection = db2_pconnect($dataSourceName, $username, $password, $driverOptions);
} else {
$connection = db2_connect($dataSourceName, $username, $password, $driverOptions);
}
if ($connection === false) {
throw ConnectionFailed::new();
}
return new Connection($connection);
}
}

View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\IBMDB2\Exception;
use Doctrine\DBAL\Driver\AbstractException;
/** @internal */
final class CannotCopyStreamToStream extends AbstractException
{
/** @phpstan-param array{message: string, ...}|null $error */
public static function new(?array $error): self
{
$message = 'Could not copy source stream to temporary file';
if ($error !== null) {
$message .= ': ' . $error['message'];
}
return new self($message);
}
}

View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\IBMDB2\Exception;
use Doctrine\DBAL\Driver\AbstractException;
/** @internal */
final class CannotCreateTemporaryFile extends AbstractException
{
/** @phpstan-param array{message: string, ...}|null $error */
public static function new(?array $error): self
{
$message = 'Could not create temporary file';
if ($error !== null) {
$message .= ': ' . $error['message'];
}
return new self($message);
}
}

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\IBMDB2\Exception;
use Doctrine\DBAL\Driver\AbstractException;
use function db2_conn_error;
use function db2_conn_errormsg;
/** @internal */
final class ConnectionError extends AbstractException
{
/** @param resource $connection */
public static function new($connection): self
{
$message = db2_conn_errormsg($connection);
$sqlState = db2_conn_error($connection);
return Factory::create($message, static function (int $code) use ($message, $sqlState): self {
return new self($message, $sqlState, $code);
});
}
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\IBMDB2\Exception;
use Doctrine\DBAL\Driver\AbstractException;
use function db2_conn_error;
use function db2_conn_errormsg;
/** @internal */
final class ConnectionFailed extends AbstractException
{
public static function new(): self
{
$message = db2_conn_errormsg();
$sqlState = db2_conn_error();
return Factory::create($message, static function (int $code) use ($message, $sqlState): self {
return new self($message, $sqlState, $code);
});
}
}

View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\IBMDB2\Exception;
use Doctrine\DBAL\Driver\AbstractException;
use function preg_match;
/** @internal */
final class Factory
{
/**
* @param callable(int): T $constructor
*
* @return T
*
* @template T of AbstractException
*/
public static function create(string $message, callable $constructor): AbstractException
{
$code = 0;
if (preg_match('/ SQL(\d+)N /', $message, $matches) === 1) {
$code = -(int) $matches[1];
}
return $constructor($code);
}
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\IBMDB2\Exception;
use Doctrine\DBAL\Driver\AbstractException;
/** @internal */
final class PrepareFailed extends AbstractException
{
/** @phpstan-param array{message: string, ...}|null $error */
public static function new(?array $error): self
{
if ($error === null) {
return new self('Unknown error');
}
return new self($error['message']);
}
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\IBMDB2\Exception;
use Doctrine\DBAL\Driver\AbstractException;
use function db2_stmt_error;
use function db2_stmt_errormsg;
/** @internal */
final class StatementError extends AbstractException
{
/** @param resource|null $statement */
public static function new($statement = null): self
{
if ($statement !== null) {
$message = db2_stmt_errormsg($statement);
$sqlState = db2_stmt_error($statement);
} else {
$message = db2_stmt_errormsg();
$sqlState = db2_stmt_error();
}
return Factory::create($message, static function (int $code) use ($message, $sqlState): self {
return new self($message, $sqlState, $code);
});
}
}

View File

@@ -0,0 +1,118 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\IBMDB2;
use Doctrine\DBAL\Driver\FetchUtils;
use Doctrine\DBAL\Driver\IBMDB2\Exception\StatementError;
use Doctrine\DBAL\Driver\Result as ResultInterface;
use Doctrine\DBAL\Exception\InvalidColumnIndex;
use function db2_fetch_array;
use function db2_fetch_assoc;
use function db2_free_result;
use function db2_num_fields;
use function db2_num_rows;
use function db2_stmt_error;
final class Result implements ResultInterface
{
/**
* @internal The result can be only instantiated by its driver connection or statement.
*
* @param resource $statement
*/
public function __construct(private readonly mixed $statement)
{
}
public function fetchNumeric(): array|false
{
$row = @db2_fetch_array($this->statement);
if ($row === false && db2_stmt_error($this->statement) !== '02000') {
throw StatementError::new($this->statement);
}
return $row;
}
public function fetchAssociative(): array|false
{
$row = @db2_fetch_assoc($this->statement);
if ($row === false && db2_stmt_error($this->statement) !== '02000') {
throw StatementError::new($this->statement);
}
return $row;
}
public function fetchOne(): mixed
{
return FetchUtils::fetchOne($this);
}
/**
* {@inheritDoc}
*/
public function fetchAllNumeric(): array
{
return FetchUtils::fetchAllNumeric($this);
}
/**
* {@inheritDoc}
*/
public function fetchAllAssociative(): array
{
return FetchUtils::fetchAllAssociative($this);
}
/**
* {@inheritDoc}
*/
public function fetchFirstColumn(): array
{
return FetchUtils::fetchFirstColumn($this);
}
public function rowCount(): int
{
$numRows = @db2_num_rows($this->statement);
if ($numRows === false) {
throw StatementError::new($this->statement);
}
return $numRows;
}
public function columnCount(): int
{
$count = db2_num_fields($this->statement);
if ($count !== false) {
return $count;
}
return 0;
}
public function getColumnName(int $index): string
{
$name = db2_field_name($this->statement, $index);
if ($name === false) {
throw InvalidColumnIndex::new($index);
}
return $name;
}
public function free(): void
{
db2_free_result($this->statement);
}
}

View File

@@ -0,0 +1,156 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\IBMDB2;
use Doctrine\DBAL\Driver\Exception;
use Doctrine\DBAL\Driver\IBMDB2\Exception\CannotCopyStreamToStream;
use Doctrine\DBAL\Driver\IBMDB2\Exception\CannotCreateTemporaryFile;
use Doctrine\DBAL\Driver\IBMDB2\Exception\StatementError;
use Doctrine\DBAL\Driver\Statement as StatementInterface;
use Doctrine\DBAL\ParameterType;
use function assert;
use function db2_bind_param;
use function db2_execute;
use function error_get_last;
use function fclose;
use function is_int;
use function is_resource;
use function stream_copy_to_stream;
use function stream_get_meta_data;
use function tmpfile;
use const DB2_BINARY;
use const DB2_CHAR;
use const DB2_LONG;
use const DB2_PARAM_FILE;
use const DB2_PARAM_IN;
final class Statement implements StatementInterface
{
/** @var mixed[] */
private array $parameters = [];
/**
* Map of LOB parameter positions to the tuples containing reference to the variable bound to the driver statement
* and the temporary file handle bound to the underlying statement
*
* @var array<int,string|resource|null>
*/
private array $lobs = [];
/**
* @internal The statement can be only instantiated by its driver connection.
*
* @param resource $stmt
*/
public function __construct(private readonly mixed $stmt)
{
}
public function bindValue(int|string $param, mixed $value, ParameterType $type): void
{
assert(is_int($param));
switch ($type) {
case ParameterType::INTEGER:
$this->bind($param, $value, DB2_PARAM_IN, DB2_LONG);
break;
case ParameterType::LARGE_OBJECT:
$this->lobs[$param] = &$value;
break;
default:
$this->bind($param, $value, DB2_PARAM_IN, DB2_CHAR);
break;
}
}
/** @throws Exception */
private function bind(int $position, mixed &$variable, int $parameterType, int $dataType): void
{
$this->parameters[$position] =& $variable;
if (! db2_bind_param($this->stmt, $position, '', $parameterType, $dataType)) {
throw StatementError::new($this->stmt);
}
}
public function execute(): Result
{
$handles = $this->bindLobs();
$result = @db2_execute($this->stmt, $this->parameters);
foreach ($handles as $handle) {
fclose($handle);
}
$this->lobs = [];
if ($result === false) {
throw StatementError::new($this->stmt);
}
return new Result($this->stmt);
}
/**
* @return list<resource>
*
* @throws Exception
*/
private function bindLobs(): array
{
$handles = [];
foreach ($this->lobs as $param => $value) {
if (is_resource($value)) {
$handle = $handles[] = $this->createTemporaryFile();
$path = stream_get_meta_data($handle)['uri'];
$this->copyStreamToStream($value, $handle);
$this->bind($param, $path, DB2_PARAM_FILE, DB2_BINARY);
} else {
$this->bind($param, $value, DB2_PARAM_IN, DB2_CHAR);
}
unset($value);
}
return $handles;
}
/**
* @return resource
*
* @throws Exception
*/
private function createTemporaryFile()
{
$handle = @tmpfile();
if ($handle === false) {
throw CannotCreateTemporaryFile::new(error_get_last());
}
return $handle;
}
/**
* @param resource $source
* @param resource $target
*
* @throws Exception
*/
private function copyStreamToStream($source, $target): void
{
if (@stream_copy_to_stream($source, $target) === false) {
throw CannotCopyStreamToStream::new(error_get_last());
}
}
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver;
use Doctrine\DBAL\Driver;
interface Middleware
{
public function wrap(Driver $driver): Driver;
}

View File

@@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\Middleware;
use Doctrine\DBAL\Driver\Connection;
use Doctrine\DBAL\Driver\Result;
use Doctrine\DBAL\Driver\Statement;
abstract class AbstractConnectionMiddleware implements Connection
{
public function __construct(private readonly Connection $wrappedConnection)
{
}
public function prepare(string $sql): Statement
{
return $this->wrappedConnection->prepare($sql);
}
public function query(string $sql): Result
{
return $this->wrappedConnection->query($sql);
}
public function quote(string $value): string
{
return $this->wrappedConnection->quote($value);
}
public function exec(string $sql): int|string
{
return $this->wrappedConnection->exec($sql);
}
public function lastInsertId(): int|string
{
return $this->wrappedConnection->lastInsertId();
}
public function beginTransaction(): void
{
$this->wrappedConnection->beginTransaction();
}
public function commit(): void
{
$this->wrappedConnection->commit();
}
public function rollBack(): void
{
$this->wrappedConnection->rollBack();
}
public function getServerVersion(): string
{
return $this->wrappedConnection->getServerVersion();
}
/**
* {@inheritDoc}
*/
public function getNativeConnection()
{
return $this->wrappedConnection->getNativeConnection();
}
}

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\Middleware;
use Doctrine\DBAL\Driver;
use Doctrine\DBAL\Driver\API\ExceptionConverter;
use Doctrine\DBAL\Driver\Connection as DriverConnection;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\ServerVersionProvider;
use SensitiveParameter;
abstract class AbstractDriverMiddleware implements Driver
{
public function __construct(private readonly Driver $wrappedDriver)
{
}
/**
* {@inheritDoc}
*/
public function connect(
#[SensitiveParameter]
array $params,
): DriverConnection {
return $this->wrappedDriver->connect($params);
}
public function getDatabasePlatform(ServerVersionProvider $versionProvider): AbstractPlatform
{
return $this->wrappedDriver->getDatabasePlatform($versionProvider);
}
public function getExceptionConverter(): ExceptionConverter
{
return $this->wrappedDriver->getExceptionConverter();
}
}

View File

@@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\Middleware;
use Doctrine\DBAL\Driver\Result;
use LogicException;
use function get_debug_type;
use function method_exists;
use function sprintf;
abstract class AbstractResultMiddleware implements Result
{
public function __construct(private readonly Result $wrappedResult)
{
}
public function fetchNumeric(): array|false
{
return $this->wrappedResult->fetchNumeric();
}
public function fetchAssociative(): array|false
{
return $this->wrappedResult->fetchAssociative();
}
public function fetchOne(): mixed
{
return $this->wrappedResult->fetchOne();
}
/**
* {@inheritDoc}
*/
public function fetchAllNumeric(): array
{
return $this->wrappedResult->fetchAllNumeric();
}
/**
* {@inheritDoc}
*/
public function fetchAllAssociative(): array
{
return $this->wrappedResult->fetchAllAssociative();
}
/**
* {@inheritDoc}
*/
public function fetchFirstColumn(): array
{
return $this->wrappedResult->fetchFirstColumn();
}
public function rowCount(): int|string
{
return $this->wrappedResult->rowCount();
}
public function columnCount(): int
{
return $this->wrappedResult->columnCount();
}
public function getColumnName(int $index): string
{
if (! method_exists($this->wrappedResult, 'getColumnName')) {
throw new LogicException(sprintf(
'The driver result %s does not support accessing the column name.',
get_debug_type($this->wrappedResult),
));
}
return $this->wrappedResult->getColumnName($index);
}
public function free(): void
{
$this->wrappedResult->free();
}
}

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\Middleware;
use Doctrine\DBAL\Driver\Result;
use Doctrine\DBAL\Driver\Statement;
use Doctrine\DBAL\ParameterType;
abstract class AbstractStatementMiddleware implements Statement
{
public function __construct(private readonly Statement $wrappedStatement)
{
}
public function bindValue(int|string $param, mixed $value, ParameterType $type): void
{
$this->wrappedStatement->bindValue($param, $value, $type);
}
public function execute(): Result
{
return $this->wrappedStatement->execute();
}
}

View File

@@ -0,0 +1,114 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\Mysqli;
use Doctrine\DBAL\Driver\Connection as ConnectionInterface;
use Doctrine\DBAL\Driver\Exception;
use Doctrine\DBAL\Driver\Mysqli\Exception\ConnectionError;
use mysqli;
use mysqli_sql_exception;
final class Connection implements ConnectionInterface
{
/**
* Name of the option to set connection flags
*/
public const OPTION_FLAGS = 'flags';
/** @internal The connection can be only instantiated by its driver. */
public function __construct(private readonly mysqli $connection)
{
}
public function getServerVersion(): string
{
return $this->connection->get_server_info();
}
public function prepare(string $sql): Statement
{
try {
$stmt = $this->connection->prepare($sql);
} catch (mysqli_sql_exception $e) {
throw ConnectionError::upcast($e);
}
if ($stmt === false) {
throw ConnectionError::new($this->connection);
}
return new Statement($stmt);
}
public function query(string $sql): Result
{
return $this->prepare($sql)->execute();
}
public function quote(string $value): string
{
return "'" . $this->connection->escape_string($value) . "'";
}
public function exec(string $sql): int|string
{
try {
$result = $this->connection->query($sql);
} catch (mysqli_sql_exception $e) {
throw ConnectionError::upcast($e);
}
if ($result === false) {
throw ConnectionError::new($this->connection);
}
return $this->connection->affected_rows;
}
public function lastInsertId(): int|string
{
$lastInsertId = $this->connection->insert_id;
if ($lastInsertId === 0) {
throw Exception\NoIdentityValue::new();
}
return $this->connection->insert_id;
}
public function beginTransaction(): void
{
if (! $this->connection->begin_transaction()) {
throw ConnectionError::new($this->connection);
}
}
public function commit(): void
{
try {
if (! $this->connection->commit()) {
throw ConnectionError::new($this->connection);
}
} catch (mysqli_sql_exception $e) {
throw ConnectionError::upcast($e);
}
}
public function rollBack(): void
{
try {
if (! $this->connection->rollback()) {
throw ConnectionError::new($this->connection);
}
} catch (mysqli_sql_exception $e) {
throw ConnectionError::upcast($e);
}
}
public function getNativeConnection(): mysqli
{
return $this->connection;
}
}

View File

@@ -0,0 +1,117 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\Mysqli;
use Doctrine\DBAL\Driver\AbstractMySQLDriver;
use Doctrine\DBAL\Driver\Mysqli\Exception\ConnectionFailed;
use Doctrine\DBAL\Driver\Mysqli\Exception\HostRequired;
use Doctrine\DBAL\Driver\Mysqli\Initializer\Charset;
use Doctrine\DBAL\Driver\Mysqli\Initializer\Options;
use Doctrine\DBAL\Driver\Mysqli\Initializer\Secure;
use Generator;
use mysqli;
use mysqli_sql_exception;
use SensitiveParameter;
final class Driver extends AbstractMySQLDriver
{
/**
* {@inheritDoc}
*/
public function connect(
#[SensitiveParameter]
array $params,
): Connection {
if (! empty($params['persistent'])) {
if (! isset($params['host'])) {
throw HostRequired::forPersistentConnection();
}
$host = 'p:' . $params['host'];
} else {
$host = $params['host'] ?? '';
}
$connection = new mysqli();
foreach ($this->compilePreInitializers($params) as $initializer) {
$initializer->initialize($connection);
}
try {
$success = @$connection->real_connect(
$host,
$params['user'] ?? '',
$params['password'] ?? '',
$params['dbname'] ?? '',
$params['port'] ?? 0,
$params['unix_socket'] ?? '',
$params['driverOptions'][Connection::OPTION_FLAGS] ?? 0,
);
} catch (mysqli_sql_exception $e) {
throw ConnectionFailed::upcast($e);
}
if (! $success) {
throw ConnectionFailed::new($connection);
}
foreach ($this->compilePostInitializers($params) as $initializer) {
$initializer->initialize($connection);
}
return new Connection($connection);
}
/**
* @param array<string, mixed> $params
*
* @return Generator<int, Initializer>
*/
private function compilePreInitializers(
#[SensitiveParameter]
array $params,
): Generator {
unset($params['driverOptions'][Connection::OPTION_FLAGS]);
if (isset($params['driverOptions']) && $params['driverOptions'] !== []) {
yield new Options($params['driverOptions']);
}
if (
! isset($params['ssl_key']) &&
! isset($params['ssl_cert']) &&
! isset($params['ssl_ca']) &&
! isset($params['ssl_capath']) &&
! isset($params['ssl_cipher'])
) {
return;
}
yield new Secure(
$params['ssl_key'] ?? '',
$params['ssl_cert'] ?? '',
$params['ssl_ca'] ?? '',
$params['ssl_capath'] ?? '',
$params['ssl_cipher'] ?? '',
);
}
/**
* @param array<string, mixed> $params
*
* @return Generator<int, Initializer>
*/
private function compilePostInitializers(
#[SensitiveParameter]
array $params,
): Generator {
if (! isset($params['charset'])) {
return;
}
yield new Charset($params['charset']);
}
}

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\Mysqli\Exception;
use Doctrine\DBAL\Driver\AbstractException;
use mysqli;
use mysqli_sql_exception;
use ReflectionProperty;
/** @internal */
final class ConnectionError extends AbstractException
{
public static function new(mysqli $connection): self
{
return new self($connection->error, $connection->sqlstate, $connection->errno);
}
public static function upcast(mysqli_sql_exception $exception): self
{
$p = new ReflectionProperty(mysqli_sql_exception::class, 'sqlstate');
return new self($exception->getMessage(), $p->getValue($exception), $exception->getCode(), $exception);
}
}

View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\Mysqli\Exception;
use Doctrine\DBAL\Driver\AbstractException;
use mysqli;
use mysqli_sql_exception;
use ReflectionProperty;
use function assert;
/** @internal */
final class ConnectionFailed extends AbstractException
{
public static function new(mysqli $connection): self
{
$error = $connection->connect_error;
assert($error !== null);
return new self($error, 'HY000', $connection->connect_errno);
}
public static function upcast(mysqli_sql_exception $exception): self
{
$p = new ReflectionProperty(mysqli_sql_exception::class, 'sqlstate');
return new self($exception->getMessage(), $p->getValue($exception), $exception->getCode(), $exception);
}
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\Mysqli\Exception;
use Doctrine\DBAL\Driver\AbstractException;
use function sprintf;
/** @internal */
final class FailedReadingStreamOffset extends AbstractException
{
public static function new(int $parameter): self
{
return new self(sprintf('Failed reading the stream resource for parameter #%d.', $parameter));
}
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\Mysqli\Exception;
use Doctrine\DBAL\Driver\AbstractException;
/** @internal */
final class HostRequired extends AbstractException
{
public static function forPersistentConnection(): self
{
return new self('The "host" parameter is required for a persistent connection');
}
}

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\Mysqli\Exception;
use Doctrine\DBAL\Driver\AbstractException;
use mysqli;
use mysqli_sql_exception;
use ReflectionProperty;
use function sprintf;
/** @internal */
final class InvalidCharset extends AbstractException
{
public static function fromCharset(mysqli $connection, string $charset): self
{
return new self(
sprintf('Failed to set charset "%s": %s', $charset, $connection->error),
$connection->sqlstate,
$connection->errno,
);
}
public static function upcast(mysqli_sql_exception $exception, string $charset): self
{
$p = new ReflectionProperty(mysqli_sql_exception::class, 'sqlstate');
return new self(
sprintf('Failed to set charset "%s": %s', $charset, $exception->getMessage()),
$p->getValue($exception),
$exception->getCode(),
$exception,
);
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\Mysqli\Exception;
use Doctrine\DBAL\Driver\AbstractException;
use function sprintf;
/** @internal */
final class InvalidOption extends AbstractException
{
public static function fromOption(int $option, mixed $value): self
{
return new self(
sprintf('Failed to set option %d with value "%s"', $option, $value),
);
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\Mysqli\Exception;
use Doctrine\DBAL\Driver\AbstractException;
use function sprintf;
/** @internal */
final class NonStreamResourceUsedAsLargeObject extends AbstractException
{
public static function new(int $parameter): self
{
return new self(
sprintf('The resource passed as a LARGE_OBJECT parameter #%d must be of type "stream"', $parameter),
);
}
}

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\Mysqli\Exception;
use Doctrine\DBAL\Driver\AbstractException;
use mysqli_sql_exception;
use mysqli_stmt;
use ReflectionProperty;
/** @internal */
final class StatementError extends AbstractException
{
public static function new(mysqli_stmt $statement): self
{
return new self($statement->error, $statement->sqlstate, $statement->errno);
}
public static function upcast(mysqli_sql_exception $exception): self
{
$p = new ReflectionProperty(mysqli_sql_exception::class, 'sqlstate');
return new self($exception->getMessage(), $p->getValue($exception), $exception->getCode(), $exception);
}
}

View File

@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\Mysqli;
use Doctrine\DBAL\Driver\Exception;
use mysqli;
interface Initializer
{
/** @throws Exception */
public function initialize(mysqli $connection): void;
}

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\Mysqli\Initializer;
use Doctrine\DBAL\Driver\Mysqli\Exception\InvalidCharset;
use Doctrine\DBAL\Driver\Mysqli\Initializer;
use mysqli;
use mysqli_sql_exception;
final class Charset implements Initializer
{
public function __construct(private readonly string $charset)
{
}
public function initialize(mysqli $connection): void
{
try {
$success = $connection->set_charset($this->charset);
} catch (mysqli_sql_exception $e) {
throw InvalidCharset::upcast($e, $this->charset);
}
if ($success) {
return;
}
throw InvalidCharset::fromCharset($connection, $this->charset);
}
}

View File

@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\Mysqli\Initializer;
use Doctrine\DBAL\Driver\Mysqli\Exception\InvalidOption;
use Doctrine\DBAL\Driver\Mysqli\Initializer;
use mysqli;
use function mysqli_options;
final class Options implements Initializer
{
/** @param array<int,mixed> $options */
public function __construct(private readonly array $options)
{
}
public function initialize(mysqli $connection): void
{
foreach ($this->options as $option => $value) {
if (! mysqli_options($connection, $option, $value)) {
throw InvalidOption::fromOption($option, $value);
}
}
}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\Mysqli\Initializer;
use Doctrine\DBAL\Driver\Mysqli\Initializer;
use mysqli;
use SensitiveParameter;
final class Secure implements Initializer
{
public function __construct(
#[SensitiveParameter]
private readonly string $key,
private readonly string $cert,
private readonly string $ca,
private readonly string $capath,
private readonly string $cipher,
) {
}
public function initialize(mysqli $connection): void
{
$connection->ssl_set($this->key, $this->cert, $this->ca, $this->capath, $this->cipher);
}
}

View File

@@ -0,0 +1,177 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\Mysqli;
use Doctrine\DBAL\Driver\Exception;
use Doctrine\DBAL\Driver\FetchUtils;
use Doctrine\DBAL\Driver\Mysqli\Exception\StatementError;
use Doctrine\DBAL\Driver\Result as ResultInterface;
use Doctrine\DBAL\Exception\InvalidColumnIndex;
use mysqli_sql_exception;
use mysqli_stmt;
use function array_column;
use function array_combine;
use function array_fill;
use function count;
final class Result implements ResultInterface
{
/**
* Whether the statement result has columns. The property should be used only after the result metadata
* has been fetched ({@see $metadataFetched}). Otherwise, the property value is undetermined.
*/
private readonly bool $hasColumns;
/**
* Mapping of statement result column indexes to their names. The property should be used only
* if the statement result has columns ({@see $hasColumns}). Otherwise, the property value is undetermined.
*
* @var array<int,string>
*/
private readonly array $columnNames;
/** @var mixed[] */
private array $boundValues = [];
/**
* @internal The result can be only instantiated by its driver connection or statement.
*
* @param Statement|null $statementReference Maintains a reference to the Statement that generated this result. This
* ensures that the lifetime of the Statement is managed in conjunction
* with its associated results, so they are destroyed together at the
* appropriate time, see {@see Statement::__destruct()}.
*
* @throws Exception
*/
public function __construct(
private readonly mysqli_stmt $statement,
private ?Statement $statementReference = null, // @phpstan-ignore property.onlyWritten
) {
$meta = $statement->result_metadata();
$this->hasColumns = $meta !== false;
$this->columnNames = $meta !== false ? array_column($meta->fetch_fields(), 'name') : [];
if ($meta === false) {
return;
}
$meta->free();
// Store result of every execution which has it. Otherwise it will be impossible
// to execute a new statement in case if the previous one has non-fetched rows
// @link http://dev.mysql.com/doc/refman/5.7/en/commands-out-of-sync.html
$this->statement->store_result();
// Bind row values _after_ storing the result. Otherwise, if mysqli is compiled with libmysql,
// it will have to allocate as much memory as it may be needed for the given column type
// (e.g. for a LONGBLOB column it's 4 gigabytes)
// @link https://bugs.php.net/bug.php?id=51386#1270673122
//
// Make sure that the values are bound after each execution. Otherwise, if free() has been
// previously called on the result, the values are unbound making the statement unusable.
//
// It's also important that row values are bound after _each_ call to store_result(). Otherwise,
// if mysqli is compiled with libmysql, subsequently fetched string values will get truncated
// to the length of the ones fetched during the previous execution.
$this->boundValues = array_fill(0, count($this->columnNames), null);
// The following is necessary as PHP cannot handle references to properties properly
$refs = &$this->boundValues;
if (! $this->statement->bind_result(...$refs)) {
throw StatementError::new($this->statement);
}
}
public function fetchNumeric(): array|false
{
try {
$ret = $this->statement->fetch();
} catch (mysqli_sql_exception $e) {
throw StatementError::upcast($e);
}
if ($ret === false) {
throw StatementError::new($this->statement);
}
if ($ret === null) {
return false;
}
$values = [];
foreach ($this->boundValues as $v) {
$values[] = $v;
}
return $values;
}
public function fetchAssociative(): array|false
{
$values = $this->fetchNumeric();
if ($values === false) {
return false;
}
return array_combine($this->columnNames, $values);
}
public function fetchOne(): mixed
{
return FetchUtils::fetchOne($this);
}
/**
* {@inheritDoc}
*/
public function fetchAllNumeric(): array
{
return FetchUtils::fetchAllNumeric($this);
}
/**
* {@inheritDoc}
*/
public function fetchAllAssociative(): array
{
return FetchUtils::fetchAllAssociative($this);
}
/**
* {@inheritDoc}
*/
public function fetchFirstColumn(): array
{
return FetchUtils::fetchFirstColumn($this);
}
public function rowCount(): int|string
{
if ($this->hasColumns) {
return $this->statement->num_rows;
}
return $this->statement->affected_rows;
}
public function columnCount(): int
{
return $this->statement->field_count;
}
public function getColumnName(int $index): string
{
return $this->columnNames[$index] ?? throw InvalidColumnIndex::new($index);
}
public function free(): void
{
$this->statement->free_result();
}
}

View File

@@ -0,0 +1,159 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\Mysqli;
use Doctrine\DBAL\Driver\Exception;
use Doctrine\DBAL\Driver\Mysqli\Exception\FailedReadingStreamOffset;
use Doctrine\DBAL\Driver\Mysqli\Exception\NonStreamResourceUsedAsLargeObject;
use Doctrine\DBAL\Driver\Mysqli\Exception\StatementError;
use Doctrine\DBAL\Driver\Statement as StatementInterface;
use Doctrine\DBAL\ParameterType;
use mysqli_sql_exception;
use mysqli_stmt;
use function array_fill;
use function assert;
use function count;
use function feof;
use function fread;
use function get_resource_type;
use function is_int;
use function is_resource;
use function str_repeat;
final class Statement implements StatementInterface
{
private const PARAMETER_TYPE_STRING = 's';
private const PARAMETER_TYPE_INTEGER = 'i';
private const PARAMETER_TYPE_BINARY = 'b';
/** @var mixed[] */
private array $boundValues;
private string $types;
/**
* Contains ref values for bindValue().
*
* @var mixed[]
*/
private array $values = [];
/** @internal The statement can be only instantiated by its driver connection. */
public function __construct(private readonly mysqli_stmt $stmt)
{
$paramCount = $this->stmt->param_count;
$this->types = str_repeat(self::PARAMETER_TYPE_STRING, $paramCount);
$this->boundValues = array_fill(1, $paramCount, null);
}
public function __destruct()
{
@$this->stmt->close();
}
public function bindValue(int|string $param, mixed $value, ParameterType $type): void
{
assert(is_int($param));
$this->types[$param - 1] = $this->convertParameterType($type);
$this->values[$param] = $value;
$this->boundValues[$param] =& $this->values[$param];
}
public function execute(): Result
{
if (count($this->boundValues) > 0) {
$this->bindParameters();
}
try {
if (! $this->stmt->execute()) {
throw StatementError::new($this->stmt);
}
} catch (mysqli_sql_exception $e) {
throw StatementError::upcast($e);
}
return new Result($this->stmt, $this);
}
/**
* Binds parameters with known types previously bound to the statement
*
* @throws Exception
*/
private function bindParameters(): void
{
$streams = $values = [];
$types = $this->types;
foreach ($this->boundValues as $parameter => $value) {
assert(is_int($parameter));
if (! isset($types[$parameter - 1])) {
$types[$parameter - 1] = self::PARAMETER_TYPE_STRING;
}
if ($types[$parameter - 1] === self::PARAMETER_TYPE_BINARY) {
if (is_resource($value)) {
if (get_resource_type($value) !== 'stream') {
throw NonStreamResourceUsedAsLargeObject::new($parameter);
}
$streams[$parameter] = $value;
$values[$parameter] = null;
continue;
}
$types[$parameter - 1] = self::PARAMETER_TYPE_STRING;
}
$values[$parameter] = $value;
}
if (! $this->stmt->bind_param($types, ...$values)) {
throw StatementError::new($this->stmt);
}
$this->sendLongData($streams);
}
/**
* Handle $this->_longData after regular query parameters have been bound
*
* @param array<int, resource> $streams
*
* @throws Exception
*/
private function sendLongData(array $streams): void
{
foreach ($streams as $paramNr => $stream) {
while (! feof($stream)) {
$chunk = fread($stream, 8192);
if ($chunk === false) {
throw FailedReadingStreamOffset::new($paramNr);
}
if (! $this->stmt->send_long_data($paramNr - 1, $chunk)) {
throw StatementError::new($this->stmt);
}
}
}
}
private function convertParameterType(ParameterType $type): string
{
return match ($type) {
ParameterType::NULL,
ParameterType::STRING,
ParameterType::ASCII,
ParameterType::BINARY => self::PARAMETER_TYPE_STRING,
ParameterType::INTEGER,
ParameterType::BOOLEAN => self::PARAMETER_TYPE_INTEGER,
ParameterType::LARGE_OBJECT => self::PARAMETER_TYPE_BINARY,
};
}
}

View File

@@ -0,0 +1,125 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\OCI8;
use Doctrine\DBAL\Driver\Connection as ConnectionInterface;
use Doctrine\DBAL\Driver\Exception;
use Doctrine\DBAL\Driver\Exception\IdentityColumnsNotSupported;
use Doctrine\DBAL\Driver\OCI8\Exception\Error;
use Doctrine\DBAL\SQL\Parser;
use function addcslashes;
use function assert;
use function is_resource;
use function oci_commit;
use function oci_parse;
use function oci_rollback;
use function oci_server_version;
use function preg_match;
use function str_replace;
final class Connection implements ConnectionInterface
{
private readonly Parser $parser;
private readonly ExecutionMode $executionMode;
/**
* @internal The connection can be only instantiated by its driver.
*
* @param resource $connection
*/
public function __construct(private readonly mixed $connection)
{
$this->parser = new Parser(false);
$this->executionMode = new ExecutionMode();
}
public function getServerVersion(): string
{
$version = oci_server_version($this->connection);
assert($version !== false);
$result = preg_match('/\s+(\d+\.\d+\.\d+\.\d+\.\d+)\s+/', $version, $matches);
assert($result === 1);
return $matches[1];
}
/**
* @throws Parser\Exception
* @throws Error
*/
public function prepare(string $sql): Statement
{
$visitor = new ConvertPositionalToNamedPlaceholders();
$this->parser->parse($sql, $visitor);
$statement = @oci_parse($this->connection, $visitor->getSQL());
if (! is_resource($statement)) {
throw Error::new($this->connection);
}
return new Statement($this->connection, $statement, $visitor->getParameterMap(), $this->executionMode);
}
/**
* @throws Exception
* @throws Parser\Exception
*/
public function query(string $sql): Result
{
return $this->prepare($sql)->execute();
}
public function quote(string $value): string
{
return "'" . addcslashes(str_replace("'", "''", $value), "\000\n\r\\\032") . "'";
}
/**
* @throws Exception
* @throws Parser\Exception
*/
public function exec(string $sql): int|string
{
return $this->prepare($sql)->execute()->rowCount();
}
public function lastInsertId(): int|string
{
throw IdentityColumnsNotSupported::new();
}
public function beginTransaction(): void
{
$this->executionMode->disableAutoCommit();
}
public function commit(): void
{
if (! @oci_commit($this->connection)) {
throw Error::new($this->connection);
}
$this->executionMode->enableAutoCommit();
}
public function rollBack(): void
{
if (! oci_rollback($this->connection)) {
throw Error::new($this->connection);
}
$this->executionMode->enableAutoCommit();
}
/** @return resource */
public function getNativeConnection()
{
return $this->connection;
}
}

View File

@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\OCI8;
use Doctrine\DBAL\SQL\Parser\Visitor;
use function count;
use function implode;
/**
* Converts positional (?) into named placeholders (:param<num>).
*
* Oracle does not support positional parameters, hence this method converts all
* positional parameters into artificially named parameters.
*
* @internal This class is not covered by the backward compatibility promise
*/
final class ConvertPositionalToNamedPlaceholders implements Visitor
{
/** @var list<string> */
private array $buffer = [];
/** @var array<int,string> */
private array $parameterMap = [];
public function acceptOther(string $sql): void
{
$this->buffer[] = $sql;
}
public function acceptPositionalParameter(string $sql): void
{
$position = count($this->parameterMap) + 1;
$param = ':param' . $position;
$this->parameterMap[$position] = $param;
$this->buffer[] = $param;
}
public function acceptNamedParameter(string $sql): void
{
$this->buffer[] = $sql;
}
public function getSQL(): string
{
return implode('', $this->buffer);
}
/** @return array<int,string> */
public function getParameterMap(): array
{
return $this->parameterMap;
}
}

View File

@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\OCI8;
use Doctrine\DBAL\Driver\AbstractOracleDriver;
use Doctrine\DBAL\Driver\OCI8\Exception\ConnectionFailed;
use Doctrine\DBAL\Driver\OCI8\Exception\InvalidConfiguration;
use SensitiveParameter;
use function oci_connect;
use function oci_new_connect;
use function oci_pconnect;
use const OCI_NO_AUTO_COMMIT;
/**
* A Doctrine DBAL driver for the Oracle OCI8 PHP extensions.
*/
final class Driver extends AbstractOracleDriver
{
/**
* {@inheritDoc}
*/
public function connect(
#[SensitiveParameter]
array $params,
): Connection {
$username = $params['user'] ?? '';
$password = $params['password'] ?? '';
$charset = $params['charset'] ?? '';
$sessionMode = $params['sessionMode'] ?? OCI_NO_AUTO_COMMIT;
$connectionString = $this->getEasyConnectString($params);
$persistent = ! empty($params['persistent']);
$exclusive = ! empty($params['driverOptions']['exclusive']);
if ($persistent && $exclusive) {
throw InvalidConfiguration::forPersistentAndExclusive();
}
if ($persistent) {
$connection = @oci_pconnect($username, $password, $connectionString, $charset, $sessionMode);
} elseif ($exclusive) {
$connection = @oci_new_connect($username, $password, $connectionString, $charset, $sessionMode);
} else {
$connection = @oci_connect($username, $password, $connectionString, $charset, $sessionMode);
}
if ($connection === false) {
throw ConnectionFailed::new();
}
return new Connection($connection);
}
}

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\OCI8\Exception;
use Doctrine\DBAL\Driver\AbstractException;
use function assert;
use function oci_error;
/** @internal */
final class ConnectionFailed extends AbstractException
{
public static function new(): self
{
$error = oci_error();
assert($error !== false);
return new self($error['message'], null, $error['code']);
}
}

View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\OCI8\Exception;
use Doctrine\DBAL\Driver\AbstractException;
use function assert;
use function oci_error;
/** @internal */
final class Error extends AbstractException
{
/** @param resource $resource */
public static function new($resource): self
{
$error = oci_error($resource);
assert($error !== false);
return new self($error['message'], null, $error['code']);
}
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\OCI8\Exception;
use Doctrine\DBAL\Driver\AbstractException;
/** @internal */
final class InvalidConfiguration extends AbstractException
{
public static function forPersistentAndExclusive(): self
{
return new self('The "persistent" parameter and the "exclusive" driver option are mutually exclusive');
}
}

View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\OCI8\Exception;
use Doctrine\DBAL\Driver\AbstractException;
use function sprintf;
/** @internal */
final class NonTerminatedStringLiteral extends AbstractException
{
public static function new(int $offset): self
{
return new self(
sprintf(
'The statement contains non-terminated string literal starting at offset %d.',
$offset,
),
);
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\OCI8\Exception;
use Doctrine\DBAL\Driver\AbstractException;
use function sprintf;
/** @internal */
final class UnknownParameterIndex extends AbstractException
{
public static function new(int $index): self
{
return new self(
sprintf('Could not find variable mapping with index %d, in the SQL statement', $index),
);
}
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\OCI8;
/**
* Encapsulates the execution mode that is shared between the connection and its statements.
*
* @internal This class is not covered by the backward compatibility promise
*/
final class ExecutionMode
{
private bool $isAutoCommitEnabled = true;
public function enableAutoCommit(): void
{
$this->isAutoCommitEnabled = true;
}
public function disableAutoCommit(): void
{
$this->isAutoCommitEnabled = false;
}
public function isAutoCommitEnabled(): bool
{
return $this->isAutoCommitEnabled;
}
}

View File

@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\OCI8\Middleware;
use Doctrine\DBAL\Driver;
use Doctrine\DBAL\Driver\Connection;
use Doctrine\DBAL\Driver\Middleware;
use Doctrine\DBAL\Driver\Middleware\AbstractDriverMiddleware;
use SensitiveParameter;
final class InitializeSession implements Middleware
{
public function wrap(Driver $driver): Driver
{
return new class ($driver) extends AbstractDriverMiddleware {
/**
* {@inheritDoc}
*/
public function connect(
#[SensitiveParameter]
array $params,
): Connection {
$connection = parent::connect($params);
$connection->exec(
'ALTER SESSION SET'
. " NLS_DATE_FORMAT = 'YYYY-MM-DD HH24:MI:SS'"
. " NLS_TIME_FORMAT = 'HH24:MI:SS'"
. " NLS_TIMESTAMP_FORMAT = 'YYYY-MM-DD HH24:MI:SS'"
. " NLS_TIMESTAMP_TZ_FORMAT = 'YYYY-MM-DD HH24:MI:SS TZH:TZM'"
. " NLS_NUMERIC_CHARACTERS = '.,'",
);
return $connection;
}
};
}
}

View File

@@ -0,0 +1,142 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\OCI8;
use Doctrine\DBAL\Driver\Exception;
use Doctrine\DBAL\Driver\FetchUtils;
use Doctrine\DBAL\Driver\OCI8\Exception\Error;
use Doctrine\DBAL\Driver\Result as ResultInterface;
use Doctrine\DBAL\Exception\InvalidColumnIndex;
use function oci_cancel;
use function oci_error;
use function oci_fetch_all;
use function oci_fetch_array;
use function oci_field_name;
use function oci_num_fields;
use function oci_num_rows;
use const OCI_ASSOC;
use const OCI_FETCHSTATEMENT_BY_COLUMN;
use const OCI_FETCHSTATEMENT_BY_ROW;
use const OCI_NUM;
use const OCI_RETURN_LOBS;
use const OCI_RETURN_NULLS;
final class Result implements ResultInterface
{
/**
* @internal The result can be only instantiated by its driver connection or statement.
*
* @param resource $statement
*/
public function __construct(private readonly mixed $statement)
{
}
public function fetchNumeric(): array|false
{
return $this->fetch(OCI_NUM);
}
public function fetchAssociative(): array|false
{
return $this->fetch(OCI_ASSOC);
}
public function fetchOne(): mixed
{
return FetchUtils::fetchOne($this);
}
/**
* {@inheritDoc}
*/
public function fetchAllNumeric(): array
{
return $this->fetchAll(OCI_NUM, OCI_FETCHSTATEMENT_BY_ROW);
}
/**
* {@inheritDoc}
*/
public function fetchAllAssociative(): array
{
return $this->fetchAll(OCI_ASSOC, OCI_FETCHSTATEMENT_BY_ROW);
}
/**
* {@inheritDoc}
*/
public function fetchFirstColumn(): array
{
return $this->fetchAll(OCI_NUM, OCI_FETCHSTATEMENT_BY_COLUMN)[0];
}
public function rowCount(): int
{
$count = oci_num_rows($this->statement);
if ($count !== false) {
return $count;
}
return 0;
}
public function columnCount(): int
{
$count = oci_num_fields($this->statement);
if ($count !== false) {
return $count;
}
return 0;
}
public function getColumnName(int $index): string
{
// OCI expects a 1-based index while DBAL works with a O-based index.
$name = @oci_field_name($this->statement, $index + 1);
if ($name === false) {
throw InvalidColumnIndex::new($index);
}
return $name;
}
public function free(): void
{
oci_cancel($this->statement);
}
/** @throws Exception */
private function fetch(int $mode): mixed
{
$result = oci_fetch_array($this->statement, $mode | OCI_RETURN_NULLS | OCI_RETURN_LOBS);
if ($result === false && oci_error($this->statement) !== false) {
throw Error::new($this->statement);
}
return $result;
}
/** @return array<mixed> */
private function fetchAll(int $mode, int $fetchStructure): array
{
oci_fetch_all(
$this->statement,
$result,
0,
-1,
$mode | OCI_RETURN_NULLS | $fetchStructure | OCI_RETURN_LOBS,
);
return $result;
}
}

View File

@@ -0,0 +1,103 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\OCI8;
use Doctrine\DBAL\Driver\OCI8\Exception\Error;
use Doctrine\DBAL\Driver\OCI8\Exception\UnknownParameterIndex;
use Doctrine\DBAL\Driver\Statement as StatementInterface;
use Doctrine\DBAL\ParameterType;
use function is_int;
use function oci_bind_by_name;
use function oci_execute;
use function oci_new_descriptor;
use const OCI_B_BIN;
use const OCI_B_BLOB;
use const OCI_COMMIT_ON_SUCCESS;
use const OCI_D_LOB;
use const OCI_NO_AUTO_COMMIT;
use const OCI_TEMP_BLOB;
use const SQLT_CHR;
final class Statement implements StatementInterface
{
/**
* @internal The statement can be only instantiated by its driver connection.
*
* @param resource $connection
* @param resource $statement
* @param array<int,string> $parameterMap
*/
public function __construct(
private readonly mixed $connection,
private readonly mixed $statement,
private readonly array $parameterMap,
private readonly ExecutionMode $executionMode,
) {
}
public function bindValue(int|string $param, mixed $value, ParameterType $type): void
{
if (is_int($param)) {
if (! isset($this->parameterMap[$param])) {
throw UnknownParameterIndex::new($param);
}
$param = $this->parameterMap[$param];
}
if ($type === ParameterType::LARGE_OBJECT) {
if ($value !== null) {
$lob = oci_new_descriptor($this->connection, OCI_D_LOB);
$lob->writeTemporary($value, OCI_TEMP_BLOB);
$value =& $lob;
} else {
$type = ParameterType::STRING;
}
}
if (
! @oci_bind_by_name(
$this->statement,
$param,
$value,
-1,
$this->convertParameterType($type),
)
) {
throw Error::new($this->statement);
}
}
/**
* Converts DBAL parameter type to oci8 parameter type
*/
private function convertParameterType(ParameterType $type): int
{
return match ($type) {
ParameterType::BINARY => OCI_B_BIN,
ParameterType::LARGE_OBJECT => OCI_B_BLOB,
default => SQLT_CHR,
};
}
public function execute(): Result
{
if ($this->executionMode->isAutoCommitEnabled()) {
$mode = OCI_COMMIT_ON_SUCCESS;
} else {
$mode = OCI_NO_AUTO_COMMIT;
}
$ret = @oci_execute($this->statement, $mode);
if (! $ret) {
throw Error::new($this->statement);
}
return new Result($this->statement);
}
}

View File

@@ -0,0 +1,133 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\PDO;
use Doctrine\DBAL\Driver\Connection as ConnectionInterface;
use Doctrine\DBAL\Driver\Exception\IdentityColumnsNotSupported;
use Doctrine\DBAL\Driver\Exception\NoIdentityValue;
use PDO;
use PDOException;
use PDOStatement;
use function assert;
final class Connection implements ConnectionInterface
{
/** @internal The connection can be only instantiated by its driver. */
public function __construct(private readonly PDO $connection)
{
$connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
}
public function exec(string $sql): int
{
try {
$result = $this->connection->exec($sql);
assert($result !== false);
return $result;
} catch (PDOException $exception) {
throw Exception::new($exception);
}
}
public function getServerVersion(): string
{
return $this->connection->getAttribute(PDO::ATTR_SERVER_VERSION);
}
public function prepare(string $sql): Statement
{
try {
$stmt = $this->connection->prepare($sql);
assert($stmt instanceof PDOStatement);
return new Statement($stmt);
} catch (PDOException $exception) {
throw Exception::new($exception);
}
}
public function query(string $sql): Result
{
try {
$stmt = $this->connection->query($sql);
assert($stmt instanceof PDOStatement);
return new Result($stmt);
} catch (PDOException $exception) {
throw Exception::new($exception);
}
}
public function quote(string $value): string
{
return $this->connection->quote($value);
}
public function lastInsertId(): int|string
{
try {
$value = $this->connection->lastInsertId();
} catch (PDOException $exception) {
assert($exception->errorInfo !== null);
[$sqlState] = $exception->errorInfo;
// if the PDO driver does not support this capability, PDO::lastInsertId() triggers an IM001 SQLSTATE
// see https://www.php.net/manual/en/pdo.lastinsertid.php
if ($sqlState === 'IM001') {
throw IdentityColumnsNotSupported::new();
}
// PDO PGSQL throws a 'lastval is not yet defined in this session' error when no identity value is
// available, with SQLSTATE 55000 'Object Not In Prerequisite State'
if ($sqlState === '55000' && $this->connection->getAttribute(PDO::ATTR_DRIVER_NAME) === 'pgsql') {
throw NoIdentityValue::new($exception);
}
throw Exception::new($exception);
}
// pdo_mysql & pdo_sqlite return '0', pdo_sqlsrv returns '' or false depending on the PHP version
if ($value === '0' || $value === '' || $value === false) {
throw NoIdentityValue::new();
}
return $value;
}
public function beginTransaction(): void
{
try {
$this->connection->beginTransaction();
} catch (PDOException $exception) {
throw Exception::new($exception);
}
}
public function commit(): void
{
try {
$this->connection->commit();
} catch (PDOException $exception) {
throw Exception::new($exception);
}
}
public function rollBack(): void
{
try {
$this->connection->rollBack();
} catch (PDOException $exception) {
throw Exception::new($exception);
}
}
public function getNativeConnection(): PDO
{
return $this->connection;
}
}

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\PDO;
use Doctrine\DBAL\Driver\AbstractException;
use PDOException;
/** @internal */
final class Exception extends AbstractException
{
public static function new(PDOException $exception): self
{
if ($exception->errorInfo !== null) {
[$sqlState, $code] = $exception->errorInfo;
$code ??= 0;
} else {
$code = $exception->getCode();
$sqlState = null;
}
return new self($exception->getMessage(), $sqlState, $code, $exception);
}
}

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\PDO\Exception;
use Doctrine\DBAL\Driver\AbstractException;
use function get_debug_type;
use function sprintf;
final class InvalidConfiguration extends AbstractException
{
public static function notAStringOrNull(string $key, mixed $value): self
{
return new self(sprintf(
'The %s configuration parameter is expected to be either a string or null, got %s.',
$key,
get_debug_type($value),
));
}
}

View File

@@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\PDO\MySQL;
use Doctrine\DBAL\Driver\AbstractMySQLDriver;
use Doctrine\DBAL\Driver\PDO\Connection;
use Doctrine\DBAL\Driver\PDO\Exception;
use Doctrine\DBAL\Driver\PDO\Exception\InvalidConfiguration;
use Doctrine\DBAL\Driver\PDO\PDOConnect;
use PDO;
use PDOException;
use SensitiveParameter;
use function is_string;
final class Driver extends AbstractMySQLDriver
{
use PDOConnect;
/**
* {@inheritDoc}
*/
public function connect(
#[SensitiveParameter]
array $params,
): Connection {
$driverOptions = $params['driverOptions'] ?? [];
if (! empty($params['persistent'])) {
$driverOptions[PDO::ATTR_PERSISTENT] = true;
}
foreach (['user', 'password'] as $key) {
if (isset($params[$key]) && ! is_string($params[$key])) {
throw InvalidConfiguration::notAStringOrNull($key, $params[$key]);
}
}
$safeParams = $params;
unset($safeParams['password']);
try {
$pdo = $this->doConnect(
$this->constructPdoDsn($safeParams),
$params['user'] ?? '',
$params['password'] ?? '',
$driverOptions,
);
} catch (PDOException $exception) {
throw Exception::new($exception);
}
return new Connection($pdo);
}
/**
* Constructs the MySQL PDO DSN.
*
* @param mixed[] $params
*/
private function constructPdoDsn(array $params): string
{
$dsn = 'mysql:';
if (isset($params['host']) && $params['host'] !== '') {
$dsn .= 'host=' . $params['host'] . ';';
}
if (isset($params['port'])) {
$dsn .= 'port=' . $params['port'] . ';';
}
if (isset($params['dbname'])) {
$dsn .= 'dbname=' . $params['dbname'] . ';';
}
if (isset($params['unix_socket'])) {
$dsn .= 'unix_socket=' . $params['unix_socket'] . ';';
}
if (isset($params['charset'])) {
$dsn .= 'charset=' . $params['charset'] . ';';
}
return $dsn;
}
}

View File

@@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\PDO\OCI;
use Doctrine\DBAL\Driver\AbstractOracleDriver;
use Doctrine\DBAL\Driver\PDO\Connection;
use Doctrine\DBAL\Driver\PDO\Exception;
use Doctrine\DBAL\Driver\PDO\Exception\InvalidConfiguration;
use Doctrine\DBAL\Driver\PDO\PDOConnect;
use PDO;
use PDOException;
use SensitiveParameter;
use function is_string;
final class Driver extends AbstractOracleDriver
{
use PDOConnect;
/**
* {@inheritDoc}
*/
public function connect(
#[SensitiveParameter]
array $params,
): Connection {
$driverOptions = $params['driverOptions'] ?? [];
if (! empty($params['persistent'])) {
$driverOptions[PDO::ATTR_PERSISTENT] = true;
}
foreach (['user', 'password'] as $key) {
if (isset($params[$key]) && ! is_string($params[$key])) {
throw InvalidConfiguration::notAStringOrNull($key, $params[$key]);
}
}
$safeParams = $params;
unset($safeParams['password']);
try {
$pdo = $this->doConnect(
$this->constructPdoDsn($params),
$params['user'] ?? '',
$params['password'] ?? '',
$driverOptions,
);
} catch (PDOException $exception) {
throw Exception::new($exception);
}
return new Connection($pdo);
}
/**
* Constructs the Oracle PDO DSN.
*
* @param mixed[] $params
*/
private function constructPdoDsn(array $params): string
{
$dsn = 'oci:dbname=' . $this->getEasyConnectString($params);
if (isset($params['charset'])) {
$dsn .= ';charset=' . $params['charset'];
}
return $dsn;
}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\PDO;
use PDO;
use const PHP_VERSION_ID;
/** @internal */
trait PDOConnect
{
/** @param array<int, mixed> $options */
private function doConnect(
string $dsn,
string $username,
string $password,
array $options,
): PDO {
if (PHP_VERSION_ID < 80400) {
return new PDO($dsn, $username, $password, $options);
}
return PDO::connect($dsn, $username, $password, $options);
}
}

View File

@@ -0,0 +1,125 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\PDO\PgSQL;
use Doctrine\DBAL\Driver\AbstractPostgreSQLDriver;
use Doctrine\DBAL\Driver\PDO\Connection;
use Doctrine\DBAL\Driver\PDO\Exception;
use Doctrine\DBAL\Driver\PDO\Exception\InvalidConfiguration;
use Doctrine\DBAL\Driver\PDO\PDOConnect;
use PDO;
use PDOException;
use SensitiveParameter;
use function is_string;
final class Driver extends AbstractPostgreSQLDriver
{
use PDOConnect;
/**
* {@inheritDoc}
*/
public function connect(
#[SensitiveParameter]
array $params,
): Connection {
$driverOptions = $params['driverOptions'] ?? [];
if (! empty($params['persistent'])) {
$driverOptions[PDO::ATTR_PERSISTENT] = true;
}
foreach (['user', 'password'] as $key) {
if (isset($params[$key]) && ! is_string($params[$key])) {
throw InvalidConfiguration::notAStringOrNull($key, $params[$key]);
}
}
$safeParams = $params;
unset($safeParams['password']);
try {
$pdo = $this->doConnect(
$this->constructPdoDsn($safeParams),
$params['user'] ?? '',
$params['password'] ?? '',
$driverOptions,
);
} catch (PDOException $exception) {
throw Exception::new($exception);
}
if (
! isset($driverOptions[PDO::PGSQL_ATTR_DISABLE_PREPARES])
|| $driverOptions[PDO::PGSQL_ATTR_DISABLE_PREPARES] === true
) {
$pdo->setAttribute(PDO::PGSQL_ATTR_DISABLE_PREPARES, true);
}
$connection = new Connection($pdo);
/* defining client_encoding via SET NAMES to avoid inconsistent DSN support
* - passing client_encoding via the 'options' param breaks pgbouncer support
*/
if (isset($params['charset'])) {
$connection->exec('SET NAMES \'' . $params['charset'] . '\'');
}
return $connection;
}
/**
* Constructs the Postgres PDO DSN.
*
* @param array<string, mixed> $params
*/
private function constructPdoDsn(array $params): string
{
$dsn = 'pgsql:';
if (isset($params['host']) && $params['host'] !== '') {
$dsn .= 'host=' . $params['host'] . ';';
}
if (isset($params['port']) && $params['port'] !== '') {
$dsn .= 'port=' . $params['port'] . ';';
}
if (isset($params['dbname'])) {
$dsn .= 'dbname=' . $params['dbname'] . ';';
}
if (isset($params['sslmode'])) {
$dsn .= 'sslmode=' . $params['sslmode'] . ';';
}
if (isset($params['sslrootcert'])) {
$dsn .= 'sslrootcert=' . $params['sslrootcert'] . ';';
}
if (isset($params['sslcert'])) {
$dsn .= 'sslcert=' . $params['sslcert'] . ';';
}
if (isset($params['sslkey'])) {
$dsn .= 'sslkey=' . $params['sslkey'] . ';';
}
if (isset($params['sslcrl'])) {
$dsn .= 'sslcrl=' . $params['sslcrl'] . ';';
}
if (isset($params['application_name'])) {
$dsn .= 'application_name=' . $params['application_name'] . ';';
}
if (isset($params['gssencmode'])) {
$dsn .= 'gssencmode=' . $params['gssencmode'] . ';';
}
return $dsn;
}
}

View File

@@ -0,0 +1,129 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\PDO;
use Doctrine\DBAL\Driver\Result as ResultInterface;
use Doctrine\DBAL\Exception\InvalidColumnIndex;
use PDO;
use PDOException;
use PDOStatement;
use ValueError;
final class Result implements ResultInterface
{
/** @internal The result can be only instantiated by its driver connection or statement. */
public function __construct(private readonly PDOStatement $statement)
{
}
public function fetchNumeric(): array|false
{
return $this->fetch(PDO::FETCH_NUM);
}
public function fetchAssociative(): array|false
{
return $this->fetch(PDO::FETCH_ASSOC);
}
public function fetchOne(): mixed
{
return $this->fetch(PDO::FETCH_COLUMN);
}
/**
* {@inheritDoc}
*/
public function fetchAllNumeric(): array
{
return $this->fetchAll(PDO::FETCH_NUM);
}
/**
* {@inheritDoc}
*/
public function fetchAllAssociative(): array
{
return $this->fetchAll(PDO::FETCH_ASSOC);
}
/**
* {@inheritDoc}
*/
public function fetchFirstColumn(): array
{
return $this->fetchAll(PDO::FETCH_COLUMN);
}
public function rowCount(): int
{
try {
return $this->statement->rowCount();
} catch (PDOException $exception) {
throw Exception::new($exception);
}
}
public function columnCount(): int
{
try {
return $this->statement->columnCount();
} catch (PDOException $exception) {
throw Exception::new($exception);
}
}
public function getColumnName(int $index): string
{
try {
$meta = $this->statement->getColumnMeta($index);
} catch (ValueError $exception) {
throw InvalidColumnIndex::new($index, $exception);
} catch (PDOException $exception) {
throw Exception::new($exception);
}
if ($meta === false) {
throw InvalidColumnIndex::new($index);
}
return $meta['name'];
}
public function free(): void
{
$this->statement->closeCursor();
}
/**
* @phpstan-param PDO::FETCH_* $mode
*
* @throws Exception
*/
private function fetch(int $mode): mixed
{
try {
return $this->statement->fetch($mode);
} catch (PDOException $exception) {
throw Exception::new($exception);
}
}
/**
* @phpstan-param PDO::FETCH_* $mode
*
* @return list<mixed>
*
* @throws Exception
*/
private function fetchAll(int $mode): array
{
try {
return $this->statement->fetchAll($mode);
} catch (PDOException $exception) {
throw Exception::new($exception);
}
}
}

View File

@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\PDO\SQLSrv;
use Doctrine\DBAL\Driver\Middleware\AbstractConnectionMiddleware;
use Doctrine\DBAL\Driver\PDO\Connection as PDOConnection;
use PDO;
final class Connection extends AbstractConnectionMiddleware
{
public function __construct(private readonly PDOConnection $connection)
{
parent::__construct($connection);
}
public function prepare(string $sql): Statement
{
return new Statement(
$this->connection->prepare($sql),
);
}
public function getNativeConnection(): PDO
{
return $this->connection->getNativeConnection();
}
}

View File

@@ -0,0 +1,119 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\PDO\SQLSrv;
use Doctrine\DBAL\Driver\AbstractSQLServerDriver;
use Doctrine\DBAL\Driver\AbstractSQLServerDriver\Exception\PortWithoutHost;
use Doctrine\DBAL\Driver\Exception;
use Doctrine\DBAL\Driver\PDO\Connection as PDOConnection;
use Doctrine\DBAL\Driver\PDO\Exception as PDOException;
use Doctrine\DBAL\Driver\PDO\Exception\InvalidConfiguration;
use Doctrine\DBAL\Driver\PDO\PDOConnect;
use PDO;
use SensitiveParameter;
use function is_int;
use function is_string;
use function sprintf;
final class Driver extends AbstractSQLServerDriver
{
use PDOConnect;
/**
* {@inheritDoc}
*/
public function connect(
#[SensitiveParameter]
array $params,
): Connection {
$driverOptions = $dsnOptions = [];
if (isset($params['driverOptions'])) {
foreach ($params['driverOptions'] as $option => $value) {
if (is_int($option)) {
$driverOptions[$option] = $value;
} else {
$dsnOptions[$option] = $value;
}
}
}
if (! empty($params['persistent'])) {
$driverOptions[PDO::ATTR_PERSISTENT] = true;
}
foreach (['user', 'password'] as $key) {
if (isset($params[$key]) && ! is_string($params[$key])) {
throw InvalidConfiguration::notAStringOrNull($key, $params[$key]);
}
}
$safeParams = $params;
unset($safeParams['password']);
try {
$pdo = $this->doConnect(
$this->constructDsn($safeParams, $dsnOptions),
$params['user'] ?? '',
$params['password'] ?? '',
$driverOptions,
);
} catch (\PDOException $exception) {
throw PDOException::new($exception);
}
return new Connection(new PDOConnection($pdo));
}
/**
* Constructs the Sqlsrv PDO DSN.
*
* @param mixed[] $params
* @param string[] $connectionOptions
*
* @throws Exception
*/
private function constructDsn(array $params, array $connectionOptions): string
{
$dsn = 'sqlsrv:server=';
if (isset($params['host'])) {
$dsn .= $params['host'];
if (isset($params['port'])) {
$dsn .= ',' . $params['port'];
}
} elseif (isset($params['port'])) {
throw PortWithoutHost::new();
}
if (isset($params['dbname'])) {
$connectionOptions['Database'] = $params['dbname'];
}
if (isset($params['MultipleActiveResultSets'])) {
$connectionOptions['MultipleActiveResultSets'] = $params['MultipleActiveResultSets'] ? 'true' : 'false';
}
return $dsn . $this->getConnectionOptionsDsn($connectionOptions);
}
/**
* Converts a connection options array to the DSN
*
* @param string[] $connectionOptions
*/
private function getConnectionOptionsDsn(array $connectionOptions): string
{
$connectionOptionsDsn = '';
foreach ($connectionOptions as $paramName => $paramValue) {
$connectionOptionsDsn .= sprintf(';%s=%s', $paramName, $paramValue);
}
return $connectionOptionsDsn;
}
}

View File

@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\PDO\SQLSrv;
use Doctrine\DBAL\Driver\Middleware\AbstractStatementMiddleware;
use Doctrine\DBAL\Driver\PDO\Statement as PDOStatement;
use Doctrine\DBAL\ParameterType;
use PDO;
final class Statement extends AbstractStatementMiddleware
{
/** @internal The statement can be only instantiated by its driver connection. */
public function __construct(private readonly PDOStatement $statement)
{
parent::__construct($statement);
}
public function bindValue(int|string $param, mixed $value, ParameterType $type): void
{
switch ($type) {
case ParameterType::LARGE_OBJECT:
case ParameterType::BINARY:
$this->statement->bindParamWithDriverOptions(
$param,
$value,
$type,
PDO::SQLSRV_ENCODING_BINARY,
);
break;
case ParameterType::ASCII:
$this->statement->bindParamWithDriverOptions(
$param,
$value,
ParameterType::STRING,
PDO::SQLSRV_ENCODING_SYSTEM,
);
break;
default:
$this->statement->bindValue($param, $value, $type);
}
}
}

View File

@@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\PDO\SQLite;
use Doctrine\DBAL\Driver\AbstractSQLiteDriver;
use Doctrine\DBAL\Driver\PDO\Connection;
use Doctrine\DBAL\Driver\PDO\Exception;
use Doctrine\DBAL\Driver\PDO\Exception\InvalidConfiguration;
use Doctrine\DBAL\Driver\PDO\PDOConnect;
use PDOException;
use SensitiveParameter;
use function array_intersect_key;
use function is_string;
final class Driver extends AbstractSQLiteDriver
{
use PDOConnect;
/**
* {@inheritDoc}
*/
public function connect(
#[SensitiveParameter]
array $params,
): Connection {
foreach (['user', 'password'] as $key) {
if (isset($params[$key]) && ! is_string($params[$key])) {
throw InvalidConfiguration::notAStringOrNull($key, $params[$key]);
}
}
try {
$pdo = $this->doConnect(
$this->constructPdoDsn(array_intersect_key($params, ['path' => true, 'memory' => true])),
$params['user'] ?? '',
$params['password'] ?? '',
$params['driverOptions'] ?? [],
);
} catch (PDOException $exception) {
throw Exception::new($exception);
}
return new Connection($pdo);
}
/**
* Constructs the Sqlite PDO DSN.
*
* @param array<string, mixed> $params
*/
private function constructPdoDsn(array $params): string
{
$dsn = 'sqlite:';
if (isset($params['path'])) {
$dsn .= $params['path'];
} elseif (isset($params['memory'])) {
$dsn .= ':memory:';
}
return $dsn;
}
}

Some files were not shown because too many files have changed in this diff Show More