Writing Custom Session Handlers

Session are a tool which helps the web programmer overcome the stateless nature of the internet. You can use them to build shopping carts, monitor visits to a website, and even track how a user navigates through your application. PHP’s default session handling behavior can provide all you need in most cases, but there may be times when you want to expand the functionality and store session data differently. This article will show you how the default functionality works and then goes on to show you how override it to provide a custom solution.

The Anatomy of Session Storage

Before you implement a custom session save handler, it’s helpful to understand how PHP stores session data normally. The data is saved in a small file on the server which is associated with a unique ID which is then stored in a cookie on the client by the browser. If cookies aren’t used, the ID is usually passed along as a parameter in the URL. Whichever method is used, PHP retrieves the session data in subsequent page requests using the session ID. You can examine how this works by first determining where PHP saves the session data and examine its contents. You can check the session.save_path directive in your php.ini file, or use the session_save_path() function to output the path.

<?php
echo session_save_path();

The output will be the path to where the session data is stored. You can change the location in php.ini or by simply passing the new path to the session_save_path() function.

<?php
session_save_path("/path/to/session/data");

If you want to set a different directory in which to store session data, it’s a good practice to pick a location that’s outside your root web directory as this can reduce the risk of someone hijacking a session. Just make sure you’ve given the directory adequate permissions to be read from and written to by the server.

Now that you know where the session data is stored, you can navigate to that directory and find the individual files that store the information related to each active session. Typically the name of the files is “sess_” followed by the session ID associated with the data. You can find out what your session ID is by using the session_id() function.

<?php
echo session_id();

The output is a pseudo-random string of 32 characters, much like the following:

k623qubavm8acku19somu6ce1k0nb9aj

Opening the file sess_k623qubavm8acku19somu6ce1k0nb9aj you’ll see a string which describes the data in the session. If you were to store the following array in the session like this:

<?php
$arr = array("red", "blue"); 
$_SESSION["colors"] = $arr;

The contents of the file would look like this:

colors|a:2:{i:0;s:3:"red";i:1;s:4:"blue";}

The data is encoded in much the same way the serialize() function would treat the data. When data is stored in a session, all of it is collated together, serialized and, in the case of the default session storage mechanism in PHP, placed in a file. When you need to retrieve the data, the session unserializes the data for use by the application.

The thing to remember here is that the default way in which sessions read and write data is via the serialize() and unserialize() functions. The same will hold true if you were to alter the way in which this data was stored. You can change where the data is stored but not how it is stored.

The Session Life Cycle

When you start or continue a session with session_start(), the session’s data file is opened and the data is read into the $_SESSION array. When the script’s execution ends, the data is saved back to the file. So when you set a session variable, it is not immediately stored. You can, of course, force the session to store the data by calling session_write_close().

session_set_save_handler() provides a way to override the default session handling mechanism with new functionality so that you can store the data where you’d like. It requires six arguments, each a callback that handles a specific stage of the session life cycle. They are:

  1. Opening the session file
  2. Closing the session file
  3. Reading the session data
  4. Writing the session data
  5. Destroying the session
  6. Garbage collection of the session file and data

You must register a function for each stage of the life cycle or PHP will emit a warning that it cannot locate the function.

Warning: session_set_save_handler(): Argument 1 is not a valid callback

The callbacks can be defined in any way that PHP allows, so they could be straight functions, closures, object methods, or static class methods.

<?php
session_set_save_handler("open", "close", "read", "write", "destroy", "garbage");

In this example I’ve assumed the functions open(), close(), read(), write(), destroy(), and garbage() have been defined and registered them as callbacks to session_set_save_handler().

Creating a Custom Session Save Handler

To showcase each callback function I will override the default session handling behavior to instead store them within a MySQL database. The basic schema for the table should include a field for the session ID, a field for the data and a field to determine the time the session was last accessed.

CREATE TABLE session ( 
    session_id CHAR(32) NOT NULL, 
    session_data TEXT NOT NULL, 
    session_lastaccesstime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 
    PRIMARY KEY (session_id)
);

The default method of generating a session ID uses the MD5 algorithm which generates a 32-character string, and so I’ve set my session_id field as CHAR(32). The data held by the session can be unlimited but it’s good practice not to abuse this facility. The access time field can be of type TIMESTAMP.

Opening the Session

The first stage the session goes through is the opening of the session file. Here you can perform any action you like; the PHP documentation indicates that this function should be treated as a constructor, so you could use it to initialize class variables if you’re using an OOP approach. The callback takes two arguments which are passed automatically – the save path and the name of the session.

<?php
function open($path, $name) {
    $db = new PDO("mysql:host=myhost;dbname=mydb", "myuser", "mypassword");

    $sql = "INSERT INTO session SET session_id =" . $db->quote($sessionId) . ", session_data = '' ON DUPLICATE KEY UPDATE session_lastaccesstime = NOW()";
    $db->query($sql);    
}

The example implementation here will create an entry in the database to stores the data. In the event session entry already exists, the session_last_accesstime will be updated with the current timestamp. The timestamp is used to ensure the session is “live”; it is used later with garbage collection to purge stale sessions which I will discuss shortly.

Reading the Session

Immediately after the session is opened, the contents of the session are read from whatever store you have nominated and placed into the $_SESSION array. The callback takes one argument which is the session ID which enables you to identify the session that is being read. The callback must return a string of serialized data as PHP will then unserialize it as discussed previously. There is no need to cater for this in your code. If there is no session data for this session, you should return an empty string.

<?php
function read($sessionId) { 
    $db = new PDO("mysql:host=myhost;dbname=mydb", "myuser", "mypassword");

    $sql = "SELECT session_data FROM session where session_id =" . $db->quote($sessionId);
    $result = $db->query($sql);
    $data = $result->fetchColumn();
    $result->closeCursor();

    return $data;
}

It is important to understand that this data is not pulled every time you access a session variable. It is only pulled at the beginning of the session life cycle when PHP calls the open callback and then the read callback.

Writing to the Session

Writing the data back to whatever store you’re using occurs either at the end of the script’s execution or when you call session_write_close(). The callback receives two arguments, the data that is to be written and the session ID. The data received will already have been serialized by PHP.

<?php
function write($sessionId, $data) { 
    $db = new PDO("mysql:host=myhost;dbname=mydb", "myuser", "mypassword");

    $sql = "INSERT INTO session SET session_id =" . $db->quote($sessionId) . ", session_data =" . $db->quote($data) . " ON DUPLICATE KEY UPDATE session_data =" . $db->quote($data);
    $db->query($sql)
}

There are numerous ways in which you can handle the data that is passed into the read and write callbacks. PHP passes the data in serialized to the write function, and expects it serialized back from the read function, but that doesn’t mean you have to store it that way. You could unserialize the data immediately in the write callback and then perform some action dependent on the data or store it however you wish. The same applies to the read callback. All these decisions are implementation dependent and it’s up to you to decide what’s best for your situation.

Closing the Session

Closing the session occurs at the end of the session life cycle, just after the session data has been written. No parameters are passed to this callback so if you need to process something here specific to the session, you can call session_id() to obtain the ID.

<?php
function close() {
    $sessionId = session_id();
    //perform some action here
}

Destroying the Session

Destroying the session manually is essential especially when using sessions as a way to secure sections of your application. The callback is called when the session_destroy() function is called. The session ID is passed as a parameter.

<?php
function destroy($sessionId) {
    $db = new PDO("mysql:host=myhost;dbname=mydb", "myuser", "mypassword");

    $sql = "DELETE FROM session WHERE session_id =" . $db->quote($sessionId); 
    $db->query($sql);

    setcookie(session_name(), "", time() - 3600);
}

In its default session handling capability, the session_destroy() function will clear the $_SESSION array of all data. The documentation on php.net states that any global variables or cookies (if they are used) will not cleared, so if you are using a custom session handler you can perform these tasks in this callback also.

Garbage Collection

The session handler needs to cater to the fact that the programmer won’t always have a chance to manually destroy session data. For example, you may destroy session data when a user logs out and it is no longer needed, but there’s no guarantee a user will use the logout functionality to trigger the deletion. The garbage collection callback will occasionally be invoked by PHP to clean out stale session data. The parameter that is passed here is the max lifetime of the session which is an integer detailing the number of seconds that the lifetime spans.

<?php
function gc($lifetime) {
    $db = new PDO("mysql:host=myhost;dbname=mydb", "myuser", "mypassword");

    $sql = "DELETE FROM session WHERE session_lastaccesstime < DATE_SUB(NOW(), INTERVAL " . $lifetime . " SECOND)";
    $db->query($sql);
}

Garbage collection is performed on a random basis by PHP. The probability that garbage collection is invoked is decided through the php.ini directives session.gc_probability and session.gc_divisor. If the probability is set to 1 and the divisor is set to 100 for example, the garbage collector has a 1% chance of being run on each request (1/100).

The example callback uses the max lifetime of the session to compare the time that the session was last accessed. If the lifetime of the session has been exceeded, it removes the session and all data relating to it.

Summary

Changing the default behavior of PHP’s session handling can be useful in numerous situations. This article showed you how PHP treats session data “out of the box” and how this can be changed to suit your own application’s needs by storing the data in a MySQL database. Of course, you could pick your favorite database or another solution such as XML, Memcache, or another file-based system. Unserializing the data before it’s written will enable you to store the data as you see fit, just remember that you have to send the data back in serialized form in order for PHP to make use of it when it’s read.

Image via Sergey Mironov / Shutterstock

Win an Annual Membership to Learnable,

