Skip to content

Commit

Permalink
Use the DsnParser to handle the url option
Browse files Browse the repository at this point in the history
  • Loading branch information
stof committed Jun 5, 2023
1 parent 94c583d commit 1e404b1
Show file tree
Hide file tree
Showing 9 changed files with 167 additions and 2 deletions.
80 changes: 79 additions & 1 deletion ConnectionFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@
use Doctrine\DBAL\Configuration;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\DriverManager;
use Doctrine\DBAL\Exception;
use Doctrine\DBAL\Exception as DBALException;
use Doctrine\DBAL\Exception\DriverException;
use Doctrine\DBAL\Exception\MalformedDsnException;
use Doctrine\DBAL\Platforms\AbstractMySQLPlatform;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Tools\DsnParser;
use Doctrine\DBAL\Types\Type;
use Doctrine\Deprecations\Deprecation;

Expand All @@ -22,15 +25,33 @@
/** @psalm-import-type Params from DriverManager */
class ConnectionFactory
{
/**
* @internal
*/
public const DEFAULT_SCHEME_MAP = [
'db2' => 'ibm_db2',
'mssql' => 'pdo_sqlsrv',
'mysql' => 'pdo_mysql',
'mysql2' => 'pdo_mysql', // Amazon RDS, for some weird reason
'postgres' => 'pdo_pgsql',
'postgresql' => 'pdo_pgsql',
'pgsql' => 'pdo_pgsql',
'sqlite' => 'pdo_sqlite',
'sqlite3' => 'pdo_sqlite',
];

/** @var mixed[][] */
private array $typesConfig = [];

private DsnParser $dsnParser;

private bool $initialized = false;

/** @param mixed[][] $typesConfig */
public function __construct(array $typesConfig)
public function __construct(array $typesConfig, ?DsnParser $dsnParser = null)
{
$this->typesConfig = $typesConfig;
$this->dsnParser = $dsnParser ?? new DsnParser(self::DEFAULT_SCHEME_MAP);
}

/**
Expand All @@ -55,6 +76,19 @@ public function createConnection(array $params, ?Configuration $config = null, ?
unset($params['connection_override_options']);
}

$params = $this->parseDatabaseUrl($params);

// URL support for PrimaryReplicaConnection
if (isset($params['primary'])) {
$params['primary'] = $this->parseDatabaseUrl($params['primary']);
}

if (isset($params['replica'])) {
foreach ($params['replica'] as $key => $replicaParams) {
$params['replica'][$key] = $this->parseDatabaseUrl($replicaParams);
}
}

if (! isset($params['pdo']) && (! isset($params['charset']) || $overriddenOptions || isset($params['dbname_suffix']))) {
$wrapperClass = null;

Expand Down Expand Up @@ -182,4 +216,48 @@ private function addDatabaseSuffix(array $params): array

return $params;
}

/**
* Extracts parts from a database URL, if present, and returns an
* updated list of parameters.
*
* @param mixed[] $params The list of parameters.
* @psalm-param Params $params
*
* @return mixed[] A modified list of parameters with info from a database
* URL extracted into individual parameter parts.
* @psalm-return Params
*
* @throws Exception
*/
private function parseDatabaseUrl(array $params): array
{
if (! isset($params['url'])) {
return $params;
}

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

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']);
}

$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'])) {
throw Exception::driverRequired($params['url']);
}

unset($params['url']);

