Custom Session Management Using PHP and MySQL

In this PHP programming tutorial, we will look at how to create and manage your own custom sessions using LAMP with MySQL and PHP as the session storage engine.

Sessions are automatically managed by PHP as long as you call session_start() at the top of every script which needs access to the session. However, many people don’t know what actually happens when you call session_start().

When your script requests access to the session, PHP checks to see if the user passed in a cookie called PHPSESSID. If so, the value in the cookie (a 32-character unique hex string) is used as the filename to look up the user’s actual session DATA from a temporary file on the server. That data is a plaintext string serialized into a format that PHP can turn back into the session array that you’re familiar with already.

Of course, if there is not a PHPSESSID cookie in the user’s cookie headers, one is created for that user and the cookie is set on  their machine with an expiration time of zero (which means it persists until the window is closed). The data you put into $_SESSION is serialized and placed into the temporary session file when the script dies normally. Sessions will only fail to be written if the script comes to some catastrophic end, such as a segmentation fault.

NOTE: The serialization format PHP uses for session data is NOT the serialization format you get from the built-in serialize() function. The data that will be passed into your write() function will not be able to be parsed by any built-in function.  If you need to store your data in some other format (if you need it to be readable by another programming language, for instance) then you will have to use the existing $_SESSION array directly. This is more memory and processor intensive so it should be avoided if possible.

Now the default PHP session management functions work just fine for most uses, and there is no reason to create a custom session handler if all you have is one server with a moderate amount of traffic and session use. However, remember that session data is stored locally on your server’s hard drive. If you have more than one web server, obviously you will need to figure out another way to manage sessions so that the user’s session can be restored regardless of which web server they hit. This is where PHP’s session_set_save_handler() comes in handy.

Custom Session Handling – The Basics

Using session_set_save_handler(), you can set a custom function for every session operation (open, close, read, write, destroy, and garbage collection).

OPEN
The “open” function works like the session’s constructor. The open function will establish a connection to whatever storage location you choose to use for the session. For our purposes, we will be using a MySQL database, so the “open” function will he used to initialize the database connection.

The open function needs to accept two arguments. The first argument is the location to save the session file (a folder location) and the second is the name of the session file (the value of the PHPSESSID cookie). Even though your functions may not need those values, it still must accept them to avoid throwing errors.

CLOSE
The “close” functions works like the session’s destructor. The close function will disconnect from the storage location and perform any cleanup operations other than garbage collection (which has its own function). For our purposes (and for most purposes) there is no need for a close function or a destructor. Any open database, memcache, or filesystem connections are closed when the script dies anyway.

READ
The “read” function loads the session data from its storage location and returns a serialized string representing the serialized session data. Remember, the serialization function used by PHP for sessions is NOT serialize().

