Saving PHP Sessions in Redis

Sessions allow a web-based application to maintain state across multiple HTTP requests. You can register any number of variables as session variables which are then typically stored in a temporary directory on the server or passed to the client browser through cookies. Subsequent requests have access to the saved information and the application’s state is preserved across multiple requests.

PHP’s default handling of session data is probably sufficient for most applications, but sometimes a project will demand a different storage approach. Luckily, the session handling routines can be overridden either by a series of functions (in older versions of PHP) or a class with methods (in newer versions) which handle various aspects of session management.

In this article we’ll learn how to create a custom session handler which implements PHP’s SessionHandlerInterface interface and stores the session data in a Redis database.

Why Custom Storage?

You may be wondering why someone might want (or need) to go through the trouble of writing a custom handler. If so, it may be beneficial to consider the following.

It’s common when applications reside within a server farm for each host to handle requests for load balancing and redundancy. Session data stored in temporary files would only be accessible to their particular host. Since the server handling a request may not be the same one that handled the previous request, guaranteed access to state information is impossible without an alternative handling mechanism. Session data could be stored to a central storage mechanism and be made available to all machines in the cluster.

On a shared hosting server, it’s likely that all temporary session files are stored within the same directory. The directory and its contents is accessible to any processes initiated under the web service account which has the potential to pose a serious security risk. A malicious user who can access someone else’s session data has the ability to impersonate the legitimate user.

Custom session handling also provides a greater degree of flexibility. Because you’re specifying a new method of storage and access for the information, you can manipulate the data any way you want to meet your needs. Perhaps you may want to retain information for security auditing or troubleshooting. Custom handlers can be used to as hooks to expand an application in terms of size and functionality in any number of ways.

Session Management Actions

Since we’re going to manage our own session data instead of letting PHP handle it for us, any code we write needs to address each of the six session management tasks.

  • open – opens a resource to the storage mechanism designed to save session information.
  • close – closes the storage resource and initiates any other necessary cleanup activities.
  • read – retrieves previously saved session data from the storage mechanism.
  • write – stores new session data which the application needs to remember.
  • destroy – resets a session and discards any information from it.
  • gc – purges data from the storage mechanism after it has become stale and is no longer needed.

Each of these tasks must be addressed when writing custom session handlers; PHP will not let us override the open process but forget to override the garbage collection process, for example. It’s all or nothing.

In versions of PHP prior to 5.4, we would need to specify six callables (one function or method for handling each task) in the order above as arguments to the session_set_save_handler() function. In 5.4 and later, we can create a class that implements the SessionHandlerInterface interface and pass an instance instead. The interface exposes six methods with the same signatures as would be used for the functions in the pre-5.4 approach.

Storage Mechanism

Your choice of a storage mechanism is dictated by your needs. It could be anything—remote temporary files, a MySQL database, an LDAP server, shared memory segments, an XML-RPC service, an IMAP inbox, etc. For this article I’m going to illustrate storing session data in Redis.

PHP automatically generates a unique identifier to track the session and link it to a specific client. Because this token is unique for each session, the identifier is well suited for use as a key (in fact, this token usually serves as the filename in PHP’s default disk-based handling approach).

The session data is also automatically serialized and unserialized by PHP. That is, the method that receives the data for storing is passed the data already serialized, and the method that retrieves the data is expected to return serialized data. The functions session_encode() and session_decode() are also available if we need them for any reason, but generally we can simply store and retrieve the session data as provided.

Of course it’s important to clean up stale sessions that are no longer needed. For example, if we’re storing session data in a MySQL database for example, we’d want to include a timestamp with each record. The timestamp would be checked by our method that overrides the garbage collection behavior. But with Redis, we can take advantage of its EXPIRE command. EXPIRE sets a timeout or a TTL (time to live) on a key, and the key is automatically deleted after the timeout expires.

Show Me the Code!

We now know what functionality the SessionHandlerInterface interface promises, and we have a rough idea how the methods should interact with Redis, we can write our code. Without further ado, here’s the class:

<?php
class RedisSessionHandler implements SessionHandlerInterface
{
    public $ttl = 1800; // 30 minutes default
    protected $db;
    protected $prefix;

    public function __construct(PredisClient $db, $prefix = 'PHPSESSID:') {
        $this->db = $db;
        $this->prefix = $prefix;
    }

    public function open($savePath, $sessionName) {
        // No action necessary because connection is injected
        // in constructor and arguments are not applicable.
    }

    public function close() {
        $this->db = null;
        unset($this->db);
    }

    public function read($id) {
        $id = $this->prefix . $id;
        $sessData = $this->db->get($id);
        $this->db->expire($id, $this->ttl);
        return $sessData;
    }

    public function write($id, $data) {
        $id = $this->prefix . $id;
        $this->db->set($id, $data);
        $this->db->expire($id, $this->ttl);
    }

    public function destroy($id) {
        $this->db->del($this->prefix . $id);
    }

    public function gc($maxLifetime) {
        // no action necessary because using EXPIRE
    }
}

The first thing you might notice is that the open() and gc() methods are empty. I’ve made use of dependency injection to provide the Redis connection which opens the class up to unit testing, so nothing needs to be done in open(). Nothing needs to be done in gc() because Redis will handle expiring stale keys for us.

In addition to the Redis connection, the constructor also accepts a prefix. The prefix and the session ID generated by PHP are combined and used as the key for storing and retrieving values. This is primarily a means to prevent name collisions, but also provides the benefit that we’ll know what we’re looking at if we issue KEYS * in a Redis client.

When PHP calls the write() method, it passes two values: the session ID and a serialized string of session data. A SET command is used to store the data in Redis and we touch the key’s TTL. Any entries placed in the super global $_SESSION array are now stored.

When PHP calls the read() method, it passes the session ID. A GET command is used to retrieves the data and we also touch the TTL again – after all, if we’re accessing the session then it makes sense to consider it still fresh. Note that the session data is returned in its serialized form directly from storage. PHP receives the string, unserializes it, and populates the $_SESSION array.

Using our handler is as simple as creating an instance and passing that instance to session_set_save_handler().

<?php
$db = new PredisClient();
$sessHandler = new RedisSessionHandler($db);
session_set_save_handler($sessHandler);
session_start();

When session_start() is called, PHP will use our custom handler to manage sessions instead of its default approach; no other modification to our code is necessary.

If you’re stuck on a version of PHP earlier than 5.4, you can still use the class above (although you’ll have to either mock SessionHandlerInterface or remove the implements piece entirely). Its registration would look like this:

<?php
$db = new PredisClient();
$sessHandler = new RedisSessionHandler($db);
session_set_save_handler(
    array($sessHandler, 'open'),
    array($sessHandler, 'close'),
    array($sessHandler, 'read'),
    array($sessHandler, 'write'),
    array($sessHandler, 'destroy'),
    array($sessHandler, 'gc')
);
session_start();

Conclusion

In this article we’ve seen how easy it is to implement the SessionHandlerInterface interface and provide the logic needed by PHP to store session data in a Redis database. Custom session handling is transparent at the code level and the result is an exciting way to increase the security and flexibility of you application with very little effort! For more information, read what the PHP Manual has to say about custom session handling, and explore the accompanying code for this article on GitHub.

Image via Fotolia

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • maserg

    session.save_handler = redis
    session.save_path = “unix:///var/run/redis/redis.sock?persistent=1&weight=1&database=0″

  • kenlas

    You could use
    $this->ttl = ini_get(‘session.gc_maxlifetime’);
    instead of the hard-coded 1800 seconds.

    And redis has the atomic SETEX command to set the key and the expiration at the same time:
    $this->db->setEx($id, $this->ttl, $data);

  • pineyscripter

    Been doing some google searching and can’t find any “shared” hosting sites that mention support of redis ??? Why is that ?