PHP - - By Tim Smith

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

Sponsors