The read() function accepts a single argument, the session ID (the contents of the PHPSESSID cookie. A serialized string must be returned, which PHP will unserialize and place into $_SESSION. Even if you want to store your session data in another format, you must return a “php session serialized” string. More on this advanced technique later.

WRITE
The “write” function is what actually stores the session data into the storage location. The write function accepts two arguments: the session ID and the session data (which is already a serialized string at this point). The session data being stored has to be accessible by the read() function, obviously.

DESTROY
The session “destroy” function is what gets invoked if session_destroy() is called by your scripts. Destroying the session involves simply deleting the serialized string your are storing in your storage location. Destroy() takes only one argument: the session ID (the value of the PHPSESSID cookie)

GC
“GC” stands for “Garbage Collection,” it is the function that clears out old stored sessions. It takes a single argument: the maximum session lifetime set in your php.ini file. Your garbage collection function should be designed to destroy ALL sessions older than the lifetime value. Using a self-expiring storage engine like memcache alleviates the need to have a garbage collection function at all, but permanent storage engines like databases and filesystems require cleaning up.

Using this basic framework, you can easily make a custom session handler that uses a specific file location. Check the PHP manual entry for session_set_save_handler() for a quick example of a very basic session handler. Naturally, since the default session handling built into PHP uses filed, the custom filesystem handler is not really all that handy.

Using a MySQL Database as a Storage Engine

By now you should be familiar with the basics of database access. The functionality we need for session management is INSERT, UPDATE, SELECT, and DELETE. First, we need to create a table to store the session data. Remember that the session garbage collection function requires a timestamp on the session data. Therefore, we need three columns in this table.

ID
The session ID is a 32 character string. Normally you’re probably used to a table’s ID being numeric, like from an auto-incrememt column, but since PHP creates this string for you, use it as the primary ID. There is no real threat of collision. Make this field varchar(32)

CONTENTS
The session contents are a string of variable length.  Depending on how much data you plan to put into the session, it can be quite large. For this reason, the session contents should be a BLOB.  

UPDATE_DATE
The most recent date the session was modified needs to be stored along with the session data so that the garbage collection function knows which sessions to destroy.

CREATE TABLE EXAMPLE
CREATE TABLE `sessions` (
`id` varchar(32) NOT NULL,
`contents` blob,
`modify_date` datetime NOT NULL,
PRIMARY KEY (`id`),
KEY `modify_date` (`modify_date`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8


{mospagebreak title=PHP Session Handling Functions}

Now that we have the database table created, we need the session handling functions in PHP.  Using a CLASS to set the session would actually be the more correct solution, but there is a problem with using classes to handle session management.  The problem is, the write() function is called after the script finishes executing, which happens after the objects are already destroyed.  Using a database object or a session handling object would mean trying to solve the chicken-and-egg problem of how to use an object after it’s been destroyed.  The PHP manual has tips on this problem, but to keep this article brief we’ll be using standard functions in the global scope.  Note that this is generally considered a sloppy way to go about things.

<?php
 
/**
* Session Handling Functions using MySQL.
* simply include() this file at the top of any script you wish to use sessions in.
* As long as the table exists in the database, all sessions will be stored in that table.
* This file can be places onto multiple webservers running the same website and they will begin to share sessions between them.
*/


/**
* Open function is the constructor, it prepares the session connection
*
* @param string $savePath The storage location of the session. Unnecessary for our operations
* @param string $sessionId The ID of the session, unnecessary in the constructor for us
* @return bool
*/
function open( $savePath, $sessionID )
{
  // Declare a global session database connection.
  global $sessionDB;
  // Connect to the database
  $sessionDB = mysql_connect( HOST, USER, PASS );
  // If the connection was successful...
  if ( $sessionDB )
  {
    // Select the proper database
    mysql_select_db( DB, $sessionDB );
    // return true so PHP knows it's safe to continue using the session
    return true;
  }
  // If the connection failed, returning false from open() will prevent PHP from using the session since it's not working
  return false;
}
 
/**
* The close function destroys the open database connection.
*
* This function is unnecessary for our purposes, since open database connections are killed automatically by PHP at the end of scripts.
* However, to be explicit I'm including the proper contents here. Note that this function MUST EXIST, but could easily be empty.
*
* @return bool
*/
function close( )
{
  // Load the same global session database connection
  global $sessionDB;
  // destroy the database resource
  mysql_close( $sessionDB );
  // return true to indicate success
  return true;
}


/**
* The read function is what actually loads the session from the database.
*
* @param string $sessionID The 32-character session ID, from $_COOKIE['PHPSESSID']
* @return string
*/
function read( $sessionID )
{
  // Use the existing database connection created by open()
  global $sessionDB;
  // Build the SQL string to select the contents.
  // Even though this is a system function, you MUST use mysql_real_escape_string because $sessionID comes from a user's cookie and may be malicious
  $sql = 'SELECT contents FROM sessions WHERE id = "' . mysql_real_escape_string( $sessionID ) '"';
  //execute the query
  $rs = mysql_query( $sql, $sessionDB );
  //if the query succeeded...
  if ( $rs !== false )
  {
    // fetch the first (and only) row
    $row = mysql_fetch_array( $rs );
    // return the 'contents' field:
    return $row['contents'];
  }
  // if the query did not succeed, return an empty string (which will unserialize to an empty array for a new session)
  return '';
}

/**
* The write function writes the existing session data to the database AND UPDATES the most recent timestamp
*
* @param string $sessionID The 32-character session ID, from $_COOKIE['PHPSESSID']
* @param string $sessionData The serialized contents of the session
* @return bool
*/
function write( $sessionID, $sessionData )
{
  // Use the existing database connection created by open()
  global $sessionDB;
  // pre-format the variables for mysql since we use them twice:
  $sessionID = mysql_real_escape_string( $sessionID );
  $sessionData = mysql_real_escape_string( $sessionData );
 
  // Build the SQL statement.  Note that since we have the session ID set up as a primary key, we can use the INSERT ... ON DUPLICATE KEY UPDATE syntax:
  $sql = 'INSERT INTO sessions (id, contents, modify_date) VALUES ("' . $sessionId . '", "' . $sessionData . '", NOW() ) ' .
    'ON DUPLICATE KEY UPDATE contents = "' . $sessionData . '", modify_date = NOW()';
  // execute the query.  If successful, return true
  if ( mysql_query( $sql ) )
  {
    return true;
  }
  // query failed, return false:
  return false;
}

/**
* Destroys the session ID passed in whenever session_destroy() is called by your scripts
*
* @param string $sessionID The 32-character session ID, from $_COOKIE['PHPSESSID']
* @return bool
*/
function destroy( $sessionID )
{
  // Use the existing database connection created by open()
  global $sessionDB;
  // Build the SQL string to delete the contents.
  $sql = 'DELETE FROM sessions WHERE id = ' . mysql_real_escape_string( $sessionID );
  // execute the query.  If successful, return true
  if ( mysql_query( $sql ) )
  {
    return true;
  }
  // query failed, return false:
  return false;
}

/**
* Garbage collection happens at random, and destroys ALL sessions older than the lifetime passed in (in seconds)
*
* @param int $sessionMaxLifetime The maximum lifetime (in seconds) of sessions in your storage engine
* @return bool
*/
function garbageCollect( $sessionMaxLifetime )
{
  // Use the existing database connection created by open()
  global $sessionDB;
  // process sessionMaxLifetime, even though it should be trusted from php.ini
  $sessionMaxLifetime = intval( $sessionMaxLifetime );
  // Build the SQL string to delete the contents.
  $sql = 'DELETE FROM sessions WHERE modify_date < DATE_SUB( NOW(), INTERVAL ' . $sessionMaxLifetime . ' seconds )';
  // execute the query.  If successful, return true
  if ( mysql_query( $sql ) )
  {
    return true;
  }
  // query failed, return false:
  return false;
}


// once all the functions are delcared in the global scope, we can initiate the session handling in PHP:
session_set_save_handler( 'open', 'close', 'read', 'write', 'destroy', 'garbageCollect' );
session_start();



{mospagebreak title=Advanced PHP Session Techniques}

Note that there are a number of ways to improve this process.  First, putting all these functions into a session handling class would alleviate the risk of accidentally overwriting the $sessionDB variable or misuse of the session functions themselves. 

Second, this technique doesn’t make use of any kind of caching.  Clearly, a session which is being actively used will need to be fetched from the database every single time there’s a page load.  Adding a layer of memcache (or using memcache exclusively) would speed up this process significantly.

Third, and most important for very complex organizations, is the fact that this technique still uses the PHP default session serialization format, which is not compatible with other programming languages.  Now that your sessions are in a database, you should be able to crawl the database and examine the sessions using some kind of daemon in C, Python, or whatever language your organization uses for such tasks.  However, due to the restrictive nature of the serialization format, you can’t do that.  Look into session_encode and session_decode for ways of potentially handling that, but note that session_encode does not accept an argument, it operates directly on $_SESSION and your custom session functions will still return data that will overwrite the current contents of $_SESSION.

Conclusion

Using the technique above (indeed, simply copying and pasting the code) will allow you to split your website across multiple servers and still allow a user to maintain their session regardless of which server they connect to.  Putting your sessions in a database also allows you greater control of when garbage collection happens and is more reliable than straight filesystem access, especially for large numbers of sessions.

Google+ Comments

Google+ Comments