return $params;
}
}
30 changes: 29 additions & 1 deletion DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Mapping\ClassMetadataFactory;
use InvalidArgumentException;
use ReflectionClass;
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
use Symfony\Component\Config\Definition\Builder\NodeDefinition;
Expand Down Expand Up @@ -74,7 +75,7 @@ public function getConfigTreeBuilder(): TreeBuilder
private function addDbalSection(ArrayNodeDefinition $node): void
{
// Key that should not be rewritten to the connection config
$excludedKeys = ['default_connection' => true, 'types' => true, 'type' => true];
$excludedKeys = ['default_connection' => true, 'driver_schemes' => true, 'driver_scheme' => true, 'types' => true, 'type' => true];

$node
->children()
Expand Down Expand Up @@ -135,6 +136,33 @@ private function addDbalSection(ArrayNodeDefinition $node): void
->end()
->end()
->end()
->fixXmlConfig('driver_scheme')
->children()
->arrayNode('driver_schemes')
->useAttributeAsKey('scheme')
->normalizeKeys(false)
->scalarPrototype()->end()
->info('Defines a driver for given URL schemes. Schemes being driver names cannot be redefined. However, other default schemes can be overwritten.')
->validate()
->always()
->then(function (array $value) {
$unsupportedSchemes = [];

foreach ($value as $scheme => $driver) {
if (\in_array($scheme, ['pdo-mysql', 'pdo-sqlite', 'pdo-pgsql', 'pdo-oci', 'oci8', 'ibm-db2', 'pdo-sqlsrv', 'mysqli', 'pgsql', 'sqlsrv', 'sqlite3'], true)) {
$unsupportedSchemes[] = $scheme;
}
}

if ($unsupportedSchemes) {
throw new InvalidArgumentException(sprintf('Registering a scheme with the name of one of the official drivers is forbidden, as those are defined in DBAL itself. The following schemes are forbidden: %s', implode(', ', $unsupportedSchemes)));
}

return $value;
})
->end()
->end()
->end()
->fixXmlConfig('connection')
->append($this->getDbalConnectionsNode())
->end();
Expand Down
3 changes: 3 additions & 0 deletions DependencyInjection/DoctrineExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Doctrine\Bundle\DoctrineBundle\Attribute\AsEntityListener;
use Doctrine\Bundle\DoctrineBundle\Attribute\AsMiddleware;
use Doctrine\Bundle\DoctrineBundle\CacheWarmer\DoctrineMetadataCacheWarmer;
use Doctrine\Bundle\DoctrineBundle\ConnectionFactory;
use Doctrine\Bundle\DoctrineBundle\Dbal\ManagerRegistryAwareConnectionProvider;
use Doctrine\Bundle\DoctrineBundle\Dbal\RegexSchemaAssetFilter;
use Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\IdGeneratorPass;
Expand Down Expand Up @@ -172,6 +173,8 @@ protected function dbalLoad(array $config, ContainerBuilder $container)

$container->setParameter('doctrine.dbal.connection_factory.types', $config['types']);

$container->getDefinition('doctrine.dbal.connection_factory.dsn_parser')->setArgument(0, array_merge(ConnectionFactory::DEFAULT_SCHEME_MAP, $config['driver_schemes']));

$connections = [];

foreach (array_keys($config['connections']) as $name) {
Expand Down
5 changes: 5 additions & 0 deletions Resources/config/dbal.xml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@

<service id="doctrine.dbal.connection_factory" class="%doctrine.dbal.connection_factory.class%">
<argument>%doctrine.dbal.connection_factory.types%</argument>
<argument type="service" id="doctrine.dbal.connection_factory.dsn_parser" />
</service>

<service id="doctrine.dbal.connection_factory.dsn_parser" class="Doctrine\DBAL\Tools\DsnParser">
<argument type="collection" />
</service>

<service id="doctrine.dbal.connection" class="Doctrine\DBAL\Connection" abstract="true">
Expand Down
9 changes: 9 additions & 0 deletions Resources/config/schema/doctrine-1.0.xsd
Original file line number Diff line number Diff line change
Expand Up @@ -89,13 +89,22 @@
<xsd:choice minOccurs="0" maxOccurs="unbounded">
<xsd:element name="connection" type="connection" />
<xsd:element name="type" type="named_scalar" />
<xsd:element name="driver-scheme" type="driver_scheme" />
<xsd:group ref="connection-child-config" />
</xsd:choice>

<xsd:attribute name="default-connection" type="xsd:string" />
<xsd:attributeGroup ref="connection-config" />
</xsd:complexType>

<xsd:complexType name="driver_scheme">
<xsd:simpleContent>
<xsd:extension base="xsd:string">
<xsd:attribute name="scheme" type="xsd:string" use="required" />
</xsd:extension>
</xsd:simpleContent>
</xsd:complexType>

<xsd:complexType name="option">
<xsd:complexContent>
<xsd:extension base="xsd:anyType">
Expand Down
10 changes: 10 additions & 0 deletions Tests/DependencyInjection/AbstractDoctrineExtensionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,16 @@ public function testDbalDbnameSuffix(): void
$this->assertSame('_test', $config['dbname_suffix']);
}

public function testDbalDriverScheme(): void
{
$container = $this->loadContainer('dbal_driver_schemes');
$schemes = $container->getDefinition('doctrine.dbal.connection_factory.dsn_parser')->getArgument(0);

$this->assertSame('my_driver', $schemes['my-scheme']);
$this->assertSame('pgsql', $schemes['postgresql'], 'Overriding a default mapping should be supported.');
$this->assertSame('pdo_mysql', $schemes['mysql']);
}

public function testDbalLoadSinglePrimaryReplicaConnection(): void
{
$container = $this->loadContainer('dbal_service_single_primary_replica_connection');
Expand Down
11 changes: 11 additions & 0 deletions Tests/DependencyInjection/DoctrineExtensionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
use Symfony\Bridge\Doctrine\Messenger\DoctrineClearEntityManagerWorkerSubscriber;
use Symfony\Component\Cache\Adapter\ArrayAdapter;
use Symfony\Component\Cache\Adapter\PhpArrayAdapter;
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\Compiler\ResolveChildDefinitionsPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
Expand Down Expand Up @@ -231,6 +232,16 @@ public function testDbalOverrideDefaultConnectionWithAdditionalConfiguration():
$this->assertEquals('foo', $container->getParameter('doctrine.default_connection'), '->load() overrides existing configuration options');
}

public function testDbalInvalidDriverScheme(): void
{
$extension = new DoctrineExtension();

$this->expectException(InvalidConfigurationException::class);
$this->expectExceptionMessage('Invalid configuration for path "doctrine.dbal.driver_schemes": Registering a scheme with the name of one of the official drivers is forbidden, as those are defined in DBAL itself. The following schemes are forbidden: pdo-mysql, pgsql');

$extension->load([['dbal' => ['driver_schemes' => ['pdo-mysql' => 'sqlite3', 'pgsql' => 'pgsql', 'other' => 'mysqli']]]], $this->getContainer());
}

public function testOrmRequiresDbal(): void
{
if (! interface_exists(EntityManagerInterface::class)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0" ?>

<srv:container xmlns="http://symfony.com/schema/dic/doctrine"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:srv="http://symfony.com/schema/dic/services"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd
http://symfony.com/schema/dic/doctrine http://symfony.com/schema/dic/doctrine/doctrine-1.0.xsd">

<config>
<dbal url="mysql://root:password@database:3306/main?serverVersion=mariadb-10.5.8">
<driver-scheme scheme="postgresql">pgsql</driver-scheme>
<driver-scheme scheme="my-scheme">my_driver</driver-scheme>
</dbal>
</config>
</srv:container>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
doctrine:
dbal:
url: 'mysql://root:password@database:3306/main?serverVersion=mariadb-10.5.8'
driver_schemes:
postgresql: pgsql
my-scheme: my_driver

0 comments on commit 1e404b1

Please sign in to comment.