Skip to content

Commit

Permalink
Extract DSN parsing into a dedicated class
Browse files Browse the repository at this point in the history
  • Loading branch information
derrabus committed Dec 30, 2022
1 parent 7e0d352 commit 856f6e9
Show file tree
Hide file tree
Showing 5 changed files with 429 additions and 197 deletions.
22 changes: 22 additions & 0 deletions UPGRADE.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,28 @@ awareness about deprecated code.

# Upgrade to 3.6

## Deprecated the `url` connection parameter

DBAL ships with a new and configurable DSN parser that can be used to parse a
database URL into connection parameters understood by `DriverManager`.

### Before

```php
$connection = DriverManager::getConnection(
['url' => 'mysql://my-user:t0ps3cr3t@my-host/my-database']
);
```

### After

```php
$dsnParser = new DsnParser(['mysql' => 'pdo_mysql']);
$connection = DriverManager::getConnection(
$dsnParser->parse('mysql://my-user:t0ps3cr3t@my-host/my-database')
);
```

## Deprecated `Connection::PARAM_*_ARRAY` constants

Use the corresponding constants on `ArrayParameterType` instead. Please be aware that
Expand Down
215 changes: 18 additions & 197 deletions src/DriverManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,15 @@
use Doctrine\DBAL\Driver\PDO;
use Doctrine\DBAL\Driver\SQLite3;
use Doctrine\DBAL\Driver\SQLSrv;
use Doctrine\DBAL\Exception\MalformedDsnException;
use Doctrine\DBAL\Tools\DsnParser;
use Doctrine\Deprecations\Deprecation;

use function array_keys;
use function array_merge;
use function assert;
use function class_implements;
use function in_array;
use function is_string;
use function is_subclass_of;
use function parse_str;
use function parse_url;
use function preg_replace;
use function rawurldecode;
use function str_replace;
use function strpos;
use function substr;

/**
* Factory for creating {@see Connection} instances.
Expand Down Expand Up @@ -266,17 +259,6 @@ private static function createDriver(array $params): Driver
throw Exception::driverRequired();
}

/**
* Normalizes the given connection URL path.
*
* @return string The normalized connection URL path
*/
private static function normalizeDatabaseUrlPath(string $urlPath): string
{
// Trim leading slash from URL path.
return substr($urlPath, 1);
}

