From c40f2647555082d259c1e195522d52d649b15630 Mon Sep 17 00:00:00 2001 From: flashwave Date: Wed, 10 Apr 2024 23:26:13 +0000 Subject: [PATCH] Added DSN documentation for Database and Cache backends. --- VERSION | 2 +- src/Cache/ArrayCache/ArrayCacheBackend.php | 5 ++ src/Cache/CacheTools.php | 48 +++++++++++++++-- src/Cache/Memcached/MemcachedBackend.php | 30 +++++++++++ src/Cache/Valkey/ValkeyBackend.php | 19 +++++++ src/Data/DbTools.php | 42 +++++++++++++++ src/Data/MariaDB/MariaDBBackend.php | 62 +++++++++++++--------- src/Data/NullDb/NullDbBackend.php | 7 ++- src/Data/SQLite/SQLiteBackend.php | 30 +++++++---- tests/CacheToolsTest.php | 4 +- tests/DbToolsTest.php | 14 ++--- 11 files changed, 214 insertions(+), 49 deletions(-) diff --git a/VERSION b/VERSION index d6d3167..60d83af 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.2404.102223 +0.2404.102325 diff --git a/src/Cache/ArrayCache/ArrayCacheBackend.php b/src/Cache/ArrayCache/ArrayCacheBackend.php index b9f8086..a772223 100644 --- a/src/Cache/ArrayCache/ArrayCacheBackend.php +++ b/src/Cache/ArrayCache/ArrayCacheBackend.php @@ -30,6 +30,11 @@ class ArrayCacheBackend implements ICacheBackend { } /** + * Constructs a cache info instance from a dsn. + * + * ArrayCache has no parameters that can be controlled using the DSN. + * + * @param string|array $dsn DSN with provider information. * @return ArrayCacheProviderInfo Dummy provider info instance. */ public function parseDsn(string|array $dsn): ICacheProviderInfo { diff --git a/src/Cache/CacheTools.php b/src/Cache/CacheTools.php index 2147fea..b403d35 100644 --- a/src/Cache/CacheTools.php +++ b/src/Cache/CacheTools.php @@ -10,16 +10,28 @@ use RuntimeException; /** * Common cache actions. + * + * DSN documentation: + * + * CacheTools only handles the scheme part of the URL, + * the rest of the URL is described in the documentation of the parseDsn implementation of the respective backend. + * + * The scheme can be a PHP classpath to an implementation of ICacheBackend, or any of these aliases: + * - `array`, `null`: maps to `ArrayCache\ArrayCacheBackend` or `Index-Cache-ArrayCache-ArrayCacheBackend`, uses a simple array backed cache that doesn't get saved. + * - `memcached`, `memcache`: maps to `Memcached\MemcachedBackend` or `Index-Cache-Memcached-MemcachedBackend`, provides a backend based on the memcached or memcache extension. + * - `valkey`, `keydb`, `redis`: maps to `Valkey\ValkeyBackend` or `Index-Cache-Valkey-ValkeyBackend`, provides a backend based on the phpredis extension. + * + * Short names are currently hardcoded and cannot be expanded. */ final class CacheTools { private const CACHE_PROTOS = [ - 'null' => ArrayCache\ArrayCacheBackend::class, 'array' => ArrayCache\ArrayCacheBackend::class, - 'memcache' => Memcached\MemcachedBackend::class, + 'null' => ArrayCache\ArrayCacheBackend::class, 'memcached' => Memcached\MemcachedBackend::class, + 'memcache' => Memcached\MemcachedBackend::class, + 'valkey' => Valkey\ValkeyBackend::class, 'redis' => Valkey\ValkeyBackend::class, 'keydb' => Valkey\ValkeyBackend::class, - 'valkey' => Valkey\ValkeyBackend::class, ]; private static function parseDsnUri(string $dsn): array { @@ -60,15 +72,45 @@ final class CacheTools { return $backend; } + /** + * Retrieves cache provider based on DSN URL. + * + * Format of the DSN URLs are described in the documentation of the CacheTools class as a whole. + * + * @param string $dsn URL to create cache provider from. + * @throws InvalidArgumentException if $dsn is not a valid URL. + * @throws RuntimeException if no cache provider can be made using the URL. + * @return ICacheBackend Cache backend instance. + */ public static function backend(string $dsn): ICacheBackend { return self::resolveBackend(self::parseDsnUri($dsn)); } + /** + * Parses a DSN URL. + * + * Format of the DSN URLs are described in the documentation of the CacheTools class as a whole. + * + * @param string $dsn URL to create cache provider from. + * @throws InvalidArgumentException if $dsn is not a valid URL. + * @throws RuntimeException if no cache provider can be made using the URL. + * @return ICacheProviderInfo Cache provider info. + */ public static function parse(string $dsn): ICacheProviderInfo { $uri = self::parseDsnUri($dsn); return self::resolveBackend($uri)->parseDsn($uri); } + /** + * Uses a DSN URL to create a cache provider instance. + * + * Format of the DSN URLs are described in the documentation of the CacheTools class as a whole. + * + * @param string $dsn URL to create cache provider from. + * @throws InvalidArgumentException if $dsn is not a valid URL. + * @throws RuntimeException if no cache provider can be made using the URL. + * @return ICacheProvider A cache provider. + */ public static function create(string $dsn): ICacheProvider { $uri = self::parseDsnUri($dsn); $backend = self::resolveBackend($uri); diff --git a/src/Cache/Memcached/MemcachedBackend.php b/src/Cache/Memcached/MemcachedBackend.php index 4e17516..27cbd4b 100644 --- a/src/Cache/Memcached/MemcachedBackend.php +++ b/src/Cache/Memcached/MemcachedBackend.php @@ -42,6 +42,36 @@ class MemcachedBackend implements ICacheBackend { } /** + * Constructs a cache info instance from a dsn. + * + * Memcached does not support a username or password. + * + * The host part of the URL can be any DNS name, or special values `:unix:` and `:pool:`, documented further down. + * + * The path part of the URL is used as a key prefix. Any prefix slashes (`/`) are trimmed and others are converted to a colon (`:`). + * Meaning `/prefix/test/` is converted to `prefix:test:`. + * + * In order to use a Unix socket path, set the host part to `:unix:` and specify `server=/path/to/socket.sock` in the query. + * + * In order to use a pool of connections, set the host part to `:pool:` and specify any amount of params `server[]` in the query. + * Weights can be specified at the end, prefixed by a semicolon (`;`) + * Examples include: + * - `server[]=/var/run/memcached.sock;20` + * - `server[]=127.0.0.1;10` + * - `server[]=[::1]:9001;5` + * - `server[]=localhost` + * + * Internally `:unix:` and `:pool:` invoke the same behaviour, but please use them appropriately. + * + * Other query fields include: + * - `persist=`: a named persistent connection. Named only applied to the memcached implementation, the legacy memcache implementation treats this as a boolean flag. + * - `proto=ascii`: Forces the memcached implemenation to use the ASCII protocol over the binary one. The legacy memcache implementation always uses the ASCII protocol. + * - `compress=`: Turns compression of strings larger than 100 characters off. + * - `nodelay=`: Turns TCP No Delay off. Has no effect on the legacy memcache implementation. + * + * Why is the legacy memcache extension supported? cuz I felt like it. + * + * @param string|array $dsn DSN with provider information. * @return MemcachedProviderInfo Memcached provider info instance. */ public function parseDsn(string|array $dsn): ICacheProviderInfo { diff --git a/src/Cache/Valkey/ValkeyBackend.php b/src/Cache/Valkey/ValkeyBackend.php index 7284ddb..8fcd177 100644 --- a/src/Cache/Valkey/ValkeyBackend.php +++ b/src/Cache/Valkey/ValkeyBackend.php @@ -34,6 +34,25 @@ class ValkeyBackend implements ICacheBackend { } /** + * Constructs a cache info instance from a dsn. + * + * The Valkey backend supports setting a username and password in the URL. + * `//username:password@` is treated as a username and password. + * `//username@` is treated as just a password despite what is normally expected of a URL. Older versions of Redis did not have the concept of usernames. + * In order to still use a username but no password for some reason you can specify `//username:@`, just inserting a colon without anything after it. + * + * The host part of the URL can be any DNS name, or special value `:unix:`, documented further down. + * + * The path part of the URL is used as a key prefix. Any prefix slashes (`/`) are trimmed and others are converted to a colon (`:`). + * Meaning `/prefix/test/` is converted to `prefix:test:`. + * + * In order to use a Unix socket path, set the host part to `:unix:` and specify `socket=/path/to/socket.sock` in the query. + * + * Other query fields include: + * - `db=`: Allows you to select a different database. + * - `persist`: Uses a persistent connection. + * + * @param string|array $dsn DSN with provider information. * @return ValkeyProviderInfo Valkey provider info instance. */ public function parseDsn(string|array $dsn): ICacheProviderInfo { diff --git a/src/Data/DbTools.php b/src/Data/DbTools.php index eb44492..7fce2f9 100644 --- a/src/Data/DbTools.php +++ b/src/Data/DbTools.php @@ -10,6 +10,18 @@ use InvalidArgumentException; /** * Common database actions. + * + * DSN documentation: + * + * DbTools only handles the scheme part of the URL, + * the rest of the URL is described in the documentation of the parseDsn implementation of the respective backend. + * + * The scheme can be a PHP classpath to an implementation of IDbBackend, or any of these aliases: + * - `null`: maps to `NullDb\NullDbBackend` or `Index-Data-NullDb-NullDbBackend`, provides a fallback blackhole database backend. + * - `mariadb`, `mysql`: maps to `MariaDB\MariaDBBackend` or `Index-Data-MariaDB-MariaDBBackend`, provides a backend based on the mysqli extension. + * - `sqlite`, `sqlite3`: maps to `SQLite\SQLiteBackend` or `Index-Data-SQLite-SQLiteBackend`, provides a backend based on the sqlite3 extension. + * + * Short names are currently hardcoded and cannot be expanded. */ final class DbTools { private const DB_PROTOS = [ @@ -58,15 +70,45 @@ final class DbTools { return $backend; } + /** + * Retrieves database backend based on DSN URL. + * + * Format of the DSN URLs are described in the documentation of the DbTools class as a whole. + * + * @param string $dsn URL to create database connection from. + * @throws InvalidArgumentException if $dsn is not a valid URL. + * @throws DataException if no database connection can be made using the URL. + * @return IDbBackend Database backend instance. + */ public static function backend(string $dsn): IDbBackend { return self::resolveBackend(self::parseDsnUri($dsn)); } + /** + * Parses a DSN URL. + * + * Format of the DSN URLs are described in the documentation of the DbTools class as a whole. + * + * @param string $dsn URL to create database connection from. + * @throws InvalidArgumentException if $dsn is not a valid URL. + * @throws DataException if no database connection can be made using the URL. + * @return IDbConnectionInfo Database connection info. + */ public static function parse(string $dsn): IDbConnectionInfo { $uri = self::parseDsnUri($dsn); return self::resolveBackend($uri)->parseDsn($uri); } + /** + * Uses a DSN URL to create a database instance. + * + * Format of the DSN URLs are described in the documentation of the DbTools class as a whole. + * + * @param string $dsn URL to create database connection from. + * @throws InvalidArgumentException if $dsn is not a valid URL. + * @throws DataException if no database connection can be made using the URL. + * @return IDbConnection An active database connection. + */ public static function create(string $dsn): IDbConnection { $uri = self::parseDsnUri($dsn); $backend = self::resolveBackend($uri); diff --git a/src/Data/MariaDB/MariaDBBackend.php b/src/Data/MariaDB/MariaDBBackend.php index cbe3501..8a5a358 100644 --- a/src/Data/MariaDB/MariaDBBackend.php +++ b/src/Data/MariaDB/MariaDBBackend.php @@ -1,7 +1,7 @@ `: Specifies the character set to use for the connection. + * - `init=`: Any arbitrary SQL command to execute open connecting. + * - `enc_key=`: Path to a private key file for SSL. + * - `enc_cert=`: Path to a certificate file for SSL. + * - `enc_authority=`: Path to a certificate authority file for SSL. + * - `enc_trusted_certs=`: Path to a directory that contains PEM formatted SSL CA certificates. + * - `enc_ciphers=`: A list of allowable ciphers to use for SSL encryption. + * - `enc_no_verify`: Disables verification of the server certificate, allows for MITM attacks. + * - `compress`: Enables protocol compression. + * + * Previously supported query parameters: + * - `enc_verify=`: Enabled verification of server certificate. Replaced with `enc_no_verify` as it now defaults to on. + * + * @param string|array $dsn DSN with connection information. * @return MariaDBConnectionInfo MariaDB connection info. */ public function parseDsn(string|array $dsn): IDbConnectionInfo { @@ -81,30 +107,18 @@ class MariaDBBackend implements IDbBackend { $endPoint = $needsUnix ? null : EndPoint::parse($host); $dbName = str_replace('/', '_', trim($dsn['path'], '/')); // cute for table prefixes i think - if(!isset($dsn['query'])) { - $charSet = null; - $initCommand = null; - $keyPath = null; - $certPath = null; - $certAuthPath = null; - $trustedCertsPath = null; - $cipherAlgos = null; - $verifyCert = false; - $useCompression = false; - } else { - parse_str(str_replace('+', '%2B', $dsn['query']), $query); + parse_str(str_replace('+', '%2B', $dsn['query'] ?? ''), $query); - $unixPath = $query['socket'] ?? null; - $charSet = $query['charset'] ?? null; - $initCommand = $query['init'] ?? null; - $keyPath = $query['enc_key'] ?? null; - $certPath = $query['enc_cert'] ?? null; - $certAuthPath = $query['enc_authority'] ?? null; - $trustedCertsPath = $query['enc_trusted_certs'] ?? null; - $cipherAlgos = $query['enc_ciphers'] ?? null; - $verifyCert = !empty($query['enc_verify']); - $useCompression = !empty($query['compress']); - } + $unixPath = $query['socket'] ?? null; + $charSet = $query['charset'] ?? null; + $initCommand = $query['init'] ?? null; + $keyPath = $query['enc_key'] ?? null; + $certPath = $query['enc_cert'] ?? null; + $certAuthPath = $query['enc_authority'] ?? null; + $trustedCertsPath = $query['enc_trusted_certs'] ?? null; + $cipherAlgos = $query['enc_ciphers'] ?? null; + $verifyCert = !isset($query['enc_no_verify']); + $useCompression = isset($query['compress']); if($needsUnix) { if(empty($unixPath)) diff --git a/src/Data/NullDb/NullDbBackend.php b/src/Data/NullDb/NullDbBackend.php index 20106c1..596d1fd 100644 --- a/src/Data/NullDb/NullDbBackend.php +++ b/src/Data/NullDb/NullDbBackend.php @@ -1,7 +1,7 @@ ` to specify an encryption key. + * - `readOnly` to open a database in read-only mode. + * - `openOnly` to prevent a new file from being created if the specified path does not exist. + * + * @param string|array $dsn DSN with connection information. * @return SQLiteConnectionInfo SQLite connection info. */ public function parseDsn(string|array $dsn): IDbConnectionInfo { @@ -52,22 +64,18 @@ class SQLiteBackend implements IDbBackend { throw new InvalidArgumentException('$dsn is not a valid uri.'); } + $encKey = ''; + $path = $dsn['path'] ?? ''; if($path === 'memory:') $path = ':memory:'; - if(!isset($dsn['query'])) { - $encKey = ''; - $readOnly = false; - $create = true; - } else { - parse_str(str_replace('+', '%2B', $dsn['query']), $query); + parse_str(str_replace('+', '%2B', $dsn['query'] ?? ''), $query); - $encKey = $query['key'] ?? ''; - $readOnly = !empty($query['readOnly']); - $create = empty($query['openOnly']); - } + $encKey = $query['key'] ?? ''; + $readOnly = isset($query['readOnly']); + $create = !isset($query['openOnly']); return new SQLiteConnectionInfo($path, $encKey, $readOnly, $create); } diff --git a/tests/CacheToolsTest.php b/tests/CacheToolsTest.php index c6fd720..65849bc 100644 --- a/tests/CacheToolsTest.php +++ b/tests/CacheToolsTest.php @@ -142,7 +142,7 @@ final class CacheToolsTest extends TestCase { $this->assertEquals(0, $info->getDatabaseNumber()); // ipv4 - $info = $valkey->parseDsn('valkey://password@127.0.0.1:6379?db=123'); + $info = $valkey->parseDsn('keydb://password@127.0.0.1:6379?db=123'); $this->assertInstanceOf(IPEndPoint::class, $info->getEndPoint()); $this->assertEquals('127.0.0.1', $info->getServerHost()); @@ -157,7 +157,7 @@ final class CacheToolsTest extends TestCase { $this->assertEquals(123, $info->getDatabaseNumber()); // ipv6 - $info = $valkey->parseDsn('valkey://username:password@[::1]/?persist&db=1'); + $info = $valkey->parseDsn('redis://username:password@[::1]/?persist&db=1'); $this->assertInstanceOf(IPEndPoint::class, $info->getEndPoint()); $this->assertEquals('::1', $info->getServerHost()); diff --git a/tests/DbToolsTest.php b/tests/DbToolsTest.php index 3a076c1..2ae1b10 100644 --- a/tests/DbToolsTest.php +++ b/tests/DbToolsTest.php @@ -1,7 +1,7 @@ assertNull($mci1->getCertificateAuthorityPath()); $this->assertNull($mci1->getTrustedCertificatesPath()); $this->assertNull($mci1->getCipherAlgorithms()); - $this->assertFalse($mci1->shouldVerifyCertificate()); + $this->assertTrue($mci1->shouldVerifyCertificate()); $this->assertFalse($mci1->shouldUseCompression()); // generic ipv4 @@ -79,7 +79,7 @@ final class DbToolsTest extends TestCase { $this->assertNull($mci2->getCertificateAuthorityPath()); $this->assertNull($mci2->getTrustedCertificatesPath()); $this->assertNull($mci2->getCipherAlgorithms()); - $this->assertFalse($mci2->shouldVerifyCertificate()); + $this->assertTrue($mci2->shouldVerifyCertificate()); $this->assertFalse($mci2->shouldUseCompression()); // generic ipv6 with port @@ -101,21 +101,21 @@ final class DbToolsTest extends TestCase { $this->assertNull($mci3->getCertificateAuthorityPath()); $this->assertNull($mci3->getTrustedCertificatesPath()); $this->assertNull($mci3->getCipherAlgorithms()); - $this->assertFalse($mci3->shouldVerifyCertificate()); + $this->assertTrue($mci3->shouldVerifyCertificate()); $this->assertFalse($mci3->shouldUseCompression()); // sqlite normal - $sql1 = $sqlite->parseDsn('sqlite:/path/to/database.db?key=ebwOKzGkLYIxDGXk&readOnly=0&openOnly=1'); + $sql1 = $sqlite->parseDsn('sqlite:/path/to/database.db?key=ebwOKzGkLYIxDGXk&openOnly'); $this->assertEquals('/path/to/database.db', $sql1->getFileName()); $this->assertEquals('ebwOKzGkLYIxDGXk', $sql1->getEncryptionKey()); $this->assertFalse($sql1->shouldReadOnly()); $this->assertFalse($sql1->shouldCreate()); // sqlite temp file - $sql1 = $sqlite->parseDsn('sqlite:?key=Y63vrCttK8bEdPUIXVCOiLQKIgdbUn8V'); + $sql1 = $sqlite->parseDsn('sqlite:?key=Y63vrCttK8bEdPUIXVCOiLQKIgdbUn8V&readOnly'); $this->assertEquals('', $sql1->getFileName()); $this->assertEquals('Y63vrCttK8bEdPUIXVCOiLQKIgdbUn8V', $sql1->getEncryptionKey()); - $this->assertFalse($sql1->shouldReadOnly()); + $this->assertTrue($sql1->shouldReadOnly()); $this->assertTrue($sql1->shouldCreate()); // sqlite memory