MySQL Sessions
A share-nothing approach to web development is great for scalability, but there aren’t many web applications that don’t need to share anything between requests. The solution PHP (and most other web development lanugages) utilises is sessions. Sessions basically allow you to store some data between requests. That data is tied to an ID that gets passed between the browser and server in every request, using a cookie, in the URL or in GET/POST parameters.
The default data store for PHP sessions is files, and that’s fine so long as you only have one server, or you can tie each user to one server. When your app scales to the point where each request from a given user could go to one of any number of servers you need to replace this storage mechanism with something accessible from all of them. A database is the obvious choice.
I wrote the code below to solve this problem for a site that get > 1 million unique users per month (at the time of writing). It’s designed for ease of use and maximum performance. The session table exists in its own database so it can be moved to a dedicated server if required. It would also be trivial to split the session data across several tables by hashing or modifying the session ID to indicate which shard it was on.
The code is liberally commented so I won’t waste electrons describing it separately. Hopefully the way it works is straightforward and easy to understand. Don’t forget to check out the session documentation on the PHP website for full details about putting in your own session handler.
Update: Thanks to Jim Lucas for spotting some errors. I’ve updated the code below.
/***********************************************************************
MySQL Session class
This class encapsulates everything needed to store your PHP sessions
in a MySQL database. To use it simply call Session::Start() instead
of session_start().
You'll need a table like this in your database. You can change the
name but the fields should remain as they are defined here.
CREATE TABLE `sessions` (
`id` varchar(50) NOT NULL,
`name` varchar(50) NOT NULL,
`expires` int(10) unsigned NOT NULL default '0',
`data` text,
PRIMARY KEY (`id`, `name`)
) TYPE=InnoDB;
***********************************************************************/
class Session
{
private $lifetime = 900;
private $db = false;
private $table = 'sessions';
private $name = 'phpsess';
static public function Start($host = 'localhost', $username = 'root', $password = '', $db = 'sessionstore', $table = 'sessions', $lifetime = 0)
{
// Create the object
$GLOBALS['_SESSION_OBJ_'] = new self($host, $username, $password, $db, $table, $lifetime);
// Hook up the handler
session_set_save_handler(
array(&$GLOBALS['_SESSION_OBJ_'], 'Open'),
array(&$GLOBALS['_SESSION_OBJ_'], 'Close'),
array(&$GLOBALS['_SESSION_OBJ_'], 'Read'),
array(&$GLOBALS['_SESSION_OBJ_'], 'Write'),
array(&$GLOBALS['_SESSION_OBJ_'], 'Destroy'),
array(&$GLOBALS['_SESSION_OBJ_'], 'GC')
);
// Start the session
session_start();
}
private function __construct($host = 'localhost', $username = 'root', $password = '', $db = 'sessionstore', $table = 'sessions', $lifetime = 0)
{
// By default we use the session lifetime in php.ini, but this can be overridden in code
$this->lifetime = ($lifetime == 0 ? get_cfg_var('session.gc_maxlifetime') : $lifetime);
// This is the table where session data is to be stored
$this->table = $table;
// Now we connect to the database, throwing expections if anything fails
$this->db = @mysql_connect($host, $username, $password);
if ($this->db === false)
throw new Exception('Failed to connect to the session store', 1);
if (false === @mysql_select_db($db, $this->db))
throw new Exception('Failed to select session store', 2);
}
public function Open($path, $name)
{
// Store the session name for future use, we don't have any use for the path
$this->name = $name;
// Everything is OK if we have a connection to the database
return ($this->db !== false);
}
public function Close()
{
// Run the garbage collector 10% of the time
if (rand(1, 10) == 5) $this->GC($this->lifetime);
// Close the database connection
return @mysql_close($this->db);
}
public function & Read($id)
{
// By default we return nothing
$retval = '';
// Try to read an entry from the database
$result = mysql_query('select data from `'.$this->table.'` where id = "'.mysql_real_escape_string($id, $this->db).'" and name = "'.mysql_real_escape_string($this->name, $this->db).'" and expires > '.time().' order by expires desc', $this->db);
if ($result !== false and mysql_num_rows($result) > 0)
{
// Found one, get it
$retval = mysql_result($result, 0, 0);
}
return $retval;
}
public function Write($id, $data)
{
$retval = false;
// Build the query. We use the MySQL ON DUPLICATE KEY feature to do an insert/update in one query.
$sql = 'insert into `'.$this->table.'` set ';
$sql.= 'id = "'.mysql_real_escape_string($id, $this->db).'", ';
$sql.= 'name = "'.mysql_real_escape_string($this->name, $this->db).'", ';
$sql.= 'expires = '.(time() + $this->lifetime).', ';
$sql.= 'data = "'.mysql_real_escape_string($data, $this->db).'" ';
$sql.= 'on duplicate key update expires = values(expires), data = values(data)';
// Run it and return true if it was successful
$result = mysql_query($sql, $this->db);
if ($result !== false and mysql_affected_rows($this->db) > 0)
$retval = true;
@mysql_free_result($result);
return $retval;
}
public function Destroy($id)
{
// Remove this session from the database
$result = mysql_query('delete from `'.$this->table.'` where id = "'.mysql_real_escape_string($id, $this->db).'" and name = "'.mysql_real_escape_string($this->name, $this->db).'"', $this->db);
if ($result !== false and mysql_affected_rows($this->db) > 0)
return true;
return false;
}
public function GC($lifetime)
{
// Remove any sessions that have expired
$result = mysql_query('delete from `'.$this->table.'` where expires < '.time(), $this->db);
return ($result === false ? 0 : mysql_affected_rows($this->db));
}
}
Love the look of your script but I can’t actually get it to work, it sounds like exactly what I’m looking for too!
Any chance you can include a little bit of code showing how to instantiate it into my own PHP scripts as everything I try is throwing out errors.
Cheers
Craig
Hi Applemad, thanks for the comment.
To get it to work simply call the static Start method…
Session::Start(‘localhost’, ‘root’, ”, ’sessionstore’, ’sessions’, 0);
Change the arguments as needed – check the source for what each one is. This needs to be called in place of session_start(), i.e. before any output. Do not call session_start() in your own code if you’re using this class.
In this case, is the session Id still stored in a cookie?
Also, I can’t seem to get this script working. It appears that the connection to the db is being made because no exceptions are thrown, but nothing is stored to the db. In testing, the only time I can get the script to store anything to db is when I call the exit; function directly after the Session::Start() call. In which case, the session id and name are store, but no data. I’m assuming a simple $_SESSION['session_var'] = ’something’; will store some data to the db, but this doesn’t work. Any help is greatly appreciated. Thanks.
Regarding my previous post, I later realized that the data was not being stored because I was using more than one db. Upon accessing the other db, the code was making a call to the mysql_select_db() function, addressing all subsequent db queries to the wrong db. In order to make it work, I was forced to add the mysql_select_db() function before each mysql_query() call, to ensure I was addressing the correct db. Don’t know if this is considered correct coding practice when dealing with multiple db’s, but oh well it works!
@Anthony: First of all please check the php.ini settings display_errors and error_reporting in your development environment because it sounds like PHP is not telling you when something goes wrong.
Secondly you need to make sure the database connection used by this class has a different username and password to your other database connection, otherwise the MySQL extension will attempt to use the same connection which results in the behaviour you describe. By using different credentials you can be certain you will get separate connections.
Alternatively you might be better off putting your session table into your main database so you only need one database connection open, but whether this is the best idea depends on your specific application.
I changed the username and password, and removed the mysql_select_db() calls that I added. It works. Thanks for the answer!