- Mark `RedisConnection::$connected` as read-only with `private(set)`. - Simplify authentication and database selection logic in `RedisConnection`. - Comment out DI container singleton registration in `RedisPoolInitializer`. - Annotate `RedisConnectionPool` with `#[Singleton]` attribute for improved clarity.
266 lines
6.2 KiB
PHP
266 lines
6.2 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Framework\Redis;
|
|
|
|
use Redis;
|
|
use RedisException;
|
|
|
|
/**
|
|
* Managed Redis connection with health checking and reconnection using php-redis extension
|
|
*/
|
|
final class RedisConnection implements RedisConnectionInterface
|
|
{
|
|
private Redis $client;
|
|
|
|
private(set) bool $connected = false;
|
|
|
|
public function __construct(
|
|
private readonly RedisConfig $config,
|
|
private readonly string $name = 'default'
|
|
) {
|
|
$this->connect();
|
|
}
|
|
|
|
public function getClient(): Redis
|
|
{
|
|
if (! $this->isConnected()) {
|
|
$this->reconnect();
|
|
}
|
|
|
|
return $this->client;
|
|
}
|
|
|
|
public function getDatabase(): int
|
|
{
|
|
return $this->config->database;
|
|
}
|
|
|
|
public function getName(): string
|
|
{
|
|
return $this->name;
|
|
}
|
|
|
|
public function isConnected(): bool
|
|
{
|
|
if (! $this->connected) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
return $this->client->ping() === '+PONG';
|
|
} catch (RedisException) {
|
|
$this->connected = false;
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public function reconnect(): void
|
|
{
|
|
$this->connected = false;
|
|
$this->connect();
|
|
}
|
|
|
|
private function connect(): void
|
|
{
|
|
if (! extension_loaded('redis')) {
|
|
throw new RedisConnectionException(
|
|
"Redis extension is not loaded. Please install php-redis extension or use alternative cache drivers."
|
|
);
|
|
}
|
|
|
|
$this->client = new Redis();
|
|
|
|
try {
|
|
// Connect to Redis
|
|
$success = $this->client->connect(
|
|
$this->config->host,
|
|
$this->config->port,
|
|
$this->config->timeout,
|
|
null, // reserved
|
|
0, // retry_interval
|
|
$this->config->readWriteTimeout
|
|
);
|
|
|
|
if (! $success) {
|
|
throw new RedisConnectionException("Failed to connect to Redis server");
|
|
}
|
|
|
|
// Authenticate if a password is provided
|
|
|
|
if ($this->client->auth($this->config->password) === false) {
|
|
throw new RedisConnectionException("Redis authentication failed");
|
|
}
|
|
|
|
// Select database
|
|
if ($this->config->database > 0) {
|
|
if ($this->client->select($this->config->database) === false) {
|
|
throw new RedisConnectionException("Failed to select Redis database {$this->config->database}");
|
|
}
|
|
}
|
|
|
|
// Set additional options
|
|
foreach ($this->config->options as $option => $value) {
|
|
$this->client->setOption($option, $value);
|
|
}
|
|
|
|
$this->connected = true;
|
|
} catch (RedisException $e) {
|
|
$this->connected = false;
|
|
|
|
throw new RedisConnectionException(
|
|
"Failed to connect to Redis ({$this->name}): " . $e->getMessage() . " with Host: {$this->config->host} and Password: {$this->config->password}",
|
|
previous: $e
|
|
);
|
|
}
|
|
}
|
|
|
|
public function flush(): void
|
|
{
|
|
if (! $this->isConnected()) {
|
|
$this->reconnect();
|
|
}
|
|
|
|
$this->client->flushDB();
|
|
}
|
|
|
|
public function get(string $key): string|false
|
|
{
|
|
if (! $this->isConnected()) {
|
|
$this->reconnect();
|
|
}
|
|
|
|
return $this->client->get($key);
|
|
}
|
|
|
|
public function set(string $key, string $value, ?int $ttl = null): bool
|
|
{
|
|
if (! $this->isConnected()) {
|
|
$this->reconnect();
|
|
}
|
|
|
|
if ($ttl === null) {
|
|
return $this->client->set($key, $value);
|
|
}
|
|
|
|
return $this->client->setex($key, $ttl, $value);
|
|
}
|
|
|
|
public function delete(string ...$keys): int
|
|
{
|
|
if (empty($keys)) {
|
|
return 0;
|
|
}
|
|
|
|
if (! $this->isConnected()) {
|
|
$this->reconnect();
|
|
}
|
|
|
|
return $this->client->del($keys);
|
|
}
|
|
|
|
public function exists(string ...$keys): int
|
|
{
|
|
if (empty($keys)) {
|
|
return 0;
|
|
}
|
|
|
|
if (! $this->isConnected()) {
|
|
$this->reconnect();
|
|
}
|
|
|
|
return $this->client->exists(...$keys);
|
|
}
|
|
|
|
public function ttl(string $key): int
|
|
{
|
|
if (! $this->isConnected()) {
|
|
$this->reconnect();
|
|
}
|
|
|
|
return $this->client->ttl($key);
|
|
}
|
|
|
|
public function expire(string $key, int $seconds): bool
|
|
{
|
|
if (! $this->isConnected()) {
|
|
$this->reconnect();
|
|
}
|
|
|
|
return $this->client->expire($key, $seconds);
|
|
}
|
|
|
|
public function incr(string $key, int $by = 1): int
|
|
{
|
|
if (! $this->isConnected()) {
|
|
$this->reconnect();
|
|
}
|
|
|
|
return $by === 1
|
|
? $this->client->incr($key)
|
|
: $this->client->incrBy($key, $by);
|
|
}
|
|
|
|
public function decr(string $key, int $by = 1): int
|
|
{
|
|
if (! $this->isConnected()) {
|
|
$this->reconnect();
|
|
}
|
|
|
|
return $by === 1
|
|
? $this->client->decr($key)
|
|
: $this->client->decrBy($key, $by);
|
|
}
|
|
|
|
public function mGet(array $keys): array
|
|
{
|
|
if (empty($keys)) {
|
|
return [];
|
|
}
|
|
|
|
if (! $this->isConnected()) {
|
|
$this->reconnect();
|
|
}
|
|
|
|
return $this->client->mGet($keys);
|
|
}
|
|
|
|
public function mSet(array $keyValuePairs): bool
|
|
{
|
|
if (empty($keyValuePairs)) {
|
|
return true;
|
|
}
|
|
|
|
if (! $this->isConnected()) {
|
|
$this->reconnect();
|
|
}
|
|
|
|
return $this->client->mSet($keyValuePairs);
|
|
}
|
|
|
|
public function command(string $command, string|int ...$arguments): mixed
|
|
{
|
|
if (! $this->isConnected()) {
|
|
$this->reconnect();
|
|
}
|
|
|
|
return $this->client->rawCommand($command, ...$arguments);
|
|
}
|
|
|
|
/**
|
|
* Close the connection when the object is destroyed
|
|
*/
|
|
public function __destruct()
|
|
{
|
|
if ($this->connected && isset($this->client)) {
|
|
try {
|
|
$this->client->close();
|
|
} catch (RedisException) {
|
|
// Ignore disconnection errors during destruction
|
|
}
|
|
}
|
|
}
|
|
}
|