You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
178 lines
5.1 KiB
178 lines
5.1 KiB
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
/**
|
|
* @author Robert Strutts <Bob_586@Yahoo.com>
|
|
* @copyright (c) 2025, Robert Strutts
|
|
* @license MIT
|
|
*/
|
|
|
|
namespace IOcornerstone\Framework\Services;
|
|
|
|
use IOcornerstone\Framework\GzCompression;
|
|
use IOcornerstone\Framework\Enum\CompressionMethod as Method;
|
|
|
|
class RedisSessionHandler implements SessionHandlerInterface
|
|
{
|
|
private $redis;
|
|
private $prefix;
|
|
private $ttl;
|
|
private $lockTimeout = 10;
|
|
private $lockRetries = 5;
|
|
private $lockWait = 100000; // microseconds
|
|
private $enc;
|
|
|
|
public function __construct(callable $enc, array $config = [])
|
|
{
|
|
$defaults = [
|
|
'host' => '127.0.0.1',
|
|
'port' => 6379,
|
|
'prefix' => 'PHPREDIS_SESSION:',
|
|
'ttl' => null, // null means use session.gc_maxlifetime
|
|
'auth' => null,
|
|
'persistent' => true,
|
|
'database' => 1,
|
|
'timeout' => 0,
|
|
'read_timeout' => 2,
|
|
'retry_interval' => 100
|
|
];
|
|
|
|
$config = array_merge($defaults, $config);
|
|
|
|
$this->prefix = $config['prefix'];
|
|
|
|
// Use configured TTL if provided, otherwise use session.gc_maxlifetime
|
|
$this->ttl = $config['ttl'] ?? (int)ini_get('session.gc_maxlifetime');
|
|
|
|
// Fallback to 1440 seconds if neither is set
|
|
if ($this->ttl <= 0) {
|
|
$this->ttl = 1440;
|
|
}
|
|
|
|
$this->redis = new Redis();
|
|
|
|
if ($config['persistent']) {
|
|
$this->redis->pconnect($config['host'], $config['port'], $config['timeout'], $config['persistent']);
|
|
} else {
|
|
$this->redis->connect($config['host'], $config['port'], $config['timeout']);
|
|
}
|
|
|
|
if ($config['auth']) {
|
|
$this->redis->auth($config['auth']);
|
|
}
|
|
|
|
if ($config['database']) {
|
|
$this->redis->select($config['database']);
|
|
}
|
|
|
|
if ($config['read_timeout']) {
|
|
$this->redis->setOption(Redis::OPT_READ_TIMEOUT, $config['read_timeout']);
|
|
}
|
|
|
|
if ($config['retry_interval']) {
|
|
$this->redis->setOption(Redis::OPT_RETRY_INTERVAL, $config['retry_interval']);
|
|
}
|
|
|
|
$method = $config['method'] ?: Method::DEFLATE;
|
|
$level = $config['level'] ?: 4;
|
|
$enabled = $config['enabled'] ?: true;
|
|
|
|
$this->compression = new GzCompression($method, $level, $enabled);
|
|
|
|
$this->enc = $enc;
|
|
}
|
|
|
|
private function writeHelper($data): string {
|
|
$gc = $this->compression->compress($data);
|
|
if ($gc !== false) {
|
|
$data = $gc; // data is now compressed
|
|
}
|
|
if ($this->enc === false) {
|
|
return $data; // encryption is off
|
|
}
|
|
$e = $this->enc->encrypt($data);
|
|
return ($e !== false) ? $e : $data; // and encrypted
|
|
}
|
|
|
|
private function readHelper($data): string {
|
|
if ($data === null || $data === false) {
|
|
return "";
|
|
}
|
|
if ($this->enc !== false) {
|
|
$de = $this->enc->decrypt($data);
|
|
if ($de!== false) {
|
|
$data = $de; // data is now decrypted
|
|
}
|
|
}
|
|
$gd = $this->compression->decompress($data);
|
|
return ($gd !== false) ? $gd : $data;
|
|
}
|
|
|
|
public function open($savePath, $sessionName): bool
|
|
{
|
|
return true;
|
|
}
|
|
|
|
public function close(): bool
|
|
{
|
|
return true;
|
|
}
|
|
|
|
public function read($sessionId): string
|
|
{
|
|
$key = $this->prefix . $sessionId;
|
|
$attempts = 0;
|
|
|
|
// Simple locking mechanism to prevent race conditions
|
|
while ($attempts < $this->lockRetries) {
|
|
$lock = $this->redis->setnx($key . '.lock', 1);
|
|
|
|
if ($lock || $attempts === $this->lockRetries - 1) {
|
|
if ($lock) {
|
|
$this->redis->expire($key . '.lock', $this->lockTimeout);
|
|
}
|
|
|
|
$data = $this->redis->get($key);
|
|
$this->redis->del($key . '.lock');
|
|
return $this->readHelper($data) ?: '';
|
|
}
|
|
|
|
usleep($this->lockWait);
|
|
$attempts++;
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
public function write($sessionId, $data): bool
|
|
{
|
|
$data = write_helper($data);
|
|
$key = $this->prefix . $sessionId;
|
|
|
|
// Only write if the session has changed
|
|
if (isset($_SESSION['__LAST_ACTIVE__'])) {
|
|
$oldData = $this->redis->get($key);
|
|
if ($oldData === $data) {
|
|
// Just update TTL
|
|
return $this->redis->expire($key, $this->ttl);
|
|
}
|
|
}
|
|
|
|
$_SESSION['__LAST_ACTIVE__'] = time();
|
|
return $this->redis->setex($key, $this->ttl, $data);
|
|
}
|
|
|
|
public function destroy($sessionId): bool
|
|
{
|
|
$key = $this->prefix . $sessionId;
|
|
$this->redis->del($key . '.lock');
|
|
return (bool)$this->redis->del($key);
|
|
}
|
|
|
|
public function gc($maxLifetime): bool
|
|
{
|
|
// Redis handles expiration automatically
|
|
return true;
|
|
}
|
|
} |