Skip to content

Instantly share code, notes, and snippets.

@aezell
Created February 2, 2011 21:09
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save aezell/808445 to your computer and use it in GitHub Desktop.
Save aezell/808445 to your computer and use it in GitHub Desktop.
DB-backed Cached Session Handling in PHP
<?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