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.
IOcornerstone/src/Framework/Services/Sessions/RedisSessionHandler.php

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;
}
}