Skip to content

Commit 14c35ad

Browse files
committed
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).
1 parent fcca141 commit 14c35ad

File tree

2 files changed

+134
-1
lines changed

2 files changed

+134
-1
lines changed

src/Symfony/Component/HttpFoundation/Session/Storage/Handler/PdoSessionHandler.php

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ class PdoSessionHandler extends AbstractSessionHandler
164164
* * db_connection_options: An array of driver-specific connection options [default: array()]
165165
* * lock_mode: The strategy for locking, see constants [default: LOCK_TRANSACTIONAL]
166166
*
167-
* @param \PDO|string|null $pdoOrDsn A \PDO instance or DSN string or null
167+
* @param \PDO|string|null $pdoOrDsn A \PDO instance or DSN string or URL string or null
168168
* @param array $options An associative array of options
169169
*
170170
* @throws \InvalidArgumentException When PDO error mode is not PDO::ERRMODE_EXCEPTION
@@ -178,6 +178,8 @@ public function __construct($pdoOrDsn = null, array $options = array())
178178

179179
$this->pdo = $pdoOrDsn;
180180
$this->driver = $this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME);
181+
} elseif (is_string($pdoOrDsn) && false !== strpos($pdoOrDsn, '://')) {
182+
$this->dsn = $this->buildDsnFromUrl($pdoOrDsn);
181183
} else {
182184
$this->dsn = $pdoOrDsn;
183185
}
@@ -431,6 +433,102 @@ private function connect($dsn)
431433
$this->driver = $this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME);
432434
}
433435

