Skip to content

Commit dfd3ef3

Browse files
committed
Allows URL DSN in Lock and Cache
1 parent 3f759d7 commit dfd3ef3

File tree

4 files changed

+256
-2
lines changed

4 files changed

+256
-2
lines changed

src/Symfony/Component/Cache/Tests/Adapter/PdoAdapterTest.php

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,39 @@ public function testCleanupExpiredItems()
7171
$this->assertFalse($newItem->isHit());
7272
$this->assertSame(0, $getCacheItemCount(), 'PDOAdapter must clean up expired items');
7373
}
74+
75+
/**
76+
* @dataProvider provideUrlDsnPairs
77+
*/
78+
public function testUrlDsn($url, $expectedDsn, $expectedUser = null, $expectedPassword = null)
79+
{
80+
$store = new PdoAdapter($url);
81+
$reflection = new \ReflectionClass(PdoAdapter::class);
82+
83+
foreach (['dsn' => $expectedDsn, 'username' => $expectedUser, 'password' => $expectedPassword] as $property => $expectedValue) {
84+
if (!isset($expectedValue)) {
85+
continue;
86+
}
87+
$property = $reflection->getProperty($property);
88+
$property->setAccessible(true);
89+
$this->assertSame($expectedValue, $property->getValue($store));
90+
}
91+
}
92+
93+
public function provideUrlDsnPairs()
94+
{
95+
yield ['mysql://localhost/test', 'mysql:host=localhost;dbname=test;'];
96+
yield ['mysql://localhost:56/test', 'mysql:host=localhost;port=56;dbname=test;'];
97+
yield ['mysql2://root:pwd@localhost/test', 'mysql:host=localhost;dbname=test;', 'root', 'pwd'];
98+
yield ['postgres://localhost/test', 'pgsql:host=localhost;dbname=test;'];
99+
yield ['postgresql://localhost:5634/test', 'pgsql:host=localhost;port=5634;dbname=test;'];
100+
yield ['postgres://root:pwd@localhost/test', 'pgsql:host=localhost;dbname=test;', 'root', 'pwd'];
101+
yield 'sqlite relative path' => ['sqlite://localhost/tmp/test', 'sqlite:tmp/test'];
102+
yield 'sqlite absolute path' => ['sqlite://localhost//tmp/test', 'sqlite:/tmp/test'];
103+
yield 'sqlite relative path without host' => ['sqlite:///tmp/test', 'sqlite:tmp/test'];
104+
yield 'sqlite absolute path without host' => ['sqlite3:////tmp/test', 'sqlite:/tmp/test'];
105+
yield ['sqlite://localhost/:memory:', 'sqlite::memory:'];
106+
yield ['mssql://localhost/test', 'sqlsrv:server=localhost;Database=test'];
107+
yield ['mssql://localhost:56/test', 'sqlsrv:server=localhost,56;Database=test'];
108+
}
74109
}

src/Symfony/Component/Cache/Traits/PdoTrait.php

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ private function init($connOrDsn, string $namespace, int $defaultLifetime, array
5555
} elseif ($connOrDsn instanceof Connection) {
5656
$this->conn = $connOrDsn;
5757
} elseif (\is_string($connOrDsn)) {
58-
$this->dsn = $connOrDsn;
58+
$this->dsn = false !== strpos($connOrDsn, '://') ? $this->buildDsnFromUrl($connOrDsn) : $connOrDsn;
5959
} else {
6060
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)));
6161
}
@@ -418,4 +418,96 @@ private function getServerVersion(): string
418418

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

