Added cache wrappers.

This commit is contained in:
flash 2024-04-10 22:23:34 +00:00
parent 09fc6b3958
commit ad75d72aa0
19 changed files with 1258 additions and 9 deletions

View file

@ -1 +1 @@
0.2404.21726
0.2404.102223

12
composer.lock generated
View file

@ -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",

View file

@ -0,0 +1,38 @@
<?php
// ArrayCacheBackend.php
// Created: 2024-04-10
// Updated: 2024-04-10
namespace Index\Cache\ArrayCache;
use InvalidArgumentException;
use Index\Cache\{ICacheBackend,ICacheProvider,ICacheProviderInfo};
/**
* Information about the dummy cache backend.
*/
class ArrayCacheBackend implements ICacheBackend {
public function isAvailable(): bool {
return true;
}
/**
* Creates a dummy cache provider.
*
* @param ArrayCacheProviderInfo $providerInfo Dummy provider info.
* @return ArrayCacheProvider Dummy provider instance.
*/
public function createProvider(ICacheProviderInfo $providerInfo): ICacheProvider {
if(!($providerInfo instanceof ArrayCacheProviderInfo))
throw new InvalidArgumentException('$providerInfo must by of type ArrayCacheProviderInfo');
return new ArrayCacheProvider;
}
/**
* @return ArrayCacheProviderInfo Dummy provider info instance.
*/
public function parseDsn(string|array $dsn): ICacheProviderInfo {
return new ArrayCacheProviderInfo;
}
}

View file