436+
/**
437+
* Builds a PDO DSN from a URL-like connection string.
438+
*
439+
* @param string $dsnOrUrl
440+
*
441+
* @return string
442+
*
443+
* @todo implement missing support for oci DSN (which look totally different from other PDO ones)
444+
*/
445+
private function buildDsnFromUrl($dsnOrUrl)
446+
{
447+
// (pdo_)?sqlite3?:///... => (pdo_)?sqlite3?://localhost/... or else the URL will be invalid
448+
$url = preg_replace('#^((?:pdo_)?sqlite3?):///#', '$1://localhost/', $dsnOrUrl);
449+
450+
$params = parse_url($url);
451+
452+
if (false === $params) {
453+
return $dsnOrUrl; // If the URL is not valid, let's assume it might be a DSN already.
454+
}
455+
456+
$params = array_map('rawurldecode', $params);
457+
458+
// Override the default username and password. Values passed through options will still win over these in the constructor.
459+
if (isset($params['user'])) {
460+
$this->username = $params['user'];
461+
}
462+
463+
if (isset($params['pass'])) {
464+
$this->password = $params['pass'];
465+
}
466+
467+
if (!isset($params['scheme'])) {
468+
throw new \InvalidArgumentException('URLs without scheme are not supported to configure the PdoSessionHandler');
469+
}
470+
471+
$driverAliasMap = array(
472+
'mssql' => 'sqlsrv',
473+
'mysql2' => 'mysql', // Amazon RDS, for some weird reason
474+
'postgres' => 'pgsql',
475+
'postgresql' => 'pgsql',
476+
'sqlite3' => 'sqlite',
477+
);
478+
479+
$driver = isset($driverAliasMap[$params['scheme']]) ? $driverAliasMap[$params['scheme']] : $params['scheme'];
480+
481+
// Doctrine DBAL supports passing its internal pdo_* driver names directly too (allowing both dashes and underscores). This allows supporting the same here.
482+
if (0 === strpos($driver, 'pdo_') || 0 === strpos($driver, 'pdo-')) {
483+
$driver = substr($driver, 4);
484+
}
485+
486+
switch ($driver) {
487+
case 'mysql':
488+
case 'pgsql':
489+
$dsn = $driver.':';
490+
491+
if (isset($params['host']) && '' !== $params['host']) {
492+
$dsn .= 'host='.$params['host'].';';
493+
}
494+
495+
if (isset($params['port']) && '' !== $params['port']) {
496+
$dsn .= 'port='.$params['port'].';';
497+
}
498+
499+
if (isset($params['path'])) {
500+
$dbName = substr($params['path'], 1); // Remove the leading slash
501+
$dsn .= 'dbname='.$dbName.';';
502+
}
503+
504+
return $dsn;
505+
506+
case 'sqlite':
507+
return 'sqlite:'.substr($params['path'], 1);
508+
509+
case 'sqlsrv':
510+
$dsn = 'sqlsrv:server=';
511+
512+
if (isset($params['host'])) {
513+
$dsn .= $params['host'];
514+
}
515+
516+
if (isset($params['port']) && '' !== $params['port']) {
517+
$dsn .= ','.$params['port'];
518+
}
519+
520+
if (isset($params['path'])) {
521+
$dbName = substr($params['path'], 1); // Remove the leading slash
522+
$dsn .= ';Database='.$dbName;
523+
}
524+
525+
return $dsn;
526+
527+
default:
528+
throw new \InvalidArgumentException(sprintf('The scheme "%s" is not supported by the PdoSessionHandler URL configuration. Pass a PDO DSN directly.', $params['scheme']));
529+
}
530+
}
531+
434532
/**
435533
* Helper method to begin a transaction.
436534
*

src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/PdoSessionHandlerTest.php

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,41 @@ public function testGetConnectionConnectsIfNeeded()
324324
$this->assertInstanceOf('\PDO', $method->invoke($storage));
325325
}
326326

327+
/**
328+
* @dataProvider provideUrlDsnPairs
329+
*/
330+
public function testUrlDsn($url, $expectedDsn, $expectedUser = null, $expectedPassword = null)
331+
{
332+
$storage = new PdoSessionHandler($url);
333+
334+
$this->assertAttributeEquals($expectedDsn, 'dsn', $storage);
335+
336+
if (null !== $expectedUser) {
337+
$this->assertAttributeEquals($expectedUser, 'username', $storage);
338+
}
339+
340+
if (null !== $expectedPassword) {
341+
$this->assertAttributeEquals($expectedPassword, 'password', $storage);
342+
}
343+
}
344+
345+
public function provideUrlDsnPairs()
346+
{
347+
yield array('mysql://localhost/test', 'mysql:host=localhost;dbname=test;');
348+
yield array('mysql://localhost:56/test', 'mysql:host=localhost;port=56;dbname=test;');
349+
yield array('mysql2://root:pwd@localhost/test', 'mysql:host=localhost;dbname=test;', 'root', 'pwd');
350+
yield array('postgres://localhost/test', 'pgsql:host=localhost;dbname=test;');
351+
yield array('postgresql://localhost:5634/test', 'pgsql:host=localhost;port=5634;dbname=test;');
352+
yield array('postgres://root:pwd@localhost/test', 'pgsql:host=localhost;dbname=test;', 'root', 'pwd');
353+
yield 'sqlite relative path' => array('sqlite://localhost/tmp/test', 'sqlite:tmp/test');
354+
yield 'sqlite absolute path' => array('sqlite://localhost//tmp/test', 'sqlite:/tmp/test');
355+
yield 'sqlite relative path without host' => array('sqlite:///tmp/test', 'sqlite:tmp/test');
356+
yield 'sqlite absolute path without host' => array('sqlite3:////tmp/test', 'sqlite:/tmp/test');
357+
yield array('sqlite://localhost/:memory:', 'sqlite::memory:');
358+
yield array('mssql://localhost/test', 'sqlsrv:server=localhost;Database=test');
359+
yield array('mssql://localhost:56/test', 'sqlsrv:server=localhost,56;Database=test');
360+
}
361+
327362
private function createStream($content)
328363
{
329364
$stream = tmpfile();

0 commit comments

Comments
 (0)