Created
November 4, 2011 00:47
-
-
Save cowlby/1338379 to your computer and use it in GitHub Desktop.
Write-through caching of session data
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 | |
use PDO; | |
use Symfony\Component\HttpFoundation\SessionStorage\NativeSessionStorage; | |
/** | |
* CachedPdoSessionStorage. | |
* | |
* @author Jose Prado <[email protected]> | |
*/ | |
class CachedPdoSessionStorage extends NativeSessionStorage | |
{ | |
/** | |
* Default refresh time of 300 seconds (5 minutes) | |
* | |
* @var int | |
*/ | |
const REFRESH_TIME = 3600; | |
/** | |
* @var PDO | |
*/ | |
protected $_db; | |
/** | |
* @var CacheInterface | |
*/ | |
protected $_cache; | |
/** | |
* @var array | |
*/ | |
protected $dbOptions; | |
/** | |
* @var int | |
*/ | |
protected $lifetime; | |
protected $initialSessionId; | |
/** | |
* @var array | |
*/ | |
protected $initialSessionData; | |
/** | |
* Constructor. | |
* | |
* @param CacheInterface $cache A CacheInterface instance | |
* @param PDO $db A PDO instance | |
* @param array $options Optional options array | |
* @param array $dbOptions Optional DB options array | |
*/ | |
public function __construct(CacheInterface $cache, PDO $db, array $options = array(), array $dbOptions = array()) | |
{ | |
if (!array_key_exists('db_table', $dbOptions)) { | |
throw new \InvalidArgumentException('You must provide the "db_table" option for a PdoSessionStorage.'); | |
} | |
$this->_cache = $cache; | |
$this->_db = $db; | |
$this->lifetime = intval(ini_get("session.gc_maxlifetime")); | |
$options = array_merge(array( | |
'refresh_time' => self::REFRESH_TIME | |
), $options); | |
$this->dbOptions = array_merge(array( | |
'db_id_col' => 'sess_id', | |
'db_data_col' => 'sess_data', | |
'db_time_col' => 'sess_time', | |
), $dbOptions); | |
parent::__construct($options); | |
} | |
/** | |
* Starts the session. | |
*/ | |
public function start() | |
{ | |
if (self::$sessionStarted) { | |
return; | |
} | |
// use this object as the session handler | |
session_set_save_handler( | |
array($this, 'sessionOpen'), | |
array($this, 'sessionClose'), | |
array($this, 'sessionRead'), | |
array($this, 'sessionWrite'), | |
array($this, 'sessionDestroy'), | |
array($this, 'sessionGC') | |
); | |
return parent::start(); | |
} | |
/** | |
* Reads initial session data if a session exists and stores it for | |
* later comparison. | |
*/ | |
public function sessionOpen($path = null, $name = null) | |
{ | |
$id = session_id(); | |
if ($id != '') { | |
$this->initialSessionId = $id; | |
$this->initialSessionData = $this->sessionRead($id); | |
} | |
return TRUE; | |
} | |
/** | |
* Unsets all object data. | |
*/ | |
public function sessionClose() | |
{ | |
return TRUE; | |
} | |
/** | |
* Destroys a session. | |
* | |
* @param string $id A session ID | |
* @return Boolean true, if the session was destroyed, otherwise an exception is thrown | |
* @throws RuntimeException If the session cannot be destroyed | |
*/ | |
public function sessionDestroy($id) | |
{ | |
$this->_cache->delete($id); | |
$this->_cache->delete('db-'.$id); | |
// get table/column | |
$dbTable = $this->dbOptions['db_table']; | |
$dbIdCol = $this->dbOptions['db_id_col']; | |
// delete the record associated with this id | |
$sql = "DELETE FROM $dbTable WHERE $dbIdCol = :id"; | |
try { | |
$stmt = $this->_db->prepare($sql); | |
$stmt->bindParam(':id', $id, \PDO::PARAM_STR); | |
$stmt->execute(); | |
} catch (\PDOException $e) { | |
throw new \RuntimeException(sprintf('PDOException was thrown when trying to manipulate session data: %s', $e->getMessage()), 0, $e); | |
} | |
return TRUE; | |
} | |
/** | |
* Cached sessions expire automatically so we just need to evict all expired | |
* DB sessions. | |
* | |
* @param int $lifetime The lifetime of a session | |
* @return Boolean true, if old sessions have been cleaned, otherwise an exception is thrown | |
* @throws RuntimeException If any old sessions cannot be cleaned | |
*/ | |
public function sessionGC($lifetime) | |
{ | |
// get table/column | |
$dbTable = $this->dbOptions['db_table']; | |
$dbTimeCol = $this->dbOptions['db_time_col']; | |
// delete the record associated with this id | |
$sql = "DELETE FROM $dbTable WHERE $dbTimeCol < (:time - $lifetime)"; | |
try { | |
$this->_db->query($sql); | |
$stmt = $this->_db->prepare($sql); | |
$stmt->bindValue(':time', time(), \PDO::PARAM_INT); | |
$stmt->execute(); | |
} catch (\PDOException $e) { | |
throw new \RuntimeException(sprintf('PDOException was thrown when trying to manipulate session data: %s', $e->getMessage()), 0, $e); | |
} | |
return TRUE; | |
} | |
/** | |
* Reads the session data from either the cache or the DB in case of | |
* a miss. Then updates the database expiration time based on the time | |
* since the last DB access. | |
* | |
* @see Symfony\Component\HttpFoundation\SessionStorage.PdoSessionStorage::sessionRead() | |
*/ | |
public function sessionRead($id) | |
{ | |
$now = time(); | |
$data = $this->_cache->get($id); | |
if ($data === FALSE) { | |
$dbTable = $this->dbOptions['db_table']; | |
$dbDataCol = $this->dbOptions['db_data_col']; | |
$dbIdCol = $this->dbOptions['db_id_col']; | |
try { | |
$sql = "SELECT $dbDataCol FROM $dbTable WHERE $dbIdCol = :id"; | |
$stmt = $this->_db->prepare($sql); | |
$stmt->bindParam(':id', $id, \PDO::PARAM_STR, 255); | |
$stmt->execute(); | |
$sessionRows = $stmt->fetchAll(\PDO::FETCH_NUM); | |
// If session is found, update the time value, otherwise create | |
// a new session. | |
if (count($sessionRows) == 1) { | |
$data = $sessionRows[0][0]; | |
$this->updateSessionTime($id, $now); | |
} else { | |
$data = ''; | |
$this->createNewSession($id); | |
} | |
} catch (\PDOException $e) { | |
throw new \RuntimeException(sprintf('PDOException was thrown when trying to manipulate session data: %s', $e->getMessage()), 0, $e); | |
} | |
} else { | |
$expiration = $this->_cache->get('db-'.$id); | |
if ($expiration === FALSE || $now - $this->options['refresh_time'] > $expiration - $this->lifetime) { | |
$this->updateSessionTime($id, $now); | |
} | |
} | |
$this->_cache->set($id, $data, $this->lifetime); | |
return $data; | |
} | |
/** | |
* Writes the session data to the cache and the DB and updates the | |
* last db update record. | |
* | |
* @param string $id A session ID | |
* @param string $data A serialized chunk of session data | |
* @return Boolean true, if the session was written, otherwise an exception is thrown | |
* @throws \RuntimeException If the session data cannot be written | |
*/ | |
public function sessionWrite($id, $data) | |
{ | |
$retVal = $this->_cache->set($id, $data, $this->lifetime); | |
if ($this->initialSessionId !== $id || $this->initialSessionData !== $data) { | |
// get table/column | |
$dbTable = $this->dbOptions['db_table']; | |
$dbDataCol = $this->dbOptions['db_data_col']; | |
$dbIdCol = $this->dbOptions['db_id_col']; | |
$dbTimeCol = $this->dbOptions['db_time_col']; | |
switch ($this->_db->getAttribute(\PDO::ATTR_DRIVER_NAME)) { | |
case 'mysql': | |
$sql = "INSERT INTO $dbTable ($dbIdCol, $dbDataCol, $dbTimeCol) VALUES (:id, :data, :time)"; | |
$sql .= " ON DUPLICATE KEY UPDATE $dbDataCol = VALUES($dbDataCol),"; | |
$sql .= " $dbTimeCol = CASE WHEN $dbTimeCol = :time THEN (VALUES($dbTimeCol) + 1)"; | |
$sql .= " ELSE VALUES($dbTimeCol) END"; | |
break; | |
default: | |
$sql = "UPDATE $dbTable SET $dbDataCol = :data, $dbTimeCol = :time WHERE $dbIdCol = :id"; | |
break; | |
} | |
try { | |
$stmt = $this->_db->prepare($sql); | |
$stmt->bindParam(':id', $id, \PDO::PARAM_STR); | |
$stmt->bindParam(':data', $data, \PDO::PARAM_STR); | |
$stmt->bindValue(':time', time(), \PDO::PARAM_INT); | |
$stmt->execute(); | |
if (!$stmt->rowCount()) { | |
// No session exists in the database to update. This happens when we have called | |
// session_regenerate_id() | |
$this->createNewSession($id, $data); | |
} | |
} catch (\PDOException $e) { | |
throw new \RuntimeException(sprintf('PDOException was thrown when trying to manipulate session data: %s', $e->getMessage()), 0, $e); | |
} | |
$expiration = time() + $this->lifetime; | |
$this->_cache->set('db-'.$id, $expiration, $this->lifetime); | |
} | |
return $retVal; | |
} | |
/** | |
* Creates a new session with the given $id and $data | |
* | |
* @param string $id | |
* @param string $data | |
*/ | |
public function createNewSession($id, $data = '') | |
{ | |
// get table/column | |
$dbTable = $this->dbOptions['db_table']; | |
$dbDataCol = $this->dbOptions['db_data_col']; | |
$dbIdCol = $this->dbOptions['db_id_col']; | |
$dbTimeCol = $this->dbOptions['db_time_col']; | |
$sql = "INSERT INTO $dbTable ($dbIdCol, $dbDataCol, $dbTimeCol) VALUES (:id, :data, :time)"; | |
$stmt = $this->_db->prepare($sql); | |
$stmt->bindParam(':id', $id, \PDO::PARAM_STR); | |
$stmt->bindParam(':data', $data, \PDO::PARAM_STR); | |
$stmt->bindValue(':time', time(), \PDO::PARAM_INT); | |
$stmt->execute(); | |
return true; | |
} | |
/** | |
* Updates the time of the session in the database and refreshes the last | |
* DB access value in the cache. | |
* | |
* @param integer $id The session id to update. | |
* @param integer $now Optional time to use. | |
* @return Boolean true, if the session time was updated or no session was found. Otherwise exception is thrown. | |
*/ | |
public function updateSessionTime($id, $now = NULL) | |
{ | |
if ($now === NULL) { | |
$now = time(); | |
} | |
$dbTable = $this->dbOptions['db_table']; | |
$dbTimeCol = $this->dbOptions['db_time_col']; | |
$dbIdCol = $this->dbOptions['db_id_col']; | |
try { | |
$sql = "UPDATE $dbTable SET $dbTimeCol = :time WHERE $dbIdCol = :id"; | |
$stmt = $this->_db->prepare($sql); | |
$stmt->bindValue(':time', time(), \PDO::PARAM_INT); | |
$stmt->bindParam(':id', $id, \PDO::PARAM_STR); | |
$stmt->execute(); | |
} catch (\PDOException $e) { | |
throw new \RuntimeException(sprintf('PDOException was thrown when trying to manipulate session data: %s', $e->getMessage()), 0, $e); | |
} | |
$expiration = $now + $this->lifetime; | |
$this->_cache->set('db-'.$id, $expiration, $this->lifetime); | |
return TRUE; | |
} | |
} |
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 | |
/** | |
* Cache interface to provide a uniform interface for the SessionStorage classes. | |
* | |
* @author Jose Prado <[email protected]> | |
*/ | |
interface CacheInterface | |
{ | |
/** | |
* Retrieves an entry from the cache. | |
* | |
* @param string $id The id to retrieve | |
* @return mixed The data found or FALSE if cache miss | |
*/ | |
public function get($id); | |
/** | |
* Stores an entry in the cache using the supplied id and data. | |
* | |
* @param string $id The cache id | |
* @param mixed $data The data to store | |
* @param integer $lifetime The lifetime of the record. | |
* @return Boolean Whether or not the item was stored in the cache. | |
*/ | |
public function set($id, $data, $lifetime); | |
/** | |
* Deletes an entry from the cache. | |
* | |
* @param string $id The cache id to delete. | |
* @return Boolean Whether or not the item was deleted. | |
*/ | |
public function delete($id); | |
} |
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 | |
/** | |
* MemcacheAdapter | |
* | |
* @author Jose Prado <[email protected]> | |
*/ | |
class MemcacheAdapter implements CacheInterface | |
{ | |
/** | |
* @var Memcache | |
*/ | |
protected $_cache; | |
/** | |
* Constructor. | |
* | |
* @param Memcache $cache A Memcache instance | |
*/ | |
public function __construct(\Memcache $cache) | |
{ | |
$this->setCache($cache); | |
} | |
/** | |
* @return Memcache | |
*/ | |
public function getCache() | |
{ | |
return $this->_cache; | |
} | |
/** | |
* @param Memcache $cache | |
*/ | |
public function setCache(\Memcache $cache) | |
{ | |
$this->_cache = $cache; | |
return $this; | |
} | |
/** | |
* @see CacheInterface::get() | |
*/ | |
public function get($id) | |
{ | |
return $this->_cache->get($id); | |
} | |
/** | |
* @see CacheInterface::set() | |
*/ | |
public function set($id, $data, $lifetime) | |
{ | |
return $this->_cache->set($id, $data, FALSE, $lifetime); | |
} | |
/** | |
* @see CacheInterface::delete() | |
*/ | |
public function delete($id) | |
{ | |
return $this->_cache->delete($id); | |
} | |
} |
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 | |
/** | |
* MemcachedAdapter | |
* | |
* @author Jose Prado <[email protected]> | |
*/ | |
class MemcachedAdapter implements CacheInterface | |
{ | |
/** | |
* @var Memcached | |
*/ | |
protected $_cache; | |
/** | |
* Constructor. | |
* | |
* @param Memcached $cache A Memcached instance | |
*/ | |
public function __construct(\Memcached $cache) | |
{ | |
$this->setCache($cache); | |
} | |
/** | |
* @return Memcached | |
*/ | |
public function getCache() | |
{ | |
return $this->_cache; | |
} | |
/** | |
* @param Memcached $cache | |
*/ | |
public function setCache(\Memcached $cache) | |
{ | |
$this->_cache = $cache; | |
return $this; | |
} | |
/** | |
* @see CacheInterface::get() | |
*/ | |
public function get($id) | |
{ | |
return $this->_cache->get($id); | |
} | |
/** | |
* @see CacheInterface::set() | |
*/ | |
public function set($id, $data, $lifetime) | |
{ | |
return $this->_cache->set($id, $data, $lifetime); | |
} | |
/** | |
* @see CacheInterface::delete() | |
*/ | |
public function delete($id) | |
{ | |
return $this->_cache->delete($id); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment