-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add (unused yet) Dsn class for databases
- Loading branch information
Showing
4 changed files
with
248 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,174 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace MakinaCorpus\QueryBuilder; | ||
|
||
/** | ||
* Various use case this DSN implements: | ||
* - driver://user:pass@host:port/database?arg=value&arg=value... | ||
* - driver:///path/to/file?arg=value&... (socket connection, or database file name). | ||
*/ | ||
class Dsn | ||
{ | ||
public const DRIVER_ANY = 'any'; | ||
public const DRIVER_DOCTRINE = 'doctrine'; | ||
public const DRIVER_EXT = 'ext'; | ||
public const DRIVER_PDO = 'pdo'; | ||
|
||
private string $scheme; | ||
private readonly bool $isFile; | ||
private readonly string $driver; | ||
private readonly string $vendor; | ||
|
||
public function __construct( | ||
/** Database vendor, eg. "mysql", "postgresql", ... */ | ||
string $vendor, | ||
/** Host or local filename (unix socket, database file) */ | ||
private readonly string $host, | ||
bool $isFile = false, | ||
/** Driver, eg. "pdo", "ext", ... */ | ||
?string $driver = null, | ||
private readonly ?string $database = null, | ||
private readonly ?string $user = null, | ||
#[\SensitiveParameter] | ||
private readonly ?string $password = null, | ||
private readonly ?int $port = null, | ||
private readonly array $query = [], | ||
) { | ||
// Deal with some exceptions. | ||
$matches = []; | ||
if (\preg_match('@^([^-_+]+)[-_+](.+)$@i', $vendor, $matches)) { | ||
$this->driver = $matches[1]; | ||
$this->vendor = Vendor::vendorNameNormalize($matches[2]); | ||
$this->scheme = $vendor; | ||
} else { | ||
$this->driver = $driver ?? self::DRIVER_ANY; | ||
$this->vendor = $vendor; | ||
$this->scheme = $driver . '-' . $vendor; | ||
} | ||
$this->isFile = $isFile || '/' === $host[0]; | ||
} | ||
|
||
public static function fromString(#[\SensitiveParameter] string $dsn): self | ||
{ | ||
$scheme = null; | ||
$isFile = false; | ||
|
||
// parse_url() disallow some schemes with special characters we need | ||
// such as using an underscore in it. Let's split the scheme first then | ||
// parse_url() the rest. | ||
// It also disallows triple slash for other than file:// scheme, but we | ||
// do allow them, for example sqlite:///foo.db, or for connections via | ||
// a UNIX socket. | ||
$matches = []; | ||
if (\preg_match('@^([^:/]+)://(.*)$@i', $dsn, $matches)) { | ||
$scheme = $matches[1]; | ||
if ($isFile = ('/' === $matches[2][0])) { | ||
$dsn = 'file://' . $matches[2]; | ||
} else { | ||
$dsn = 'mock://' . $matches[2]; | ||
} | ||
} else { | ||
throw new \InvalidArgumentException('The database DSN must contain a scheme.'); | ||
} | ||
|
||
if (false === ($params = \parse_url($dsn))) { | ||
throw new \InvalidArgumentException('The database DSN is invalid.'); | ||
} | ||
|
||
if ($isFile) { | ||
if (empty($params['path'])) { | ||
throw new \InvalidArgumentException('The database DSN must contain a path when a targetting a local filename.'); | ||
} | ||
$host = $params['path']; | ||
$database = $params['path']; | ||
} else { | ||
if (empty($params['host'])) { | ||
throw new \InvalidArgumentException('The database DSN must contain a host (use "default" by default).'); | ||
} | ||
if (empty($params['path'])) { | ||
throw new \InvalidArgumentException('The database DSN must contain a database name.'); | ||
} | ||
$host = $params['host']; | ||
$database = \trim($params['path'], '/'); | ||
} | ||
|
||
$port = $params['port'] ?? null; | ||
$user = '' !== ($params['user'] ?? '') ? \rawurldecode($params['user']) : null; | ||
$password = '' !== ($params['pass'] ?? '') ? \rawurldecode($params['pass']) : null; | ||
|
||
$query = []; | ||
\parse_str($params['query'] ?? '', $query); | ||
|
||
return new self( | ||
database: $database, | ||
host: $host, | ||
isFile: $isFile, | ||
password: $password, | ||
port: $port, | ||
query: $query, | ||
user: $user, | ||
vendor: $scheme, | ||
); | ||
} | ||
|
||
public function getDriver(): string | ||
{ | ||
return $this->driver; | ||
} | ||
|
||
public function getVendor(): string | ||
{ | ||
return $this->vendor; | ||
} | ||
|
||
public function getHost(): string | ||
{ | ||
return $this->host; | ||
} | ||
|
||
public function isFile(): bool | ||
{ | ||
return $this->isFile; | ||
} | ||
|
||
public function getDatabase(): ?string | ||
{ | ||
return $this->database; | ||
} | ||
|
||
public function getUser(): ?string | ||
{ | ||
return $this->user; | ||
} | ||
|
||
public function getPassword(): ?string | ||
{ | ||
return $this->password; | ||
} | ||
|
||
public function getPort(?int $default = null): ?int | ||
{ | ||
return $this->port ?? $default; | ||
} | ||
|
||
public function getOption(string $key, mixed $default = null): mixed | ||
{ | ||
return $this->query[$key] ?? $default; | ||
} | ||
|
||
public function toUrl(array $excludeParams = []): string | ||
{ | ||
$database = $this->database ?? ''; | ||
|
||
$queryString = ''; | ||
if ($this->query && $values = \array_diff_key($this->query, \array_flip($excludeParams))) { | ||
$queryString = (\str_contains($database, '?') ? '&' : '?') . \http_build_query($values); | ||
} | ||
|
||
$authString = $this->user ? (\rawurlencode($this->user) . ($this->password ? ':' . \rawurlencode($this->password) : '') . '@') : ''; | ||
|
||
return $this->scheme . '://' . $authString . $this->host . ($this->port ? ':' . $this->port : '') . '/' . \ltrim($database, '/') . $queryString; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
<?php | ||
|
||
declare (strict_types=1); | ||
|
||
namespace MakinaCorpus\QueryBuilder\Tests; | ||
|
||
use MakinaCorpus\QueryBuilder\Dsn; | ||
use PHPUnit\Framework\TestCase; | ||
|
||
class DsnTest extends TestCase | ||
{ | ||
public function testBasics(): void | ||
{ | ||
$dsn = Dsn::fromString('pdo_mysql://foo:[email protected]:1234/some_database?server=mysql-10.0.0'); | ||
|
||
self::assertFalse($dsn->isFile()); | ||
self::assertSame('pdo', $dsn->getDriver()); | ||
self::assertSame('mysql', $dsn->getVendor()); | ||
self::assertSame('foo', $dsn->getUser()); | ||
self::assertSame('bar', $dsn->getPassword()); | ||
self::assertSame(1234, $dsn->getPort()); | ||
self::assertSame('somehost.com', $dsn->getHost()); | ||
self::assertSame('some_database', $dsn->getDatabase()); | ||
self::assertSame('mysql-10.0.0', $dsn->getOption('server')); | ||
self::assertNull($dsn->getOption('non_existing_option')); | ||
} | ||
|
||
public function testWithFilename(): void | ||
{ | ||
$dsn = Dsn::fromString('sqlite:///some/path.db?server=sqlite-3'); | ||
|
||
self::assertTrue($dsn->isFile()); | ||
self::assertSame('sqlite', $dsn->getVendor()); | ||
self::assertSame('any', $dsn->getDriver()); | ||
self::assertSame('sqlite-3', $dsn->getOption('server')); | ||
self::assertSame('/some/path.db', $dsn->getHost()); | ||
self::assertSame('/some/path.db', $dsn->getDatabase()); | ||
} | ||
|
||
public function testUserPassAreDecoded(): void | ||
{ | ||
$dsn = Dsn::fromString('pdo_mysql://some%20user:some%[email protected]:1234/some_database'); | ||
|
||
self::assertSame('some user', $dsn->getUser()); | ||
self::assertSame('some password', $dsn->getPassword()); | ||
} | ||
|
||
public function testNoDatabaseRaiseException(): void | ||
{ | ||
self::expectException(\InvalidArgumentException::class); | ||
Dsn::fromString('pdo_mysql://some%20user:some%[email protected]'); | ||
} | ||
|
||
public function testNoSchemeRaiseException(): void | ||
{ | ||
self::expectException(\InvalidArgumentException::class); | ||
Dsn::fromString('some%20user:some%[email protected]/bla'); | ||
} | ||
|
||
public function testNoHostRaiseException(): void | ||
{ | ||
self::expectException(\InvalidArgumentException::class); | ||
Dsn::fromString('oauth://some%20user:some%20password@?foo=bar'); | ||
} | ||
|
||
public function testToUrl(): void | ||
{ | ||
$dsn = Dsn::fromString('pdo_mysql://foo:[email protected]:1234/some_database?server=mysql-10.0.0&bla=bla'); | ||
|
||
self::assertSame('pdo_mysql://foo:[email protected]:1234/some_database?server=mysql-10.0.0', $dsn->toUrl(['bla'])); | ||
} | ||
} |