/**
* Extracts parts from a database URL, if present, and returns an
* updated list of parameters.
Expand All @@ -296,190 +278,29 @@ private static function parseDatabaseUrl(array $params): array
return $params;
}

// (pdo_)?sqlite3?:///... => (pdo_)?sqlite3?://localhost/... or else the URL will be invalid
$url = preg_replace('#^((?:pdo_)?sqlite3?):///#', '$1://localhost/', $params['url']);
assert($url !== null);

$url = parse_url($url);

if ($url === false) {
throw new Exception('Malformed parameter "url".');
}

foreach ($url as $param => $value) {
if (! is_string($value)) {
continue;
}

$url[$param] = rawurldecode($value);
}

$params = self::parseDatabaseUrlScheme($url['scheme'] ?? null, $params);

if (isset($url['host'])) {
$params['host'] = $url['host'];
}

if (isset($url['port'])) {
$params['port'] = $url['port'];
}

if (isset($url['user'])) {
$params['user'] = $url['user'];
}

if (isset($url['pass'])) {
$params['password'] = $url['pass'];
}

$params = self::parseDatabaseUrlPath($url, $params);
$params = self::parseDatabaseUrlQuery($url, $params);

return $params;
}

/**
* Parses the given connection URL and resolves the given connection parameters.
*
* Assumes that the connection URL scheme is already parsed and resolved into the given connection parameters
* via {@see parseDatabaseUrlScheme}.
*
* @see parseDatabaseUrlScheme
*
* @param mixed[] $url The URL parts to evaluate.
* @param mixed[] $params The connection parameters to resolve.
*
* @return mixed[] The resolved connection parameters.
*/
private static function parseDatabaseUrlPath(array $url, array $params): array
{
if (! isset($url['path'])) {
return $params;
}

$url['path'] = self::normalizeDatabaseUrlPath($url['path']);

// If we do not have a known DBAL driver, we do not know any connection URL path semantics to evaluate
// and therefore treat the path as regular DBAL connection URL path.
if (! isset($params['driver'])) {
return self::parseRegularDatabaseUrlPath($url, $params);
}

if (strpos($params['driver'], 'sqlite') !== false) {
return self::parseSqliteDatabaseUrlPath($url, $params);
Deprecation::trigger(
'doctrine/dbal',
'https://github.com/doctrine/dbal/pull/5843',
'The "url" connection parameter is deprecated. Please use %s to parse a database url before calling %s.',
DsnParser::class,
self::class,
);

$parser = new DsnParser(self::$driverSchemeAliases);
try {
$parsedParams = $parser->parse($params['url']);
} catch (MalformedDsnException $e) {
throw new Exception('Malformed parameter "url".', 0, $e);
}

return self::parseRegularDatabaseUrlPath($url, $params);
}

/**
* Parses the query part of the given connection URL and resolves the given connection parameters.
*
* @param mixed[] $url The connection URL parts to evaluate.
* @param mixed[] $params The connection parameters to resolve.
*
* @return mixed[] The resolved connection parameters.
*/
private static function parseDatabaseUrlQuery(array $url, array $params): array
{
if (! isset($url['query'])) {
return $params;
}

$query = [];

parse_str($url['query'], $query); // simply ingest query as extra params, e.g. charset or sslmode

return array_merge($params, $query); // parse_str wipes existing array elements
}

/**
* Parses the given regular connection URL and resolves the given connection parameters.
*
* Assumes that the "path" URL part is already normalized via {@see normalizeDatabaseUrlPath}.
*
* @see normalizeDatabaseUrlPath
*
* @param mixed[] $url The regular connection URL parts to evaluate.
* @param mixed[] $params The connection parameters to resolve.
*
* @return mixed[] The resolved connection parameters.
*/
private static function parseRegularDatabaseUrlPath(array $url, array $params): array
{
$params['dbname'] = $url['path'];

return $params;
}

/**
* Parses the given SQLite connection URL and resolves the given connection parameters.
*
* Assumes that the "path" URL part is already normalized via {@see normalizeDatabaseUrlPath}.
*
* @see normalizeDatabaseUrlPath
*
* @param mixed[] $url The SQLite connection URL parts to evaluate.
* @param mixed[] $params The connection parameters to resolve.
*
* @return mixed[] The resolved connection parameters.
*/
private static function parseSqliteDatabaseUrlPath(array $url, array $params): array
{
if ($url['path'] === ':memory:') {
$params['memory'] = true;

return $params;
}

$params['path'] = $url['path']; // pdo_sqlite driver uses 'path' instead of 'dbname' key

return $params;
}

/**
* Parses the scheme part from given connection URL and resolves the given connection parameters.
*
* @param string|null $scheme The connection URL scheme, if available
* @param mixed[] $params The connection parameters to resolve.
*
* @return mixed[] The resolved connection parameters.
*
* @throws Exception If parsing failed or resolution is not possible.
*/
private static function parseDatabaseUrlScheme(?string $scheme, array $params): array
{
if ($scheme !== null) {
if (isset($parsedParams['driver'])) {
// The requested driver from the URL scheme takes precedence
// over the default custom driver from the connection parameters (if any).
unset($params['driverClass']);

// URL schemes must not contain underscores, but dashes are ok
$driver = str_replace('-', '_', $scheme);

// If the driver is an alias (e.g. "postgres"), map it to the actual name ("pdo-pgsql").
// Otherwise, let checkParams decide later if the driver exists.
if (isset(self::$driverSchemeAliases[$driver])) {
$actualDriver = self::$driverSchemeAliases[$driver];

Deprecation::trigger(
'doctrine/dbal',
'https://github.com/doctrine/dbal/pull/5697',
'Relying on driver name aliases is deprecated. Use %s instead of %s.',
str_replace('_', '-', $actualDriver),
$driver,
);

$driver = $actualDriver;
}

// The requested driver from the URL scheme takes precedence over the
// default driver from the connection parameters.
$params['driver'] = $driver;

return $params;
}

$params = array_merge($params, $parsedParams);

// If a schemeless connection URL is given, we require a default driver or default custom driver
// as connection parameter.
if (! isset($params['driverClass']) && ! isset($params['driver'])) {
Expand Down
14 changes: 14 additions & 0 deletions src/Exception/MalformedDsnException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

namespace Doctrine\DBAL\Exception;

use Doctrine\DBAL\Exception;

/** @psalm-immutable */
class MalformedDsnException extends Exception
{
public static function new(): self
{
return new self('Malformed database connection URL');
}
}
Loading

0 comments on commit 856f6e9

Please sign in to comment.