@ -0,0 +1,75 @@
<?php
// ArrayCacheProvider.php
// Created: 2024-04-10
// Updated: 2024-04-10
namespace Index\Cache\ArrayCache;
use InvalidArgumentException;
use Index\Cache\ICacheProvider;
/**
* Represents a dummy cache provider.
*/
class ArrayCacheProvider implements ICacheProvider {
private array $items = [];
public function get(string $key): mixed {
if(!array_key_exists($key, $this->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 {}
}

View file

@ -0,0 +1,13 @@
<?php
// ArrayCacheProviderInfo.php
// Created: 2024-04-10
// Updated: 2024-04-10
namespace Index\Cache\ArrayCache;
use Index\Cache\ICacheProviderInfo;
/**
* Represents dummy provider info.
*/
class ArrayCacheProviderInfo implements ICacheProviderInfo {}

77
src/Cache/CacheTools.php Normal file
View file

@ -0,0 +1,77 @@
<?php
// CacheTools.php
// Created: 2024-04-10
// Updated: 2024-04-10
namespace Index\Cache;
use InvalidArgumentException;
use RuntimeException;
/**
* Common cache actions.
*/
final class CacheTools {
private const CACHE_PROTOS = [
'null' => 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));
}
}

View file

@ -0,0 +1,36 @@
<?php
// ICacheBackend.php
// Created: 2024-04-10
// Updated: 2024-04-10
namespace Index\Cache;
/**
* Information about a cache provider. Should not have any external dependencies.
*/
interface ICacheBackend {
/**
* Checks whether the driver is available and a provider can be made.
*
* @return bool If true a provider can be made, if false a required extension is missing.
*/
function isAvailable(): bool;
/**
* Creates the cache provider described in the argument.
*
* @param ICacheProviderInfo $providerInfo Object that describes the desired provider.
* @throws \InvalidArgumentException An invalid implementation of ICacheProviderInfo was provided.
* @throws \RuntimeException If you ignored the output of isAvailable and tried to create an instance anyway.
* @return ICacheProvider The provider described in the provider info.
*/
function createProvider(ICacheProviderInfo $providerInfo): ICacheProvider;
/**
* Constructs a cache info instance from a dsn.
*
* @param string|array $dsn DSN with provider information.
* @return ICacheProviderInfo Provider info based on the dsn.
*/
function parseDsn(string|array $dsn): ICacheProviderInfo;
}

View file

@ -0,0 +1,70 @@
<?php
// ICacheProvider.php
// Created: 2024-04-10
// Updated: 2024-04-10
namespace Index\Cache;
use Index\ICloseable;
/**
* Represents a cache provider.
*/
interface ICacheProvider extends ICloseable {
/**
* Retrieve an item from the cache.
*
* @param string $key Key under which the value is stored.
* @return mixed value stored in the cache or null otherwise.
*/
function get(string $key): mixed;
/**
* Store an item in the cache.
*
* @param string $key Key under which to store the value.
* @param mixed $value Value to store.
* @param int $ttl Amount of seconds to store the value for, 0 for indefinite.
* @throws \RuntimeException if storing the value in the cache failed.
* @throws \InvalidArgumentException if an argument is incorrectly formatted, e.g. $ttl set to a negative value.
*/
function set(string $key, mixed $value, int $ttl = 0): void;
/**
* Deletes an item from the cache.
*
* @param string $key Key to be deleted.
* @throws \RuntimeException if an error occurred during deletion.
*/
function delete(string $key): void;
/**
* Sets a new expiration time on an item.
*
* @param string $key Key to be touched.
* @param int $ttl Amount of seconds to store the value for, 0 for indefinite.
* @throws \RuntimeException if storing the value in the cache failed.
* @throws \InvalidArgumentException if an argument is incorrectly formatted, e.g. $ttl set to a negative value.
*/
function touch(string $key, int $ttl = 0): void;
/**
* Increments an item.
*
* @param string $key Key to be incremented.
* @param int $amount Amount to increment by. Defaults to 1.
* @throws \RuntimeException if storing the value in the cache failed.
* @return int new, incremented value.
*/
function increment(string $key, int $amount = 1): int;
/**
* Decrements an item.
*
* @param string $key Key to be decremented.
* @param int $amount Amount to decrement by. Defaults to 1.
* @throws \RuntimeException if storing the value in the cache failed.
* @return int new, decremented value.
*/
function decrement(string $key, int $amount = 1): int;
}

View file

@ -0,0 +1,13 @@
<?php
// ICacheProviderInfo.php
// Created: 2024-04-10
// Updated: 2024-04-10
namespace Index\Cache;
/**
* Base type for cache provider info.
*
* Any cache backend should have its own implementation of this, there are no baseline requirements.
*/
interface ICacheProviderInfo {}

View file

@ -0,0 +1,108 @@
<?php
// MemcachedBackend.php
// Created: 2024-04-10
// Updated: 2024-04-10
namespace Index\Cache\Memcached;
use InvalidArgumentException;
use RuntimeException;
use Index\Cache\{ICacheBackend,ICacheProvider,ICacheProviderInfo};
use Index\Net\{EndPoint,UnixEndPoint};
/**
* Information about the memcached backend.
*/
class MemcachedBackend implements ICacheBackend {
public function isAvailable(): bool {
return extension_loaded('memcached')
|| extension_loaded('memcache');
}
/**
* Creates a memcached cache provider.
*
* Has compatibility with both the newer memcached extension and the older memcache extension.
* memcached is attempted first.
*
* @param MemcachedProviderInfo $providerInfo Memcached provider info.
* @return MemcachedProvider Memcached provider instance.
*/
public function createProvider(ICacheProviderInfo $providerInfo): ICacheProvider {
if(!($providerInfo instanceof MemcachedProviderInfo))
throw new InvalidArgumentException('$providerInfo must by of type MemcachedProviderInfo');
if(extension_loaded('memcached'))
return new MemcachedProviderModern($providerInfo);
if(extension_loaded('memcache'))
return new MemcachedProviderLegacy($providerInfo);
throw new RuntimeException('Unable to create a memcached provider.');
}
/**
* @return MemcachedProviderInfo Memcached provider info instance.
*/
public function parseDsn(string|array $dsn): ICacheProviderInfo {
if(is_string($dsn)) {
$dsn = parse_url($dsn);
if($dsn === false)
throw new InvalidArgumentException('$dsn is not a valid uri.');
}
if(!isset($dsn['host']))
throw new InvalidArgumentException('Host is missing from DSN.');
$host = $dsn['host'];
$isPool = $host === ':unix' || $host === ':pool';
$endPoints = [];
if(!$isPool) {
if(isset($dsn['port']))
$host .= ':' . $dsn['port'];
$endPoints[] = [EndPoint::parse($host), 0];
}
$endPoint = $isPool ? null : EndPoint::parse($host);
$prefixKey = str_replace('/', ':', ltrim($dsn['path'] ?? '', '/'));
parse_str(str_replace('+', '%2B', $dsn['query'] ?? ''), $query);
if(isset($query['server'])) {
if(is_string($query['server']))
$query['server'] = [$query['server']];
if(is_array($query['server']))
foreach($query['server'] as $endPoint) {
$parts = explode(';', $endPoint, 2);
$weight = count($parts) > 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);
}
}

View file

@ -0,0 +1,27 @@
<?php
// MemcachedProvider.php
// Created: 2024-04-10
// Updated: 2024-04-10
namespace Index\Cache\Memcached;
use Index\Cache\ICacheProvider;
/**
* Base Memcached provider implementation.
*/
abstract class MemcachedProvider implements ICacheProvider {
public const MAX_TTL = 30 * 24 * 60 * 60;
public abstract function get(string $key): mixed;
public abstract function set(string $key, mixed $value, int $ttl = 0): void;
public abstract function delete(string $key): void;
public abstract function touch(string $key, int $ttl = 0): void;
public abstract function increment(string $key, int $amount = 1): int;
public abstract function decrement(string $key, int $amount = 1): int;
public abstract function close(): void;
public function __destruct() {
$this->close();
}
}

View file

@ -0,0 +1,86 @@
<?php
// MemcachedProviderInfo.php
// Created: 2024-04-10
// Updated: 2024-04-10
namespace Index\Cache\Memcached;
use InvalidArgumentException;
use Index\Cache\ICacheProviderInfo;
/**
* Represents Memcached provider info.
*/
class MemcachedProviderInfo implements ICacheProviderInfo {
public function __construct(
private array $endPoints,
private string $prefixKey,
private string $persistName,
private bool $useBinaryProto,
private bool $enableCompression,
private bool $tcpNoDelay,
) {}
/**
* Gets server endpoints.
*
* @return array
*/
public function getEndPoints(): array {
return $this->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;
}
}

View file

@ -0,0 +1,98 @@
<?php
// MemcachedProviderLegacy.php
// Created: 2024-04-10
// Updated: 2024-04-10
namespace Index\Cache\Memcached;
use InvalidArgumentException;
use Memcache;
use Index\Net\{DnsEndPoint,IPEndPoint,UnixEndPoint};
/**
* Legacy Memcached provider implementation.
*/
class MemcachedProviderLegacy extends MemcachedProvider {
private Memcache $memcache;
private string $prefix;
private bool $persistent;
private bool $compress;
public function __construct(MemcachedProviderInfo $providerInfo) {
$this->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();
}
}

View file

@ -0,0 +1,118 @@
<?php
// MemcachedProviderModern.php
// Created: 2024-04-10
// Updated: 2024-04-10
namespace Index\Cache\Memcached;
use InvalidArgumentException;
use Memcached;
use RuntimeException;
use Index\Net\{DnsEndPoint,IPEndPoint,UnixEndPoint};
/**
* Base Memcached provider implementation.
*/
class MemcachedProviderModern extends MemcachedProvider {
private Memcached $memcached;
public function __construct(MemcachedProviderInfo $providerInfo) {
$this->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();
}
}

View file

@ -0,0 +1,86 @@
<?php
// ValkeyBackend.php
// Created: 2024-04-10
// Updated: 2024-04-10
namespace Index\Cache\Valkey;
use InvalidArgumentException;
use RuntimeException;
use Index\Cache\{ICacheBackend,ICacheProvider,ICacheProviderInfo};
use Index\Net\{EndPoint,UnixEndPoint};
/**
* Information about the Valkey backend.
*
* Also compatible with Redis and KeyDB.
*/
class ValkeyBackend implements ICacheBackend {
public function isAvailable(): bool {
return extension_loaded('redis');
}
/**
* Creates a Valkey cache provider.
*
* @param ValkeyProviderInfo $providerInfo Valkey provider info.
* @return ValkeyProvider Valkey provider instance.
*/
public function createProvider(ICacheProviderInfo $providerInfo): ICacheProvider {
if(!($providerInfo instanceof ValkeyProviderInfo))
throw new InvalidArgumentException('$providerInfo must by of type ValkeyProviderInfo');
return new ValkeyProvider($providerInfo);
}
/**
* @return ValkeyProviderInfo Valkey provider info instance.
*/
public function parseDsn(string|array $dsn): ICacheProviderInfo {
if(is_string($dsn)) {
$dsn = parse_url($dsn);
if($dsn === false)
throw new InvalidArgumentException('$dsn is not a valid uri.');
}
if(!isset($dsn['host']))
throw new InvalidArgumentException('Host is missing from DSN.');
$host = $dsn['host'];
$isUnix = $host === ':unix';
if(empty($dsn['user'])) {
$username = $password = '';
} else {
if(isset($dsn['pass'])) {
$username = $dsn['user'];
$password = $dsn['pass'];
} else {
$password = $dsn['user'];
$username = '';
}
}
if(!$isUnix) {
if(isset($dsn['port']))
$host .= ':' . $dsn['port'];
$endPoint = EndPoint::parse($host);
}
$prefix = str_replace('/', ':', ltrim($dsn['path'] ?? '', '/'));
parse_str(str_replace('+', '%2B', $dsn['query'] ?? ''), $query);
$unixPath = isset($query['socket']) && is_string($query['socket']) ? $query['socket'] : '';
$dbNumber = isset($query['db']) && is_string($query['db']) && ctype_digit($query['db']) ? (int)$query['db'] : 0;
$persist = isset($query['persist']);
if($isUnix) {
if(empty($unixPath))
throw new InvalidArgumentException('Unix socket path is missing from DSN.');
$endPoint = new UnixEndPoint($unixPath);
}
return new ValkeyProviderInfo($endPoint, $prefix, $persist, $username, $password, $dbNumber);
}
}

View file

@ -0,0 +1,81 @@
<?php
// ValkeyProvider.php
// Created: 2024-04-10
// Updated: 2024-04-10
namespace Index\Cache\Valkey;
use InvalidArgumentException;
use Redis;
use RedisException;
use Index\Cache\ICacheProvider;
use Index\Net\{DnsEndPoint,IPEndPoint,UnixEndPoint};
/**
* Valkey provider implementation.
*/
class ValkeyProvider implements ICacheProvider {
private Redis $redis;
private bool $persist;
public function __construct(ValkeyProviderInfo $providerInfo) {
$this->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();
}
}

View file

@ -0,0 +1,135 @@
<?php
// ValkeyProviderInfo.php
// Created: 2024-04-10
// Updated: 2024-04-10
namespace Index\Cache\Valkey;
use InvalidArgumentException;
use RuntimeException;
use Index\Cache\ICacheProviderInfo;
use Index\Net\{DnsEndPoint,EndPoint,IPEndPoint,UnixEndPoint};
/**
* Represents Valkey provider info.
*/
class ValkeyProviderInfo implements ICacheProviderInfo {
public function __construct(
private EndPoint $endPoint,
private string $prefix,
private bool $persist,
private string $username,
private string $password,
private int $dbNumber,
) {}
/**
* Gets server endpoint.
*
* @return EndPoint
*/
public function getEndPoint(): EndPoint {
return $this->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;
}
}

View file

@ -1,13 +1,12 @@
<?php
// DbTools.php
// Created: 2021-05-02
// Updated: 2023-07-21
// Updated: 2024-04-10
namespace Index\Data;
use Countable;
use InvalidArgumentException;
use Index\Type;
/**
* Common database actions.

189
tests/CacheToolsTest.php Normal file
View file

@ -0,0 +1,189 @@
<?php
// CacheToolsTest.php
// Created: 2024-04-10
// Updated: 2024-04-10
declare(strict_types=1);
use PHPUnit\Framework\TestCase;
use Index\Cache\CacheTools;
use Index\Cache\ArrayCache\{ArrayCacheBackend,ArrayCacheProvider};
use Index\Cache\Memcached\MemcachedBackend;
use Index\Cache\Valkey\ValkeyBackend;
use Index\Net\{DnsEndPoint,IPEndPoint,UnixEndPoint};
/**
* @covers CacheTools
*/
final class CacheToolsTest extends TestCase {
public function testBasicDSN(): void {
$arrayCache = CacheTools::create('array:');
$this->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());
}
}