SitePoint's Learning Platform

  • http://www.psinas.com Martin Psinas

    The PHP manual states: “As of PHP 5.0.5 the write and close handlers are called after object destruction and therefore cannot use objects or throw exceptions.” To my understanding, this basically means that when you’re using PDO you should to wrap your session handler functions in a class that overrides the internal __destruct() method to call session_write_close().

    Would you mind explaining how your code is getting around that issue? I notice you’re creating a new database object within each function as opposed to calling it globally, and you’re also using $db-quote() instead of prepared statements.

    Thanks in advance for any clarification.

    • http://www.titan21.co.uk/ Tim Smith

      You could put a call to session_write_close() at the top of the destroy() method to ensure that writing and saving are done before the session is destroyed.

      • http://joshadell.com Josh Adell

        You can also register a shutdown function that will write and close the session before any objects are destroyed at the end: `register_shutdown_function(“session_write_close”);` This way, any objects your session handling uses will still be available. If you wrap your session handling in a class, put it in the constructor. See http://pureform.wordpress.com/2009/04/08/memcache-mysql-php-session-handler/

    • http://www.kaizen-web.com TheRedDevil

      He is getting around it by creating a new database connection instead of using a global one. The normal way is to use “register_shutdown_function()” instead as that allow you to use a global db connection.

  • http://www.kaizen-web.com TheRedDevil

    I found the article lacking and disappointing.
    -The session handler is based on functions.
    -It does not use a global/semi-global db connection, but instead setup a own connection in each function. It could also have been using prepared statements for improved security.
    -No effort is done to setup additional functionality into the session handler, like: hijacking protection, user/account tracking, different session states etc.

    While the article explains how the session handler works with a bare minimum of example code, it would have better basing the article on a real world example and how you would set it up in that case. As now people that dont know PHP that well will base their systems on the code in the article.
    Which again leads to the bad reputation PHP has received in the enterprise world.

    • Tammy Lawson

      Really? You’re going to bash the article for having functions instead of classes and then turn around and say you want global variables? I think it’s morons like you that “lead to the bad reputation PHP has received in the enterprise world”

      • http://www.kaizen-web.com TheRedDevil

        Tammy, from your reply I am certain you misread my post as I nowhere mention that you should use a global variable for the database connection.
        What I said was to use a “global database connection” instead of initiating a new one each time you need one, as you know connecting to a database is expensive (Not that much with MySQL, but when you move over to other SQL systems it can be).
        Normal practice is to pass along the database object(s) to the classes you need it in, and by that reuse the same object and connection across your application.

  • Mike Ramos

    I’ve used sessions before but this is new for me. I would like to see a live demo because I don’t get exactly it’s utility, probably because I don’t have much experience. Can you point me up to one?

  • Non_E

    What I miss really much is locking. Session data can and will get lost without that every now and then.

    • Elizabeth

      Missing sessions don’t happen all that often, odds are maybe 1:1000000 ratio so not very important (it’s not life threatening) so relax; simply putting a locking system on sessions just complicates matters and introduces other problems further down the road.

      • Non_E

        I disagree. In ajax powered web sites there is a good chance for simultaneous requests. That’s why I think that an article dealing with sessions should at least mention the issue.
        FYI the default PHP (file based) session driver implements locking.

      • My app works most of the time

        I agree with Non_E. The code lacks proper locking like one would get from the filesystem. The RDBMS should enforce pessimistic locking or you will face race conditions with apps using AJAX or anyone on a modern tabbed browser even.
        See this article for a good explanation: http://thwartedefforts.org/2006/11/11/race-conditions-with-ajax-and-php-sessions/

    • http://www.kaizen-web.com TheRedDevil

      You can implement locking if you make your own handler. Both filebased, database and other options has locking as a possibility. If it is really needed is another question, though a race condition can happen if a user is visiting multiple pages at once, and execute two requests at the same time. Though there is no way to get around that unless you initiate a lock when you start the session and then release before the script terminates. This would cause the second request that was initiated to wait until the first request releases its lock, assuming you make it a deadlock making reading unavailable as well until its released.

  • http://onefoldmedia.com/ Eddie

    I use sessions all the time with PHP but I’ve never actually looked at the saved files or their contents. As said in the post this would be very useful for tracking and analytic research as you can store all session data in a database for processing and retrieval. Thanks for posting.

  • berat

    i have a situtation like that. Someone said you have to write your own session. I have database called users and login system. When users login the system. he/she is directed to home page. But In the home page or in any page if user stays “inactive” in the system for a particular time.He/she is forced to log out. But the trick is i can have 10- 15 users. Each uses can log in the system. And i need to keep track of amount of logged out. And i want to know How many times users is forced to log out. Let say i have two users. And user1 is forced to log out two times and user2 is forced to log out three times. Btw i am using time out session. And i have an admin page. I want to see user1 was forced to logout two times and user2 was forced to log out three times as output seperately. Is it possible by writing your own session. If you have any suggestion please let me know. Thanks

  • jpdave

    thanks for the post .. its really very helpful ..but i am using ec2 with auto scale and have three instance running so these three apache server may create same session id and put it into the database what we should do to overcome this problem..

    thanks in advance

  • http://www.freelancephpprogrammeur.nl Anton

    Maybe i am overlooking something but in your sample, in the open function, you are using a variable $sessionId but that is not passed as an argument and is also not initiated in the function it self so where is that comming from? You might also want to move the creation of an empty session to the write method so nothing is done when the session stays empty?

    function write($sessionId, $data) {
    $sql = “INSERT INTO php_session
    SET session_id =” . quote($sessionId) . “,
    session_data = ‘”.quote($data).”‘
    ON DUPLICATE KEY UPDATE session_lastaccesstime = NOW() , session_data = ‘”.quote($data).””;

    query($sql,__METHOD__);
    }