Skip to content

Commit

Permalink
Merge pull request #5843 from derrabus/feature/dsn-parser
Browse files Browse the repository at this point in the history
Standalone DSN parser
  • Loading branch information
derrabus authored Dec 31, 2022
2 parents 11e7042 + d380886 commit 3d17514
Show file tree
Hide file tree
Showing 7 changed files with 517 additions and 212 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
63 changes: 49 additions & 14 deletions docs/en/reference/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ You can get a DBAL Connection through the
.. code-block:: php
<?php
use Doctrine\DBAL\DriverManager;
//..
$connectionParams = [
'dbname' => 'mydb',
Expand All @@ -18,18 +20,23 @@ You can get a DBAL Connection through the
'host' => 'localhost',
'driver' => 'pdo_mysql',
];
$conn = \Doctrine\DBAL\DriverManager::getConnection($connectionParams);
$conn = DriverManager::getConnection($connectionParams);
Or, using the simpler URL form:
Alternatively, if you store your connection settings as a connection URL (DSN),
you can parse the URL to extract connection parameters for ``DriverManager``:

.. code-block:: php
<?php
use Doctrine\DBAL\DriverManager;
use Doctrine\DBAL\Tools\DsnParser;
//..
$connectionParams = [
'url' => 'mysql://user:secret@localhost/mydb',
];
$conn = \Doctrine\DBAL\DriverManager::getConnection($connectionParams);
$dsnParser = new DsnParser();
$connectionParams = $dsnParser
->parse('mysqli://user:secret@localhost/mydb');
$conn = DriverManager::getConnection($connectionParams);
The ``DriverManager`` returns an instance of
``Doctrine\DBAL\Connection`` which is a wrapper around the
Expand All @@ -42,7 +49,7 @@ Connecting using a URL
~~~~~~~~~~~~~~~~~~~~~~

The easiest way to specify commonly used connection parameters is
using a database URL. The scheme is used to specify a driver, the
using a database URL or DSN. The scheme is used to specify a driver, the
user and password in the URL encode user and password for the
connection, followed by the host and port parts (the "authority").
The path after the authority part represents the name of the
Expand Down Expand Up @@ -90,14 +97,42 @@ database name::

pdo-sqlite:///:memory:

.. note::
Using the DSN parser
^^^^^^^^^^^^^^^^^^^^

By default, the URL scheme of the parsed DSN has to match one of DBAL's driver
names. However, it might be that you have to deal with connection strings where
you don't have control over the used scheme, e.g. in a PaaS environment. In
order to make the parser understand which driver to use e.g. for ``mysql://``
DSNs, you can configure the parser with a mapping table:

.. code-block:: php
<?php
use Doctrine\DBAL\Tools\DsnParser;
//..
$dsnParser = new DsnParser(['mysql' => 'mysqli', 'postgres' => 'pdo_pgsql']);
$connectionParams = $dsnParser
->parse('mysql://user:secret@localhost/mydb');
The DSN parser returns the connection params back to you so you can add or
modify individual parameters before passing the params to the
``DriverManager``. For example, you can add a database name if its missing in
the DSN or hardcode one if the DSN is not allowed to configure the database
name.

.. code-block:: php
<?php
use Doctrine\DBAL\DriverManager;
use Doctrine\DBAL\Tools\DsnParser;
//..
$connectionParams = $dsnParser->parse($myDsn);
$connectionParams['dbname'] ??= 'default_db';
Any information extracted from the URL overwrites existing values
for the parameter in question, but the rest of the information
is merged together. You could, for example, have a URL without
the ``charset`` setting in the query string, and then add a
``charset`` connection parameter next to ``url``, to provide a
default value in case the URL doesn't contain a charset value.
$conn = DriverManager::getConnection($connectionParams);
Driver
~~~~~~
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 InvalidArgumentException;

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

0 comments on commit 3d17514

Please sign in to comment.