From 8443138680a30ddf680e41ba5c0a1becdbed3b56 Mon Sep 17 00:00:00 2001 From: Pierre Rineau Date: Mon, 22 Apr 2024 15:04:23 +0200 Subject: [PATCH] add (unused yet) Dsn class for databases --- src/Dsn.php | 174 ++++++++++++++++++++++++++++++++++++++++++++++ src/Platform.php | 2 +- src/Vendor.php | 2 +- tests/DsnTest.php | 72 +++++++++++++++++++ 4 files changed, 248 insertions(+), 2 deletions(-) create mode 100644 src/Dsn.php create mode 100644 tests/DsnTest.php diff --git a/src/Dsn.php b/src/Dsn.php new file mode 100644 index 0000000..2ba8e1b --- /dev/null +++ b/src/Dsn.php @@ -0,0 +1,174 @@ +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; + } +} diff --git a/src/Platform.php b/src/Platform.php index 347a38a..54238ba 100644 --- a/src/Platform.php +++ b/src/Platform.php @@ -13,7 +13,7 @@ final class Platform extends Vendor public const MARIADB = 'mariadb'; public const MYSQL = 'mysql'; public const ORACLE = 'oracle'; - public const POSTGRESQL = 'postgresql'; + public const POSTGRESQL = 'pgsql'; public const SQLITE = 'sqlite'; public const SQLSERVER = 'sqlsrv'; public const UNKNOWN = 'unknown'; diff --git a/src/Vendor.php b/src/Vendor.php index 018bef4..bb6853f 100644 --- a/src/Vendor.php +++ b/src/Vendor.php @@ -18,7 +18,7 @@ public const MARIADB = 'mariadb'; public const MYSQL = 'mysql'; public const ORACLE = 'oracle'; - public const POSTGRESQL = 'postgresql'; + public const POSTGRESQL = 'pgsql'; public const SQLITE = 'sqlite'; public const SQLSERVER = 'sqlsrv'; public const UNKNOWN = 'unknown'; diff --git a/tests/DsnTest.php b/tests/DsnTest.php new file mode 100644 index 0000000..fc6ca44 --- /dev/null +++ b/tests/DsnTest.php @@ -0,0 +1,72 @@ +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%20password@somehost.com: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%20password@thirdpartyprovider.com'); + } + + public function testNoSchemeRaiseException(): void + { + self::expectException(\InvalidArgumentException::class); + Dsn::fromString('some%20user:some%20password@thirdpartyprovider.com/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:bar@somehost.com:1234/some_database?server=mysql-10.0.0&bla=bla'); + + self::assertSame('pdo_mysql://foo:bar@somehost.com:1234/some_database?server=mysql-10.0.0', $dsn->toUrl(['bla'])); + } +}