Skip to content

Commit

Permalink
Allows URL DSN in Lock and Cache
Browse files Browse the repository at this point in the history
  • Loading branch information
jderusse committed Oct 22, 2019
1 parent 3f759d7 commit b184b31
Show file tree
Hide file tree
Showing 4 changed files with 256 additions and 2 deletions.
35 changes: 35 additions & 0 deletions src/Symfony/Component/Cache/Tests/Adapter/PdoAdapterTest.php
Expand Up @@ -71,4 +71,39 @@ public function testCleanupExpiredItems()
$this->assertFalse($newItem->isHit());
$this->assertSame(0, $getCacheItemCount(), 'PDOAdapter must clean up expired items');
}

/**
* @dataProvider provideUrlDsnPairs
*/
public function testUrlDsn($url, $expectedDsn, $expectedUser = null, $expectedPassword = null)
{
$store = new PdoAdapter($url);
$reflection = new \ReflectionClass(PdoAdapter::class);

foreach (['dsn' => $expectedDsn, 'username' => $expectedUser, 'password' => $expectedPassword] as $property => $expectedValue) {
if (!isset($expectedValue)) {
continue;
}
$property = $reflection->getProperty($property);
$property->setAccessible(true);
$this->assertSame($expectedValue, $property->getValue($store));
}
}

public function provideUrlDsnPairs()
{
yield ['mysql://localhost/test', 'mysql:host=localhost;dbname=test;'];
yield ['mysql://localhost:56/test', 'mysql:host=localhost;port=56;dbname=test;'];
yield ['mysql2://root:pwd@localhost/test', 'mysql:host=localhost;dbname=test;', 'root', 'pwd'];
yield ['postgres://localhost/test', 'pgsql:host=localhost;dbname=test;'];
yield ['postgresql://localhost:5634/test', 'pgsql:host=localhost;port=5634;dbname=test;'];
yield ['postgres://root:pwd@localhost/test', 'pgsql:host=localhost;dbname=test;', 'root', 'pwd'];
yield 'sqlite relative path' => ['sqlite://localhost/tmp/test', 'sqlite:tmp/test'];
yield 'sqlite absolute path' => ['sqlite://localhost//tmp/test', 'sqlite:/tmp/test'];
yield 'sqlite relative path without host' => ['sqlite:///tmp/test', 'sqlite:tmp/test'];
yield 'sqlite absolute path without host' => ['sqlite3:////tmp/test', 'sqlite:/tmp/test'];
yield ['sqlite://localhost/:memory:', 'sqlite::memory:'];
yield ['mssql://localhost/test', 'sqlsrv:server=localhost;Database=test'];
yield ['mssql://localhost:56/test', 'sqlsrv:server=localhost,56;Database=test'];
}
}
94 changes: 93 additions & 1 deletion src/Symfony/Component/Cache/Traits/PdoTrait.php
Expand Up @@ -55,7 +55,7 @@ private function init($connOrDsn, string $namespace, int $defaultLifetime, array
} elseif ($connOrDsn instanceof Connection) {
$this->conn = $connOrDsn;
} elseif (\is_string($connOrDsn)) {
$this->dsn = $connOrDsn;
$this->dsn = false !== strpos($connOrDsn, '://') ? $this->buildDsnFromUrl($connOrDsn) : $connOrDsn;;
} else {
throw new InvalidArgumentException(sprintf('"%s" requires PDO or Doctrine\DBAL\Connection instance or DSN string as first argument, "%s" given.', __CLASS__, \is_object($connOrDsn) ? \get_class($connOrDsn) : \gettype($connOrDsn)));
}
Expand Down Expand Up @@ -418,4 +418,96 @@ private function getServerVersion(): string

return $this->serverVersion;
}

/**
* Builds a PDO DSN from a URL-like connection string.
*
* @todo implement missing support for oci DSN (which look totally different from other PDO ones)
*/
private function buildDsnFromUrl(string $dsnOrUrl): string
{
// (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']));
}
}
}
94 changes: 93 additions & 1 deletion src/Symfony/Component/Lock/Store/PdoStore.php
Expand Up @@ -93,7 +93,7 @@ public function __construct($connOrDsn, array $options = [], float $gcProbabilit
} elseif ($connOrDsn instanceof Connection) {
$this->conn = $connOrDsn;
} elseif (\is_string($connOrDsn)) {
$this->dsn = $connOrDsn;
$this->dsn = false !== strpos($connOrDsn, '://') ? $this->buildDsnFromUrl($connOrDsn) : $connOrDsn;
} else {
throw new InvalidArgumentException(sprintf('"%s" requires PDO or Doctrine\DBAL\Connection instance or DSN string as first argument, "%s" given.', __CLASS__, \is_object($connOrDsn) ? \get_class($connOrDsn) : \gettype($connOrDsn)));
}
Expand Down Expand Up @@ -352,4 +352,96 @@ private function getCurrentTimestampStatement(): string
return time();
}
}

/**
* Builds a PDO DSN from a URL-like connection string.
*
* @todo implement missing support for oci DSN (which look totally different from other PDO ones)
*/
private function buildDsnFromUrl(string $dsnOrUrl): string
{
// (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']));
}
}
}
35 changes: 35 additions & 0 deletions src/Symfony/Component/Lock/Tests/Store/PdoStoreTest.php
Expand Up @@ -73,4 +73,39 @@ public function testInvalidTtlConstruct()

return new PdoStore('sqlite:'.self::$dbFile, [], 0.1, 0.1);
}

/**
* @dataProvider provideUrlDsnPairs
*/
public function testUrlDsn($url, $expectedDsn, $expectedUser = null, $expectedPassword = null)
{
$store = new PdoStore($url);
$reflection = new \ReflectionClass(PdoStore::class);

foreach (['dsn' => $expectedDsn, 'username' => $expectedUser, 'password' => $expectedPassword] as $property => $expectedValue) {
if (!isset($expectedValue)) {
continue;
}
$property = $reflection->getProperty($property);
$property->setAccessible(true);
$this->assertSame($expectedValue, $property->getValue($store));
}
}

public function provideUrlDsnPairs()
{
yield ['mysql://localhost/test', 'mysql:host=localhost;dbname=test;'];
yield ['mysql://localhost:56/test', 'mysql:host=localhost;port=56;dbname=test;'];
yield ['mysql2://root:pwd@localhost/test', 'mysql:host=localhost;dbname=test;', 'root', 'pwd'];
yield ['postgres://localhost/test', 'pgsql:host=localhost;dbname=test;'];
yield ['postgresql://localhost:5634/test', 'pgsql:host=localhost;port=5634;dbname=test;'];
yield ['postgres://root:pwd@localhost/test', 'pgsql:host=localhost;dbname=test;', 'root', 'pwd'];
yield 'sqlite relative path' => ['sqlite://localhost/tmp/test', 'sqlite:tmp/test'];
yield 'sqlite absolute path' => ['sqlite://localhost//tmp/test', 'sqlite:/tmp/test'];
yield 'sqlite relative path without host' => ['sqlite:///tmp/test', 'sqlite:tmp/test'];
yield 'sqlite absolute path without host' => ['sqlite3:////tmp/test', 'sqlite:/tmp/test'];
yield ['sqlite://localhost/:memory:', 'sqlite::memory:'];
yield ['mssql://localhost/test', 'sqlsrv:server=localhost;Database=test'];
yield ['mssql://localhost:56/test', 'sqlsrv:server=localhost,56;Database=test'];
}
}

0 comments on commit b184b31

Please sign in to comment.