Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

[DSN] Add a new DSN component #32546

Copy link
Copy link
Closed
Closed
Copy link
@Simperfit

Description

@Simperfit
Issue body actions

Description

Following this comment from @fabpot: #32392 (comment) and searching for information in the symfony repository
I've seen an attempt from @jderusse #24267. I still think the proposal was a good idea and we need a DSN component, we could even imagine a DSN contract, let me explain why:
I've looked into all of the symfony components to look for DSN usage, it seems that we use lot's of DSN and even in client project we use more and more DSN.

Lot's of symfony-linked librairies (doctrine, flysystem) use a DSN too, so instead of rechecking and reparsing the DSN all the time, we could have a simple dsn-parser that would do the job and return a DSN parsed as a PHP array/object.

If we talk about usage in symfony:

  • In the Cache 💾 component where are parsing two time the redis DSN:
    In the redis trait:

    if (0 === strpos($dsn, 'redis:')) {
    $scheme = 'redis';
    } elseif (0 === strpos($dsn, 'rediss:')) {
    $scheme = 'rediss';
    } else {
    throw new InvalidArgumentException(sprintf('Invalid Redis DSN: %s does not start with "redis:" or "rediss".', $dsn));
    }

    And in the adapter:
    if (0 === strpos($dsn, 'redis:') || 0 === strpos($dsn, 'rediss:')) {
    return RedisAdapter::createConnection($dsn, $options);
    }
    if (0 === strpos($dsn, 'memcached:')) {
    return MemcachedAdapter::createConnection($dsn, $options);
    }

    We are also parsing a memcache DSN in the adapter.
    Both of the redis check could be merged into one and an object could be passed between the two. The exception logic would be merge an a new exception would be thrown.

  • In the Messenger📮 component we are using DSN in connections classes and in the supports method, maybe a trait comming from the dns parser would help parsing this directly. I give you the exact lines of the code that each connection classes is doing but we know that's Doctrine AMQP, and redis. As an exemple we will only take doctrine.

    if (false === $components = parse_url($dsn)) {
    throw new InvalidArgumentException(sprintf('The given Doctrine Messenger DSN "%s" is invalid.', $dsn));
    }

  • In HttpKernel 📟 we parse a dsn too, that's the file DSN, it could be reworked to avoid parsing it in here and just getting the value without using substr:

    if (0 !== strpos($dsn, 'file:')) {
    throw new \RuntimeException(sprintf('Please check your configuration. You are trying to use FileStorage with an invalid dsn "%s". The expected format is "file:/path/to/the/storage/folder".', $dsn));
    }

  • In multiple places we parse PDO connection DSN with a throw if it's not a DSN, this could be all in the same places it will ease the maintenance and creating new adapter based on PDO:

    } elseif (\is_string($pdoOrDsn) && false !== strpos($pdoOrDsn, '://')) {
    $this->dsn = $this->buildDsnFromUrl($pdoOrDsn);
    with a method that fully parse de DSN.
    } elseif (\is_string($connOrDsn)) {
    $this->dsn = $connOrDsn;
    } else {

    And the PdoCache it uses the PdoTrait

  • In HttpFoundation 📡 we them found that big method that is parsing all kind of connection based sql DSN, this could totally be in the new DSN parser or in an dsn-sql-adapter ;).

    private function buildDsnFromUrl($dsnOrUrl)
    {
    // (pdo_)?sqlite3?:///... => (pdo_)?sqlite3?://localhost/... or else the URL will be invalid
    $url = preg_replace('#^((?:pdo_)?sqlite3?):///#', '$1://localhost/', $dsnOrUrl);
    $params = parse_url($url);
    if (false === $params) {
    return $dsnOrUrl; // If the URL is not valid, let's assume it might be a DSN already.
    }
    $params = array_map('rawurldecode', $params);
    // Override the default username and password. Values passed through options will still win over these in the constructor.
    if (isset($params['user'])) {
    $this->username = $params['user'];
    }
    if (isset($params['pass'])) {
    $this->password = $params['pass'];
    }
    if (!isset($params['scheme'])) {
    throw new \InvalidArgumentException('URLs without scheme are not supported to configure the PdoSessionHandler');
    }
    $driverAliasMap = [
    'mssql' => 'sqlsrv',
    'mysql2' => 'mysql', // Amazon RDS, for some weird reason
    'postgres' => 'pgsql',
    'postgresql' => 'pgsql',
    'sqlite3' => 'sqlite',
    ];
    $driver = isset($driverAliasMap[$params['scheme']]) ? $driverAliasMap[$params['scheme']] : $params['scheme'];
    // Doctrine DBAL supports passing its internal pdo_* driver names directly too (allowing both dashes and underscores). This allows supporting the same here.
    if (0 === strpos($driver, 'pdo_') || 0 === strpos($driver, 'pdo-')) {
    $driver = substr($driver, 4);
    }
    switch ($driver) {
    case 'mysql':
    case 'pgsql':
    $dsn = $driver.':';
    if (isset($params['host']) && '' !== $params['host']) {
    $dsn .= 'host='.$params['host'].';';
    }
    if (isset($params['port']) && '' !== $params['port']) {
    $dsn .= 'port='.$params['port'].';';
    }
    if (isset($params['path'])) {
    $dbName = substr($params['path'], 1); // Remove the leading slash
    $dsn .= 'dbname='.$dbName.';';
    }
    return $dsn;
    case 'sqlite':
    return 'sqlite:'.substr($params['path'], 1);
    case 'sqlsrv':
    $dsn = 'sqlsrv:server=';
    if (isset($params['host'])) {
    $dsn .= $params['host'];
    }
    if (isset($params['port']) && '' !== $params['port']) {
    $dsn .= ','.$params['port'];
    }
    if (isset($params['path'])) {
    $dbName = substr($params['path'], 1); // Remove the leading slash
    $dsn .= ';Database='.$dbName;
    }
    return $dsn;
    default:
    throw new \InvalidArgumentException(sprintf('The scheme "%s" is not supported by the PdoSessionHandler URL configuration. Pass a PDO DSN directly.', $params['scheme']));
    }
    }

  • In the Lock 🔒component we parse a DSN too in order to see what store we will instantiate:

    switch (true) {
    case 'flock' === $connection:
    return new FlockStore();
    case 0 === strpos($connection, 'flock://'):
    return new FlockStore(substr($connection, 8));
    case 'semaphore' === $connection:
    return new SemaphoreStore();
    case class_exists(AbstractAdapter::class) && preg_match('#^[a-z]++://#', $connection):
    return static::createStore(AbstractAdapter::createConnection($connection));
    default:
    throw new InvalidArgumentException(sprintf('Unsupported Connection: %s.', $connection));
    this could be refacoted to only getting the scheme and having a method that support or not the schema in each store for exemple.

  • And the Mailer components parses DSN too, the most widely used config param of the component is a DSN, all of this could simply in the DSN parser and will throw better exception when something is wrong

    if (false === $parsedDsn = parse_url($dsn)) {
    throw new InvalidArgumentException(sprintf('The "%s" mailer DSN is invalid.', $dsn));
    }
    if (!isset($parsedDsn['scheme'])) {
    throw new InvalidArgumentException(sprintf('The "%s" mailer DSN must contain a transport scheme.', $dsn));
    }
    if (!isset($parsedDsn['host'])) {
    throw new InvalidArgumentException(sprintf('The "%s" mailer DSN must contain a mailer name.', $dsn));
    }

As far I have looked into the code ans searched for DSN usages, I think we can really do something to avoid parsing all the DSN and having a system that will do it for us, to return the right types, the rights errors, the right scheme to have normalized error messages and interface on how we can parse DSN. It would avoid having the DSN parsed two times like in the RedisAdapter and in the RedisTrait.

Talking about symfony ecosystem (like doctrine for exemple) it will help too because since using DSN has been introduced widely when we use env var we can normalize on how we work with DSN.

Example

Like I said in the listing before, we could imagine the whole parsing logic being in the same place with a parser and them multiple connection like we've done with the mailer.
before would still be:

if (0 === strpos($dsn, 'redis:')) {
$scheme = 'redis';
} elseif (0 === strpos($dsn, 'rediss:')) {
$scheme = 'rediss';
} else {
throw new InvalidArgumentException(sprintf('Invalid Redis DSN: %s does not start with "redis:" or "rediss".', $dsn));
}

after it could looks like this (draaaft)
Either we inject the DsnParser or we use a static method that will return a Dsn Object:

Static version:

        $dsn = DsnParser::parse($dsn); // will return the correct Dsn object or throw if the Dsn is invalid
        $scheme = $dns->getSchema(); // will return the correct scheme

The object version is quite the same but we use the DependencyInjection to inject an instance of the DsnParser in the classe and to run parse on it.

Maybe we could even imagine a DSN component that contains all the connection that we have in symfony and all of them will be only in the DSN component, either we build a parser with different adapter either we build a whole connection object.

What do you think of this new component ?

Metadata

Metadata

Assignees

No one assigned

    Labels

    RFCRFC = Request For Comments (proposals about features that you want to be discussed)RFC = Request For Comments (proposals about features that you want to be discussed)

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

      Morty Proxy This is a proxified and sanitized view of the page, visit original site.