Created
February 2, 2011 21:09
-
-
Save aezell/808445 to your computer and use it in GitHub Desktop.
DB-backed Cached Session Handling in PHP
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
class CachedSessionHandler { | |
private static $LOG_SESSION_ACTIVITY = false; | |
private static $binding; | |
private $cache; | |
private $read_sessions = array(); | |
public static function bind($database) | |
{ | |
// The sessions are considered 'bound' if they are allowed to | |
// reference the database. If a session starts up unbound, it will | |
// only operate through memcache. | |
CachedSessionHandler::$binding = $database; | |
} | |
public function open($save_path, $session_name) | |
{ | |
$this->cache = new Memcache; | |
$this->max_session_age = (int) ini_get("session.gc_maxlifetime") | |
or die("session.gc_maxlifetime must be configured in php.ini"); | |
$this->min_session_age = (int) get_cfg_var("session_min_lifetime") | |
or die("session_min_lifetime must be configured in php.ini"); | |
$this->session_sleep = $this->max_session_age - $this->min_session_age; | |
$servers = ini_get("session.save_path"); | |
preg_match_all("/tcp:\/\/([\w\.]+)\:(\d+)/i", $servers, $matches, PREG_SET_ORDER); | |
foreach ($matches as $match) | |
{ | |
$server = $match[1]; | |
$port = $match[2]; | |
$this->cache->addServer($server, (int) $port); | |
} | |
CachedSessionHandler::bind(); | |
return true; | |
} | |
public function close() | |
{ | |
$this->cache->close(); | |
return true; | |
} | |
public function read($id) | |
{ | |
if (CachedSessionHandler::$LOG_SESSION_ACTIVITY) { | |
error_log("*** Reading the session. Bound to: " . | |
(CachedSessionHandler::$binding ? DB::getDSNString(CachedSessionHandler::$binding->dsn, true) : "(nothing)")); | |
} | |
$this->read_sessions[$id] = ""; | |
$expires_key = $id . '_expires'; | |
$session = $this->cache->get($id); | |
$expired = $this->cache->get($expires_key) ? false : true; | |
if (!$session && CachedSessionHandler::$binding != null) | |
{ | |
$safeid = pg_escape_string($id); | |
$sql = "SELECT content, extract(epoch from expires - current_timestamp) as time_left FROM sessions " . | |
"WHERE session_id = '$safeid' " . | |
"and expires >= current_timestamp"; | |
$row = CachedSessionHandler::$binding->getRow($sql); | |
$session = $row['content']; | |
$expired = $row['time_left'] < $this->min_session_age; | |
} | |
if (!$session) | |
return ""; | |
$this->cache->set($id, $session, null, $this->max_session_age); | |
if ($expired) | |
{ | |
if ($this->should_persist($session)) | |
$this->write_db_session($id, $session); | |
} | |
$this->read_sessions[$id] = $session; | |
return $session; | |
} | |
public function write($id, $sess_data) | |
{ | |
$encoded_sess_data = $this->make_valid_utf8($sess_data); | |
$this->cache->set($id, $encoded_sess_data, null, $this->max_session_age); | |
if ($sess_data != $this->read_sessions[$id]) | |
{ | |
if ($this->should_persist($sess_data)) | |
$this->write_db_session($id, $encoded_sess_data); | |
else | |
$this->cache->delete($id . '_expires'); | |
} | |
unset($this->read_sessions[$id]); | |
return true; | |
} | |
public function destroy($id) | |
{ | |
if (CachedSessionHandler::$binding != null) | |
{ | |
$safeid = pg_escape_string($id); | |
$sql = "DELETE FROM sessions WHERE session_id = '$safeid'"; | |
CachedSessionHandler::$binding->query($sql); | |
} | |
$this->cache->delete($id); | |
$this->cache->delete($id . '_expires'); | |
return true; | |
} | |
public function gc($maxlifetime) | |
{ | |
// Don't do anything. We'll clean up the sessions from outside here. | |
return true; | |
} | |
private function should_persist($session) | |
{ | |
// TODO: it would be better if we could parse the session string | |
// properly. Note that we can't use _SESSION because it won't | |
// necessarily correspond to the content of $session (e.g. if we | |
// are reading session data in, it won't be set yet). | |
if (CachedSessionHandler::$binding == null) | |
return false; // database sessions not available | |
return (strpos($session, 'user_id|s:6:"anon"') === false); | |
} | |
private function write_db_session($id, $session) | |
{ | |
if (CachedSessionHandler::$LOG_SESSION_ACTIVITY) { | |
error_log("*** Writing the session. Bound to: " . | |
(CachedSessionHandler::$binding ? DB::getDSNString(CachedSessionHandler::$binding->dsn, true) : "(nothing)")); | |
} | |
$safeid = pg_escape_string($id); | |
$session = pg_escape_string($session); | |
$sql = "update sessions set content = '$session', " . | |
"expires = current_timestamp + interval '$this->max_session_age seconds' " . | |
"where session_id = '$safeid'"; | |
$result = CachedSessionHandler::$binding->query($sql); | |
if (cepWeb::isError($result)) | |
die("Database error while trying to write session: " . $result->getMessage()); | |
if (CachedSessionHandler::$binding->affectedRows() == 0) | |
{ | |
$sql = "insert into sessions (session_id, expires, content) " . | |
"values ('$safeid', current_timestamp + interval '$this->max_session_age seconds', '$session')"; | |
$result = CachedSessionHandler::$binding->query($sql); | |
if (cepWeb::isError($result)) | |
die("Database error while trying to write session: " . $result->getMessage()); | |
} | |
$this->cache->set($id . '_expires', "1", null, $this->session_sleep); | |
} | |
private function make_valid_utf8($session) | |
{ | |
if (mb_check_encoding($session, "utf-8")) | |
return $session; | |
session_decode($session); | |
foreach ($_SESSION as $key => $value) | |
$_SESSION[$key] = $this->fix_utf8($value); | |
return session_encode(); | |
} | |
private function fix_utf8($data) | |
{ | |
if (is_array($data)) | |
{ | |
foreach($data as $key => $value) | |
$data[$key] = $this->fix_utf8($value); | |
return $data; | |
} | |
elseif (is_string($data)) | |
{ | |
if (mb_check_encoding($data, "utf-8")) | |
return $data; | |
else | |
return mb_convert_encoding($data, "utf-8", "iso-8859-1"); | |
} | |
else | |
{ | |
return $data; | |
} | |
} | |
} | |
$session = new CachedSessionHandler(); | |
session_set_save_handler(array(&$session, 'open'), array(&$session, 'close'), | |
array(&$session, 'read'), array(&$session, 'write'), | |
array(&$session, 'destroy'), array(&$session, 'gc')); | |
?> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment