Storing Sessions in a Database
Published in PHP Magazine on 14 Dec 2004Welcome to another edition of Guru Speak. I believe that one of the hallmarks of a good writer is the ability to mold a complex topic into something both palatable and interesting. These are the characteristics I strive for in Guru Speak, and I hope you consider my efforts to be a success. Please be sure to let me know what issues tend to trouble you the most or in what areas you would like to expand your knowledge and understanding. I am happy to cater to my readers.
This edition's topic is storing sessions in a database.
Background
While the default session storage mechanism is adequate for many PHP developers, you might find yourself wanting to modify its behavior from time to time. One of the most common reasons for wanting to change the default behavior is to store sessions in a database rather than the filesystem. The top reasons for this desire are:
- The application needs to be able to run on multiple servers without server affinity (methods that direct requests from the same client to the same server). An easy way to make sure that sessions continue to work properly is to store sessions in a central database that is common to all servers.
- The application needs to be able to run on a shared host, where there are significant security concerns associated with storing session data in the filesystem.
- The performance needs of the application are very demanding and require a more sophisticated storage solution for session data. There are many existing ideas and methodologies that address database performance issues, and these can be used when sessions are stored in a database.
Luckily, PHP makes this task very easy.
Session Data Store
Before you can store sessions in a database, you need to create a table. The following example demonstrates the CREATE
syntax for such a table:
CREATE TABLE sessions (
id varchar(32) NOT NULL,
access int(10) unsigned,
data text,
PRIMARY KEY (id)
);
While this is MySQL syntax, the same query should work on many databases with very little modification. Keep in mind that you can store any other information you want - perhaps you want to store the name of the application each session is associated with, the timestamp of each session's creation, or even an extra token to help complicate impersonation. I only show you the basic mechanism, and you can modify and expand it to fit your own needs.
If you DESCRIBE
this table, MySQL gives you a nice visual representation:
mysql> DESCRIBE sessions;
+--------+------------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+--------+------------------+------+-----+---------+-------+
| id | varchar(32) | | PRI | | |
| access | int(10) unsigned | YES | | NULL | |
| data | text | YES | | NULL | |
+--------+------------------+------+-----+---------+-------+
Now that you have a table to store the sessions in, you can learn how to put it to use.
session_set_save_handler()
PHP provides a function that lets you override the default session mechanism by specifying the names of your own functions for taking care of the distinct tasks. I have organized this article according to these functions that you must write. For each function, I show you an example (using MySQL) and provide an explanation.
The function is called session_set_save_handler()
, and it takes six arguments, each of which is the name of a function that you write. These functions are responsible for the following tasks:
- Opening the session data store
- Closing the session data store
- Reading session data
- Writing session data
- Destroying all session data
- Cleaning out old session data
For the purposes of this article, I assume that this function is called as follows:
<?php
session_set_save_handler('_open',
'_close',
'_read',
'_write',
'_destroy',
'_clean');
?>
You must call session_set_save_handler()
prior to calling session_start()
, but you can define the functions themselves anywhere.
The real beauty of this approach is that you don't have to modify your code or the way you use sessions in any way. $_SESSION
still exists and behaves the same way, PHP still takes care of generating and propagating the session identifier, and changes made to session configuration directives still apply. All you have to do is call this one function.
I show you examples of these functions, so that you have a more complete understanding. While I provide everything you need, I hope you take the time to understand what the functions do. This is a critical skill that you will be glad to have whenever problems arise.
_open()
and _close()
The _open()
and _close()
functions are closely related. These are used to open the session data store and close it, respectively. If you are storing sessions in the filesystem, these functions open and close files (and you likely need to use a global variable for the file handler, so that the other session functions can use it).
Because you're using a database, _open()
and _close()
can be as simple as the following:
<?php
function _open()
{
mysql_connect('127.0.0.1', 'myuser', 'mypass');
mysql_select_db('sessions');
}
function _close()
{
mysql_close();
}
?>
However, this approach isn't very flexible. When a database handler isn't specified in functions like mysql_select_db()
and mysql_query()
, MySQL uses the most recently opened connection. If
your code uses mysql_select_db()
to select a different database, the attempt to write the session data at the end of the script can potentially fail, because the most recently opened connection has a different database selected. This scenario is quite common, and developers find it terribly frustrating due to the difficulty of debugging it.
There are two ways to gracefully avoid this scenario. You can either use a separate MySQL connection for your session mechanism, or you can make a habit of always using mysql_select_db()
prior to any function that depends upon a specific database being selected (this includes both your session mechanism and your application).
In the examples in this article, I use the first method - using a separate connection for the session mechanism. I name this $_sess_db
, and my modified _open()
and _close()
functions are as follows:
<?php
function _open()
{
global $_sess_db;
$_sess_db = mysql_connect('127.0.0.1', 'myuser', 'mypass');
mysql_select_db('sessions', $_sess_db);
}
function _close()
{
global $_sess_db;
mysql_close($_sess_db);
}
?>
I begin the name of the database handler with an underscore as a naming convention to indicate that it should not be touched by application code. The idea is that it's a developer's own fault if problems arise as a result of modifying something that follows this convention.
I want to make one more minor modification. If you test the return value of a typical function in PHP, it returns TRUE
on success and FALSE
on failure. You can make _open()
and
_close()
do the same thing with a small modification to each:
<?php
function _open()
{
global $_sess_db;
if ($_sess_db = mysql_connect('127.0.0.1', 'myuser', 'mypass')) {
return mysql_select_db('sessions', $_sess_db);
}
return FALSE;
}
function _close()
{
global $_sess_db;
return mysql_close($_sess_db);
}
?>
Now that you have your _open()
and _close()
functions written, it's time to look at the other functions.
_read()
The _read()
function is called whenever PHP needs to read the session data. This takes place immediately after _open()
, and both are a direct result of your use of session_start()
.
PHP passes this function the session identifier, as the following example demonstrates:
<?php
function _read($id)
{
global $_sess_db;
$id = mysql_real_escape_string($id);
$sql = "SELECT data
FROM sessions
WHERE id = '$id'";
if ($result = mysql_query($sql, $_sess_db)) {
if (mysql_num_rows($result)) {
$record = mysql_fetch_assoc($result);
return $record['data'];
}
}
return '';
}
?>
PHP expects the session data in return, and you don't have to worry about the format, because PHP provides the data to the _write()
function (covered in the next section) in the same format that it expects it. Thus, this function returns exactly what is in the data
column for the matching record.
The handler PHP uses to handle data serialization is defined by the session.serialize_handler
configuration directive. It is set to php
by default.
_write()
The _write()
function is called whenever PHP needs to write the session data. This takes place at the very end of the script.
PHP passes this function the session identifier and the session data. You don't need to worry with the format of the data - PHP serializes it, so that you can treat it like a string. However, PHP does not modify it beyond this, so you want to properly escape it before using it in a query:
<?php
function _write($id, $data)
{
global $_sess_db;
$access = time();
$id = mysql_real_escape_string($id);
$access = mysql_real_escape_string($access);
$data = mysql_real_escape_string($data);
$sql = "REPLACE
INTO sessions
VALUES ('$id', '$access', '$data')";
return mysql_query($sql, $_sess_db);
}
?>
This function uses REPLACE
, which behaves exactly like INSERT
, except that it handles cases where a record already exists with the same session identifier by first deleting that record. This allows for a graceful way to handle choosing between INSERT
and UPDATE
without having to first perform a SELECT
to see whether a record already exists. However, although REPLACE
works with MySQL, it is not standard and does not work with all databases.
_destroy()
The _destroy()
function is called whenever PHP needs to destroy all session data associated with a specific session identifier. An obvious example is when you call session__destroy()
.
PHP passes the session identifier to the function:
<?php
function _destroy($id)
{
global $_sess_db;
$id = mysql_real_escape_string($id);
$sql = "DELETE
FROM sessions
WHERE id = '$id'";
return mysql_query($sql, $_sess_db);
}
?>
The _destroy()
function is only responsible for destroying the record in the session data store. It is up to the developer to destroy the data in $_SESSION
if desired.
_clean()
The _clean()
function is called every once in a while in order to clean out (delete) old records in the session data store. More specifically, the frequency in which this function is called is determined by two configuration directives, session.gc_probability
and session.gc_divisor
. The default values for these are 1
and 1000
, respectively, which means there is a 1 in 1000 (0.1%) chance for this function to be called per session initialization.
Because the _write()
function keeps the timestamp of the last access in the access
column for each record, this can be used to determine which records to delete. PHP passes the maximum number of seconds allowed before a session is to be considered expired:
<?php
function _clean($max)
{
global $_sess_db;
$old = time() - $max;
$old = mysql_real_escape_string($old);
$sql = "DELETE
FROM sessions
WHERE access < '$old'";
return mysql_query($sql, $_sess_db);
}
?>
The value that PHP passes to this function comes directly from the session.gc_maxlifetime
configuration directive. You can actually ignore this and determine your own maximum lifetime allowed, but it is much better to adhere to the value PHP passes. Doing so better adheres to the idea of transparently changing the storage mechanism. From a developer's perspective, the behavior of sessions should not change.
Recap
You now have all the tools you need to store sessions in a database. Hopefully you also have a better understanding about the purpose of these six functions and are better prepared to deal with problems as they arise.
That's all for Guru Speak. See you next time.