src/Symfony/Component/Lock/Store/PdoStore.php

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ public function __construct($connOrDsn, array $options = [], float $gcProbabilit
9393
} elseif ($connOrDsn instanceof Connection) {
9494
$this->conn = $connOrDsn;
9595
} elseif (\is_string($connOrDsn)) {
96-
$this->dsn = $connOrDsn;
96+
$this->dsn = false !== strpos($connOrDsn, '://') ? $this->buildDsnFromUrl($connOrDsn) : $connOrDsn;
9797
} else {
9898
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)));
9999
}
@@ -352,4 +352,96 @@ private function getCurrentTimestampStatement(): string
352352
return time();
353353
}
354354
}
355+
356+
/**
357+
* Builds a PDO DSN from a URL-like connection string.
358+
*
359+
* @todo implement missing support for oci DSN (which look totally different from other PDO ones)
360+
*/
361+
private function buildDsnFromUrl(string $dsnOrUrl): string
362+
{
363+
// (pdo_)?sqlite3?:///... => (pdo_)?sqlite3?://localhost/... or else the URL will be invalid
364+
$url = preg_replace('#^((?:pdo_)?sqlite3?):///#', '$1://localhost/', $dsnOrUrl);
365+
366+
$params = parse_url($url);
367+
368+
if (false === $params) {
369+
return $dsnOrUrl; // If the URL is not valid, let's assume it might be a DSN already.
370+
}
371+
372+
$params = array_map('rawurldecode', $params);
373+
374+
// Override the default username and password. Values passed through options will still win over these in the constructor.
375+
if (isset($params['user'])) {
376+
$this->username = $params['user'];
377+
}
378+
379+
if (isset($params['pass'])) {
380+
$this->password = $params['pass'];
381+
}
382+
383+
if (!isset($params['scheme'])) {
384+
throw new \InvalidArgumentException('URLs without scheme are not supported to configure the PdoSessionHandler');
385+
}
386+
387+
$driverAliasMap = [
388+
'mssql' => 'sqlsrv',
389+
'mysql2' => 'mysql', // Amazon RDS, for some weird reason
390+
'postgres' => 'pgsql',
391+
'postgresql' => 'pgsql',
392+
'sqlite3' => 'sqlite',
393+
];
394+
395+
$driver = isset($driverAliasMap[$params['scheme']]) ? $driverAliasMap[$params['scheme']] : $params['scheme'];
396+
397+
// Doctrine DBAL supports passing its internal pdo_* driver names directly too (allowing both dashes and underscores). This allows supporting the same here.
398+
if (0 === strpos($driver, 'pdo_') || 0 === strpos($driver, 'pdo-')) {
399+
$driver = substr($driver, 4);
400+
}
401+
402+
switch ($driver) {
403+
case 'mysql':
404+
case 'pgsql':
405+
$dsn = $driver.':';
406+
407+
if (isset($params['host']) && '' !== $params['host']) {
408+
$dsn .= 'host='.$params['host'].';';
409+
}
410+
411+
if (isset($params['port']) && '' !== $params['port']) {
412+
$dsn .= 'port='.$params['port'].';';
413+
}
414+
415+
if (isset($params['path'])) {
416+
$dbName = substr($params['path'], 1); // Remove the leading slash
417+
$dsn .= 'dbname='.$dbName.';';
418+
}
419+
420+
return $dsn;
421+
422+
case 'sqlite':
423+
return 'sqlite:'.substr($params['path'], 1);
424+
425+
case 'sqlsrv':
426+
$dsn = 'sqlsrv:server=';
427+
428+
if (isset($params['host'])) {
429+
$dsn .= $params['host'];
430+
}
431+
432+
if (isset($params['port']) && '' !== $params['port']) {
433+
$dsn .= ','.$params['port'];
434+
}
435+
436+
if (isset($params['path'])) {
437+
$dbName = substr($params['path'], 1); // Remove the leading slash
438+
$dsn .= ';Database='.$dbName;
439+
}
440+
441+
return $dsn;
442+
443+
default:
444+
throw new \InvalidArgumentException(sprintf('The scheme "%s" is not supported by the PdoSessionHandler URL configuration. Pass a PDO DSN directly.', $params['scheme']));
445+
}
446+
}
355447
}

src/Symfony/Component/Lock/Tests/Store/PdoStoreTest.php

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,4 +73,39 @@ public function testInvalidTtlConstruct()
7373

7474
return new PdoStore('sqlite:'.self::$dbFile, [], 0.1, 0.1);
7575
}
76+
77+
/**
78+
* @dataProvider provideUrlDsnPairs
79+
*/
80+
public function testUrlDsn($url, $expectedDsn, $expectedUser = null, $expectedPassword = null)
81+
{
82+
$store = new PdoStore($url);
83+
$reflection = new \ReflectionClass(PdoStore::class);
84+
85+
foreach (['dsn' => $expectedDsn, 'username' => $expectedUser, 'password' => $expectedPassword] as $property => $expectedValue) {
86+
if (!isset($expectedValue)) {
87+
continue;
88+
}
89+
$property = $reflection->getProperty($property);
90+
$property->setAccessible(true);
91+
$this->assertSame($expectedValue, $property->getValue($store));
92+
}
93+
}
94+
95+
public function provideUrlDsnPairs()
96+
{
97+
yield ['mysql://localhost/test', 'mysql:host=localhost;dbname=test;'];
98+
yield ['mysql://localhost:56/test', 'mysql:host=localhost;port=56;dbname=test;'];
99+
yield ['mysql2://root:pwd@localhost/test', 'mysql:host=localhost;dbname=test;', 'root', 'pwd'];
100+
yield ['postgres://localhost/test', 'pgsql:host=localhost;dbname=test;'];
101+
yield ['postgresql://localhost:5634/test', 'pgsql:host=localhost;port=5634;dbname=test;'];
102+
yield ['postgres://root:pwd@localhost/test', 'pgsql:host=localhost;dbname=test;', 'root', 'pwd'];
103+
yield 'sqlite relative path' => ['sqlite://localhost/tmp/test', 'sqlite:tmp/test'];
104+
yield 'sqlite absolute path' => ['sqlite://localhost//tmp/test', 'sqlite:/tmp/test'];
105+
yield 'sqlite relative path without host' => ['sqlite:///tmp/test', 'sqlite:tmp/test'];
106+
yield 'sqlite absolute path without host' => ['sqlite3:////tmp/test', 'sqlite:/tmp/test'];
107+
yield ['sqlite://localhost/:memory:', 'sqlite::memory:'];
108+
yield ['mssql://localhost/test', 'sqlsrv:server=localhost;Database=test'];
109+
yield ['mssql://localhost:56/test', 'sqlsrv:server=localhost,56;Database=test'];
110+
}
76111
}

0 commit comments

Comments
 (0)