Add support for URL-like DSNs for the PdoSessionHandler

This allows migrating away from the deprecated DbalSessionHandler when
DBAL was used for its ability to be configured through a URL (which is
what is provided on Heroku and some other PaaS).
This commit is contained in:
Christophe Coevoet 2018-02-19 12:14:59 +01:00
parent fcca141059
commit 14c35ad13c
2 changed files with 134 additions and 1 deletions

View File

@ -164,7 +164,7 @@ class PdoSessionHandler extends AbstractSessionHandler
* * db_connection_options: An array of driver-specific connection options [default: array()]
* * lock_mode: The strategy for locking, see constants [default: LOCK_TRANSACTIONAL]
*
* @param \PDO|string|null $pdoOrDsn A \PDO instance or DSN string or null
* @param \PDO|string|null $pdoOrDsn A \PDO instance or DSN string or URL string or null
* @param array $options An associative array of options
*
* @throws \InvalidArgumentException When PDO error mode is not PDO::ERRMODE_EXCEPTION
@ -178,6 +178,8 @@ class PdoSessionHandler extends AbstractSessionHandler
$this->pdo = $pdoOrDsn;
$this->driver = $this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME);
} elseif (is_string($pdoOrDsn) && false !== strpos($pdoOrDsn, '://')) {
$this->dsn = $this->buildDsnFromUrl($pdoOrDsn);
} else {
$this->dsn = $pdoOrDsn;
}
@ -431,6 +433,102 @@ class PdoSessionHandler extends AbstractSessionHandler
$this->driver = $this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME);
}
/**
* Builds a PDO DSN from a URL-like connection string.
*
* @param string $dsnOrUrl
*
* @return string
*
* @todo implement missing support for oci DSN (which look totally different from other PDO ones)
*/
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 = array(
'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']));
}
}
/**
* Helper method to begin a transaction.
*

View File

@ -324,6 +324,41 @@ class PdoSessionHandlerTest extends TestCase
$this->assertInstanceOf('\PDO', $method->invoke($storage));
}
/**
* @dataProvider provideUrlDsnPairs
*/
public function testUrlDsn($url, $expectedDsn, $expectedUser = null, $expectedPassword = null)
{
$storage = new PdoSessionHandler($url);
$this->assertAttributeEquals($expectedDsn, 'dsn', $storage);
if (null !== $expectedUser) {
$this->assertAttributeEquals($expectedUser, 'username', $storage);
}
if (null !== $expectedPassword) {
$this->assertAttributeEquals($expectedPassword, 'password', $storage);
}
}
public function provideUrlDsnPairs()
{
yield array('mysql://localhost/test', 'mysql:host=localhost;dbname=test;');
yield array('mysql://localhost:56/test', 'mysql:host=localhost;port=56;dbname=test;');
yield array('mysql2://root:pwd@localhost/test', 'mysql:host=localhost;dbname=test;', 'root', 'pwd');
yield array('postgres://localhost/test', 'pgsql:host=localhost;dbname=test;');
yield array('postgresql://localhost:5634/test', 'pgsql:host=localhost;port=5634;dbname=test;');
yield array('postgres://root:pwd@localhost/test', 'pgsql:host=localhost;dbname=test;', 'root', 'pwd');
yield 'sqlite relative path' => array('sqlite://localhost/tmp/test', 'sqlite:tmp/test');
yield 'sqlite absolute path' => array('sqlite://localhost//tmp/test', 'sqlite:/tmp/test');
yield 'sqlite relative path without host' => array('sqlite:///tmp/test', 'sqlite:tmp/test');
yield 'sqlite absolute path without host' => array('sqlite3:////tmp/test', 'sqlite:/tmp/test');
yield array('sqlite://localhost/:memory:', 'sqlite::memory:');
yield array('mssql://localhost/test', 'sqlsrv:server=localhost;Database=test');
yield array('mssql://localhost:56/test', 'sqlsrv:server=localhost,56;Database=test');
}
private function createStream($content)
{
$stream = tmpfile();