From ad75d72aa0d0cf7d4f221d9a532448b424ec253c Mon Sep 17 00:00:00 2001 From: flashwave Date: Wed, 10 Apr 2024 22:23:34 +0000 Subject: [PATCH] Added cache wrappers. --- VERSION | 2 +- composer.lock | 12 +- src/Cache/ArrayCache/ArrayCacheBackend.php | 38 ++++ src/Cache/ArrayCache/ArrayCacheProvider.php | 75 +++++++ .../ArrayCache/ArrayCacheProviderInfo.php | 13 ++ src/Cache/CacheTools.php | 77 +++++++ src/Cache/ICacheBackend.php | 36 ++++ src/Cache/ICacheProvider.php | 70 +++++++ src/Cache/ICacheProviderInfo.php | 13 ++ src/Cache/Memcached/MemcachedBackend.php | 108 ++++++++++ src/Cache/Memcached/MemcachedProvider.php | 27 +++ src/Cache/Memcached/MemcachedProviderInfo.php | 86 ++++++++ .../Memcached/MemcachedProviderLegacy.php | 98 +++++++++ .../Memcached/MemcachedProviderModern.php | 118 +++++++++++ src/Cache/Valkey/ValkeyBackend.php | 86 ++++++++ src/Cache/Valkey/ValkeyProvider.php | 81 ++++++++ src/Cache/Valkey/ValkeyProviderInfo.php | 135 +++++++++++++ src/Data/DbTools.php | 3 +- tests/CacheToolsTest.php | 189 ++++++++++++++++++ 19 files changed, 1258 insertions(+), 9 deletions(-) create mode 100644 src/Cache/ArrayCache/ArrayCacheBackend.php create mode 100644 src/Cache/ArrayCache/ArrayCacheProvider.php create mode 100644 src/Cache/ArrayCache/ArrayCacheProviderInfo.php create mode 100644 src/Cache/CacheTools.php create mode 100644 src/Cache/ICacheBackend.php create mode 100644 src/Cache/ICacheProvider.php create mode 100644 src/Cache/ICacheProviderInfo.php create mode 100644 src/Cache/Memcached/MemcachedBackend.php create mode 100644 src/Cache/Memcached/MemcachedProvider.php create mode 100644 src/Cache/Memcached/MemcachedProviderInfo.php create mode 100644 src/Cache/Memcached/MemcachedProviderLegacy.php create mode 100644 src/Cache/Memcached/MemcachedProviderModern.php create mode 100644 src/Cache/Valkey/ValkeyBackend.php create mode 100644 src/Cache/Valkey/ValkeyProvider.php create mode 100644 src/Cache/Valkey/ValkeyProviderInfo.php create mode 100644 tests/CacheToolsTest.php diff --git a/VERSION b/VERSION index b94e91a..d6d3167 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.2404.21726 +0.2404.102223 diff --git a/composer.lock b/composer.lock index 70c38e6..4c38ad9 100644 --- a/composer.lock +++ b/composer.lock @@ -627,16 +627,16 @@ }, { "name": "phpunit/phpunit", - "version": "10.5.16", + "version": "10.5.17", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "18f8d4a5f52b61fdd9370aaae3167daa0eeb69cd" + "reference": "c1f736a473d21957ead7e94fcc029f571895abf5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/18f8d4a5f52b61fdd9370aaae3167daa0eeb69cd", - "reference": "18f8d4a5f52b61fdd9370aaae3167daa0eeb69cd", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/c1f736a473d21957ead7e94fcc029f571895abf5", + "reference": "c1f736a473d21957ead7e94fcc029f571895abf5", "shasum": "" }, "require": { @@ -708,7 +708,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.16" + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.17" }, "funding": [ { @@ -724,7 +724,7 @@ "type": "tidelift" } ], - "time": "2024-03-28T10:08:10+00:00" + "time": "2024-04-05T04:39:01+00:00" }, { "name": "sebastian/cli-parser", diff --git a/src/Cache/ArrayCache/ArrayCacheBackend.php b/src/Cache/ArrayCache/ArrayCacheBackend.php new file mode 100644 index 0000000..b9f8086 --- /dev/null +++ b/src/Cache/ArrayCache/ArrayCacheBackend.php @@ -0,0 +1,38 @@ +items)) + return null; + + $item = $this->items[$key]; + if($item['ttl'] > 0 && $item['ttl'] <= time()) { + unset($this->items[$key]); + return null; + } + + return unserialize($item['value']); + } + + public function set(string $key, mixed $value, int $ttl = 0): void { + if($ttl < 0) + throw new InvalidArgumentException('$ttl must be equal to or greater than 0'); + + $this->items[$key] = [ + 'value' => serialize($value), + 'ttl' => $ttl < 1 ? 0 : time() + $ttl, + ]; + } + + public function delete(string $key): void { + if(array_key_exists($key, $this->items)) + unset($this->items[$key]); + } + + public function touch(string $key, int $ttl = 0): void { + if($ttl < 0) + throw new InvalidArgumentException('$ttl must be equal to or greater than 0'); + + if(array_key_exists($key, $this->items)) + $this->items[$key]['ttl'] = $ttl < 1 ? 0 : time() + $ttl; + } + + public function increment(string $key, int $amount = 1): int { + $exists = array_key_exists($key, $this->items); + $value = $exists ? unserialize($this->items[$key]['value']) : 0; + $value += $amount; + $serialised = serialize($value); + + if($exists) + $this->items[$key]['value'] = $serialised; + else + $this->items[$key] = [ + 'value' => $serialised, + 'ttl' => 0, + ]; + + return $value; + } + + public function decrement(string $key, int $amount = 1): int { + return $this->increment($key, $amount * -1); + } + + public function close(): void {} +} diff --git a/src/Cache/ArrayCache/ArrayCacheProviderInfo.php b/src/Cache/ArrayCache/ArrayCacheProviderInfo.php new file mode 100644 index 0000000..3af897c --- /dev/null +++ b/src/Cache/ArrayCache/ArrayCacheProviderInfo.php @@ -0,0 +1,13 @@ + ArrayCache\ArrayCacheBackend::class, + 'array' => ArrayCache\ArrayCacheBackend::class, + 'memcache' => Memcached\MemcachedBackend::class, + 'memcached' => Memcached\MemcachedBackend::class, + 'redis' => Valkey\ValkeyBackend::class, + 'keydb' => Valkey\ValkeyBackend::class, + 'valkey' => Valkey\ValkeyBackend::class, + ]; + + private static function parseDsnUri(string $dsn): array { + $uri = parse_url($dsn); + if($uri === false) + throw new InvalidArgumentException('$dsn is not a valid uri.'); + return $uri; + } + + private static function resolveBackend(array $uri): ICacheBackend { + static $backends = []; + + $scheme = $uri['scheme']; + + if(in_array($scheme, $backends)) + $backend = $backends[$scheme]; + else { + $backend = null; + + if(array_key_exists($scheme, self::CACHE_PROTOS)) + $name = self::CACHE_PROTOS[$scheme]; + else + $name = str_replace('-', '\\', $scheme); + + if(class_exists($name) && is_subclass_of($name, ICacheBackend::class)) { + $backend = new $name; + $name = get_class($backend); + } + + if($backend === null) + throw new RuntimeException('No implementation is available for the specified scheme.'); + if(!$backend->isAvailable()) + throw new RuntimeException('Requested cache backend is not available, likely due to missing dependencies.'); + + $backends[$name] = $backend; + } + + return $backend; + } + + public static function backend(string $dsn): ICacheBackend { + return self::resolveBackend(self::parseDsnUri($dsn)); + } + + public static function parse(string $dsn): ICacheProviderInfo { + $uri = self::parseDsnUri($dsn); + return self::resolveBackend($uri)->parseDsn($uri); + } + + public static function create(string $dsn): ICacheProvider { + $uri = self::parseDsnUri($dsn); + $backend = self::resolveBackend($uri); + return $backend->createProvider($backend->parseDsn($uri)); + } +} diff --git a/src/Cache/ICacheBackend.php b/src/Cache/ICacheBackend.php new file mode 100644 index 0000000..ed775b6 --- /dev/null +++ b/src/Cache/ICacheBackend.php @@ -0,0 +1,36 @@ + 1 ? (int)$parts[1] : 0; + $endPoint = EndPoint::parse($parts[0]); + $endPoints[] = [$endPoint, $weight]; + } + } + + $persistName = isset($query['persist']) && is_string($query['persist']) ? $query['persist'] : ''; + $useBinaryProto = empty($query['proto']) || $query['proto'] !== 'ascii'; + + if(empty($query['compress']) || !is_string($query['compress'])) + $enableCompression = true; + else { + $enableCompression = strtolower($query['compress']); + $enableCompression = $enableCompression !== 'no' && $enableCompression !== 'off' && $enableCompression !== 'false' && $enableCompression !== '0'; + } + + if(empty($query['nodelay']) || !is_string($query['nodelay'])) + $tcpNoDelay = true; + else { + $tcpNoDelay = strtolower($query['nodelay']); + $tcpNoDelay = $tcpNoDelay !== 'no' && $tcpNoDelay !== 'off' && $tcpNoDelay !== 'false' && $tcpNoDelay !== '0'; + } + + if(empty($endPoints)) + throw new InvalidArgumentException('No servers are specified in the DSN.'); + + return new MemcachedProviderInfo($endPoints, $prefixKey, $persistName, $useBinaryProto, $enableCompression, $tcpNoDelay); + } +} diff --git a/src/Cache/Memcached/MemcachedProvider.php b/src/Cache/Memcached/MemcachedProvider.php new file mode 100644 index 0000000..6bef224 --- /dev/null +++ b/src/Cache/Memcached/MemcachedProvider.php @@ -0,0 +1,27 @@ +close(); + } +} diff --git a/src/Cache/Memcached/MemcachedProviderInfo.php b/src/Cache/Memcached/MemcachedProviderInfo.php new file mode 100644 index 0000000..47d027d --- /dev/null +++ b/src/Cache/Memcached/MemcachedProviderInfo.php @@ -0,0 +1,86 @@ +endPoints; + } + + /** + * A prefix that gets applied to every key. + * + * @return string + */ + public function getPrefixKey(): string { + return $this->prefixKey; + } + + /** + * Whether the connection should be persistent. + * + * @return bool + */ + public function isPersistent(): bool { + return $this->persistName !== ''; + } + + /** + * Name of persistent connection, will return null if disabled. + * + * @return ?string + */ + public function getPersistentName(): ?string { + return $this->persistName === '' ? null : $this->persistName; + } + + /** + * Whether the binary protocol should be used rather than ASCII. + * + * @return bool + */ + public function shouldUseBinaryProtocol(): bool { + return $this->useBinaryProto; + } + + /** + * Whether compression should be applied. + * + * @return bool + */ + public function shouldEnableCompression(): bool { + return $this->enableCompression; + } + + /** + * Whether nagle's algorithm should be disabled on the connection. + * + * @return bool + */ + public function shouldTcpNoDelay(): bool { + return $this->tcpNoDelay; + } +} diff --git a/src/Cache/Memcached/MemcachedProviderLegacy.php b/src/Cache/Memcached/MemcachedProviderLegacy.php new file mode 100644 index 0000000..df248c7 --- /dev/null +++ b/src/Cache/Memcached/MemcachedProviderLegacy.php @@ -0,0 +1,98 @@ +prefix = $providerInfo->getPrefixKey(); + $this->compress = $providerInfo->shouldEnableCompression(); + $this->persistent = $providerInfo->isPersistent(); + $this->memcache = new Memcache; + + foreach($providerInfo->getEndPoints() as $endPointInfo) { + if($endPointInfo[0] instanceof UnixEndPoint) { + $host = 'unix://' . $endPointInfo->getPath(); + $port = 0; + } elseif($endPointInfo[0] instanceof DnsEndPoint) { + $host = $endPointInfo->getHost(); + $port = $endPointInfo->getPort(); + } elseif($endPointInfo[0] instanceof IPEndPoint) { + $host = $endPointInfo->getAddress()->getCleanAddress(); + $port = $endPointInfo->getPort(); + } else throw new InvalidArgumentException('One of the servers specified in $providerInfo is not a supported endpoint.'); + + $this->memcache->addServer($host, $port, $this->persistent, $endPointInfo[1]); + } + } + + public function get(string $key): mixed { + $value = $this->memcache->get($this->prefix . $key); + return $value === false ? null : unserialize($value); + } + + public function set(string $key, mixed $value, int $ttl = 0): void { + if($ttl < 0) + throw new InvalidArgumentException('$ttl must be equal to or greater than 0.'); + if($ttl > MemcachedProvider::MAX_TTL) + throw new InvalidArgumentException('$ttl may not be greater than 30 days.'); + if($value === null) + return; + + $serialised = serialize($value); + $flags = 0; + if($this->compress && strlen($serialised) > 100) + $flags |= MEMCACHE_COMPRESSED; + + $this->memcache->set($this->prefix . $key, $serialised, $flags, $ttl); + } + + public function delete(string $key): void { + $this->memcache->delete($this->prefix . $key); + } + + public function touch(string $key, int $ttl = 0): void { + $this->set($key, $this->get($key), $ttl); + } + + public function increment(string $key, int $amount = 1): int { + $key = $this->prefix . $key; + $result = $this->memcache->increment($key, $amount); + if($result === false) { // @phpstan-ignore-line: PHP documentation states increment returns int|false + $result = $amount; + $this->memcache->set($key, serialize($result), 0); + } + + return $result; + } + + public function decrement(string $key, int $amount = 1): int { + $key = $this->prefix . $key; + $result = $this->memcache->decrement($key, $amount); + if($result === false) { // @phpstan-ignore-line: PHP documentation states decrement returns int|false + $result = $amount * -1; + $this->memcache->set($key, serialize($result), 0); + } + + return $result; + } + + public function close(): void { + if(!$this->persistent) + $this->memcache->close(); + } +} diff --git a/src/Cache/Memcached/MemcachedProviderModern.php b/src/Cache/Memcached/MemcachedProviderModern.php new file mode 100644 index 0000000..d2a9d59 --- /dev/null +++ b/src/Cache/Memcached/MemcachedProviderModern.php @@ -0,0 +1,118 @@ +memcached = new Memcached($providerInfo->getPersistentName()); + $this->memcached->setOption(Memcached::OPT_BINARY_PROTOCOL, $providerInfo->shouldUseBinaryProtocol()); + $this->memcached->setOption(Memcached::OPT_COMPRESSION, $providerInfo->shouldEnableCompression()); + $this->memcached->setOption(Memcached::OPT_TCP_NODELAY, $providerInfo->shouldTcpNoDelay()); + $this->memcached->setOption(Memcached::OPT_PREFIX_KEY, $providerInfo->getPrefixKey()); + + foreach($providerInfo->getEndPoints() as $endPointInfo) { + if($endPointInfo[0] instanceof UnixEndPoint) { + $host = $endPointInfo->getPath(); + $port = 0; + } elseif($endPointInfo[0] instanceof DnsEndPoint) { + $host = $endPointInfo->getHost(); + $port = $endPointInfo->getPort(); + } elseif($endPointInfo[0] instanceof IPEndPoint) { + $host = $endPointInfo->getAddress()->getCleanAddress(); + $port = $endPointInfo->getPort(); + } else throw new InvalidArgumentException('One of the servers specified in $providerInfo is not a supported endpoint.'); + + $this->memcached->addServer($host, $port, $endPointInfo[1]); + } + } + + public function get(string $key): mixed { + $result = $this->memcached->get($key); + + if($result === false) { + $result = $this->memcached->getResultCode(); + if($result === Memcached::RES_NOTFOUND) + return null; + + throw new RuntimeException("[Memcached] Error during get: {$result}", $result); + } + + return $result; + } + + public function set(string $key, mixed $value, int $ttl = 0): void { + if($ttl < 0) + throw new InvalidArgumentException('$ttl must be equal to or greater than 0.'); + if($ttl > MemcachedProvider::MAX_TTL) + throw new InvalidArgumentException('$ttl may not be greater than 30 days.'); + + $result = $this->memcached->set($key, $value, $ttl); + + if(!$result) { + $result = $this->memcached->getResultCode(); + throw new RuntimeException("[Memcached] Error during set: {$result}", $result); + } + } + + public function delete(string $key): void { + $result = $this->memcached->delete($key); + + if(!$result) { + $result = $this->memcached->getResultCode(); + if($result !== Memcached::RES_NOTFOUND) + throw new RuntimeException("[Memcached] Error during delete: {$result}", $result); + } + } + + public function touch(string $key, int $ttl = 0): void { + if($ttl < 0) + throw new InvalidArgumentException('$ttl must be equal to or greater than 0.'); + if($ttl > MemcachedProvider::MAX_TTL) + throw new InvalidArgumentException('$ttl may not be greater than 30 days.'); + + $result = $this->memcached->touch($key, $ttl); + + if(!$result) { + $result = $this->memcached->getResultCode(); + throw new RuntimeException("[Memcached] Error during set: {$result}", $result); + } + } + + public function increment(string $key, int $amount = 1): int { + $result = $this->memcached->increment($key, $amount); + if($result === false) { + $result = $this->memcached->getResultCode(); + throw new RuntimeException("[Memcached] Error during increment: {$result}", $result); + } + + return $result; + } + + public function decrement(string $key, int $amount = 1): int { + $result = $this->memcached->decrement($key, $amount); + if($result === false) { + $result = $this->memcached->getResultCode(); + throw new RuntimeException("[Memcached] Error during decrement: {$result}", $result); + } + + return $result; + } + + public function close(): void { + if(!$this->memcached->isPersistent()) + $this->memcached->quit(); + } +} diff --git a/src/Cache/Valkey/ValkeyBackend.php b/src/Cache/Valkey/ValkeyBackend.php new file mode 100644 index 0000000..7284ddb --- /dev/null +++ b/src/Cache/Valkey/ValkeyBackend.php @@ -0,0 +1,86 @@ +persist = $providerInfo->isPersistent(); + $this->redis = new Redis; + $this->redis->setOption(Redis::OPT_PREFIX, $providerInfo->getPrefix()); + + if($this->persist) + $this->redis->pconnect($providerInfo->getServerHost(), $providerInfo->getServerPort()); + else + $this->redis->connect($providerInfo->getServerHost(), $providerInfo->getServerPort()); + + if($providerInfo->hasPassword()) { + if($providerInfo->hasUsername()) + $this->redis->auth([$providerInfo->getUsername(), $providerInfo->getPassword()]); + else + $this->redis->auth($providerInfo->getPassword()); + } + + if($providerInfo->hasDatabaseNumber()) + $this->redis->select($providerInfo->getDatabaseNumber()); + } + + public function get(string $key): mixed { + $result = $this->redis->get($key); + return $result === false ? null : $result; + } + + public function set(string $key, mixed $value, int $ttl = 0): void { + if($ttl < 0) + throw new InvalidArgumentException('$ttl must be equal to or greater than 0.'); + + $this->redis->setEx($key, $ttl, $value); + } + + public function delete(string $key): void { + $this->redis->unlink($key); + } + + public function touch(string $key, int $ttl = 0): void { + if($ttl < 0) + throw new InvalidArgumentException('$ttl must be equal to or greater than 0.'); + + $this->redis->expire($key, $ttl); + } + + public function increment(string $key, int $amount = 1): int { + return $this->redis->incrBy($key, $amount); + } + + public function decrement(string $key, int $amount = 1): int { + return $this->redis->decrBy($key, $amount); + } + + public function close(): void { + if(!$this->persist) + $this->redis->close(); + } + + public function __destruct() { + $this->close(); + } +} diff --git a/src/Cache/Valkey/ValkeyProviderInfo.php b/src/Cache/Valkey/ValkeyProviderInfo.php new file mode 100644 index 0000000..f4b0f6f --- /dev/null +++ b/src/Cache/Valkey/ValkeyProviderInfo.php @@ -0,0 +1,135 @@ +endPoint; + } + + /** + * Retrieves the server hostname. + * + * @throws RuntimeException Unsupported endpoint specified. + * @return string + */ + public function getServerHost(): string { + if($this->endPoint instanceof UnixEndPoint) + return $this->endPoint->getPath(); + if($this->endPoint instanceof DnsEndPoint) + return $this->endPoint->getHost(); + if($this->endPoint instanceof IPEndPoint) + return $this->endPoint->getAddress()->getCleanAddress(); + + throw new RuntimeException('EndPoint type is not supported.'); + } + + /** + * Retrieves the server Unix path. + * + * @return int + */ + public function getServerPort(): int { + if($this->endPoint instanceof DnsEndPoint || $this->endPoint instanceof IPEndPoint) + return $this->endPoint->getPort(); + + return 0; + } + + /** + * A prefix that gets applied to every key. + * + * @return string + */ + public function getPrefix(): string { + return $this->prefix; + } + + /** + * Whether the connection should be persistent. + * + * @return bool + */ + public function isPersistent(): bool { + return $this->persist; + } + + /** + * Whether a username should be used. + * + * @return bool + */ + public function hasUsername(): bool { + return $this->username !== ''; + } + + /** + * Username to authenticate with. + * + * @return string + */ + public function getUsername(): string { + return $this->username; + } + + /** + * Whether a password should be used. + * + * @return bool + */ + public function hasPassword(): bool { + return $this->password !== ''; + } + + /** + * Password to authenticate with. + * + * @return string + */ + public function getPassword(): string { + return $this->password; + } + + /** + * Whether a database should be selected. + * + * @return bool + */ + public function hasDatabaseNumber(): bool { + return $this->dbNumber !== 0; + } + + /** + * Database that should be selected. + * + * @return int + */ + public function getDatabaseNumber(): int { + return $this->dbNumber; + } +} diff --git a/src/Data/DbTools.php b/src/Data/DbTools.php index a1063d3..eb44492 100644 --- a/src/Data/DbTools.php +++ b/src/Data/DbTools.php @@ -1,13 +1,12 @@ assertInstanceOf(ArrayCacheProvider::class, $arrayCache); + + $arrayCache = CacheTools::create('Index-Cache-ArrayCache-ArrayCacheBackend:'); + $this->assertInstanceOf(ArrayCacheProvider::class, $arrayCache); + } + + public function testMemcachedDSN(): void { + $memcached = new MemcachedBackend; + + // unix socket + $info = $memcached->parseDsn('memcached://:unix:/prefix/test/?server=/var/run/memcached.sock'); + + $endPoints = $info->getEndPoints(); + $this->assertEquals(1, count($endPoints)); + $this->assertInstanceOf(UnixEndPoint::class, $endPoints[0][0]); + $this->assertEquals('/var/run/memcached.sock', $endPoints[0][0]->getPath()); + $this->assertEquals(0, $endPoints[0][1]); + $this->assertEquals('prefix:test:', $info->getPrefixKey()); + $this->assertFalse($info->isPersistent()); + $this->assertNull($info->getPersistentName()); + $this->assertTrue($info->shouldUseBinaryProtocol()); + $this->assertTrue($info->shouldEnableCompression()); + $this->assertTrue($info->shouldTcpNoDelay()); + + // ipv4 + $info = $memcached->parseDsn('memcached://127.0.0.1?compress=no&nodelay=off&persist=test'); + + $endPoints = $info->getEndPoints(); + $this->assertEquals(1, count($endPoints)); + $this->assertInstanceOf(IPEndPoint::class, $endPoints[0][0]); + $this->assertEquals('127.0.0.1', (string)$endPoints[0][0]->getAddress()); + $this->assertEquals(0, $endPoints[0][0]->getPort()); + $this->assertEquals(0, $endPoints[0][1]); + $this->assertEquals('', $info->getPrefixKey()); + $this->assertTrue($info->isPersistent()); + $this->assertEquals('test', $info->getPersistentName()); + $this->assertTrue($info->shouldUseBinaryProtocol()); + $this->assertFalse($info->shouldEnableCompression()); + $this->assertFalse($info->shouldTcpNoDelay()); + + // ipv6 + $info = $memcached->parseDsn('memcached://[::1]:9001?proto=ascii'); + + $endPoints = $info->getEndPoints(); + $this->assertEquals(1, count($endPoints)); + $this->assertInstanceOf(IPEndPoint::class, $endPoints[0][0]); + $this->assertEquals('::1', (string)$endPoints[0][0]->getAddress()); + $this->assertEquals(9001, $endPoints[0][0]->getPort()); + $this->assertEquals(0, $endPoints[0][1]); + $this->assertEquals('', $info->getPrefixKey()); + $this->assertFalse($info->isPersistent()); + $this->assertNull($info->getPersistentName()); + $this->assertFalse($info->shouldUseBinaryProtocol()); + $this->assertTrue($info->shouldEnableCompression()); + $this->assertTrue($info->shouldTcpNoDelay()); + + // dns + $info = $memcached->parseDsn('memcache://localhost'); + + $endPoints = $info->getEndPoints(); + $this->assertEquals(1, count($endPoints)); + $this->assertInstanceOf(DnsEndPoint::class, $endPoints[0][0]); + $this->assertEquals('localhost', $endPoints[0][0]->getHost()); + $this->assertEquals(0, $endPoints[0][0]->getPort()); + $this->assertEquals(0, $endPoints[0][1]); + $this->assertEquals('', $info->getPrefixKey()); + $this->assertFalse($info->isPersistent()); + $this->assertNull($info->getPersistentName()); + $this->assertTrue($info->shouldUseBinaryProtocol()); + $this->assertTrue($info->shouldEnableCompression()); + $this->assertTrue($info->shouldTcpNoDelay()); + + // pool + $info = $memcached->parseDsn('memcached://:pool:/?server[]=/var/run/memcached.sock;20&server[]=127.0.0.1;10&server[]=[::1]:9001;5&server[]=localhost'); + + $endPoints = $info->getEndPoints(); + $this->assertEquals(4, count($endPoints)); + + $this->assertInstanceOf(UnixEndPoint::class, $endPoints[0][0]); + $this->assertEquals('/var/run/memcached.sock', $endPoints[0][0]->getPath()); + $this->assertEquals(20, $endPoints[0][1]); + + $this->assertInstanceOf(IPEndPoint::class, $endPoints[1][0]); + $this->assertEquals('127.0.0.1', (string)$endPoints[1][0]->getAddress()); + $this->assertEquals(0, $endPoints[1][0]->getPort()); + $this->assertEquals(10, $endPoints[1][1]); + + $this->assertInstanceOf(IPEndPoint::class, $endPoints[2][0]); + $this->assertEquals('::1', (string)$endPoints[2][0]->getAddress()); + $this->assertEquals(9001, $endPoints[2][0]->getPort()); + $this->assertEquals(5, $endPoints[2][1]); + + $this->assertInstanceOf(DnsEndPoint::class, $endPoints[3][0]); + $this->assertEquals('localhost', $endPoints[3][0]->getHost()); + $this->assertEquals(0, $endPoints[3][0]->getPort()); + $this->assertEquals(0, $endPoints[3][1]); + + $this->assertEquals('', $info->getPrefixKey()); + $this->assertFalse($info->isPersistent()); + $this->assertNull($info->getPersistentName()); + $this->assertTrue($info->shouldUseBinaryProtocol()); + $this->assertTrue($info->shouldEnableCompression()); + $this->assertTrue($info->shouldTcpNoDelay()); + } + + public function testValkeyDSN(): void { + $valkey = new ValkeyBackend; + + // unix socket + $info = $valkey->parseDsn('valkey://:unix:/prefix/test/?socket=/var/run/valkey.sock'); + + $this->assertInstanceOf(UnixEndPoint::class, $info->getEndPoint()); + $this->assertEquals('/var/run/valkey.sock', $info->getServerHost()); + $this->assertEquals(0, $info->getServerPort()); + $this->assertEquals('prefix:test:', $info->getPrefix()); + $this->assertFalse($info->isPersistent()); + $this->assertFalse($info->hasUsername()); + $this->assertEmpty($info->getUsername()); + $this->assertFalse($info->hasPassword()); + $this->assertEmpty($info->getPassword()); + $this->assertFalse($info->hasDatabaseNumber()); + $this->assertEquals(0, $info->getDatabaseNumber()); + + // ipv4 + $info = $valkey->parseDsn('valkey://password@127.0.0.1:6379?db=123'); + + $this->assertInstanceOf(IPEndPoint::class, $info->getEndPoint()); + $this->assertEquals('127.0.0.1', $info->getServerHost()); + $this->assertEquals(6379, $info->getServerPort()); + $this->assertEmpty($info->getPrefix()); + $this->assertFalse($info->isPersistent()); + $this->assertFalse($info->hasUsername()); + $this->assertEmpty($info->getUsername()); + $this->assertTrue($info->hasPassword()); + $this->assertEquals('password', $info->getPassword()); + $this->assertTrue($info->hasDatabaseNumber()); + $this->assertEquals(123, $info->getDatabaseNumber()); + + // ipv6 + $info = $valkey->parseDsn('valkey://username:password@[::1]/?persist&db=1'); + + $this->assertInstanceOf(IPEndPoint::class, $info->getEndPoint()); + $this->assertEquals('::1', $info->getServerHost()); + $this->assertEquals(0, $info->getServerPort()); + $this->assertEmpty($info->getPrefix()); + $this->assertTrue($info->isPersistent()); + $this->assertTrue($info->hasUsername()); + $this->assertEquals('username', $info->getUsername()); + $this->assertTrue($info->hasPassword()); + $this->assertEquals('password', $info->getPassword()); + $this->assertTrue($info->hasDatabaseNumber()); + $this->assertEquals(1, $info->getDatabaseNumber()); + + // dns + $info = $valkey->parseDsn('valkey://username:@misaka.nl/'); + + $this->assertInstanceOf(DnsEndPoint::class, $info->getEndPoint()); + $this->assertEquals('misaka.nl', $info->getServerHost()); + $this->assertEquals(0, $info->getServerPort()); + $this->assertEmpty($info->getPrefix()); + $this->assertFalse($info->isPersistent()); + $this->assertTrue($info->hasUsername()); + $this->assertEquals('username', $info->getUsername()); + $this->assertFalse($info->hasPassword()); + $this->assertEquals('', $info->getPassword()); + $this->assertFalse($info->hasDatabaseNumber()); + $this->assertEquals(0, $info->getDatabaseNumber()); + } +}