Team LiB
Previous Section Next Section

What Is a Custom Session Handler?

For those of you who don't know what a custom session handler is, refer to Chapter 6, which mentions a technique for storing session-related information in an arbitrary fashion. In PHP 5, there are three primary ways to store session information internally, which are defined by the session.save_handler configuration directive in your php.ini file: the file system (file), a SQLite database (sqlite), and WDDX (wddx). However, there is a fourth optionyou can define your own as well!

Defining Your Own Session Handler

When defining your own session handler, the basic idea is to create six functions that each handle one of the following conditions:

  • Opening the session

  • Closing the session

  • Reading session data

  • Writing session data

  • Destroying a session

  • Cleaning up old session data (garbage collection)

To implement your own session-handling system, you must implement functions for all five of these tasks. For the sake of simplicity, we'll call these five functions handler_open(), handler_close(), handler_read(), handler_write(), handler_destroy(), and handler_garbage(), respectively. It is important when you define these functions that they accept a specific set of parameters, as shown:

handler_open($path, $session_name)

$path is the path where the session information should be stored as specified in the php.ini file, and $session_name is the name of the session being created, as also specified in php.ini. This function is called when PHP begins a new session to give the session handler an opportunity to open any resources that might be needed. Returns true if the session was opened successfully or false on failure.

handler_close()

This function is called when the session has completed any writing that must be done and gives the handler an opportunity to release any resources no longer needed.

handler_read($session_id)

$session_id is the unique identifier for the current session. This function must return a string representing the session data read for the given session id (or an empty string if no data was found).

handler_write($session_id, $data)

$session_id is the unique identifier for the current session, whereas $data is the session data that must be stored for the current session. This function returns a Boolean true or false to indicate the success or failure of the write operation.

handler_destroy($session_id)

$session_id is the unique identifier for the current session, which should be destroyed and all its data removed when this function is called.

handler_garbage($max_lifetime)

This function is called at a frequency defined by the session.gc_probability configuration directive in php.ini and is used to clean out stale session data from expired sessions. $max_lifetime is a timestamp representing the maximum time a session should remain active without being used before it is considered expired.

These functions must each accomplish the task as described. As you will see in this section, they can be implemented either in a standard procedural fashion or as methods of a session-handling object. To register your custom session handler after the functions have been created, use the session_set_save_handler() function, which has the following syntax:

session_set_save_handler($open, $close, $read, $write, $destroy, $garbage);

Each of the six parameters to the session_set_save_handler() function represents the function that will be called for that particular operation. This function can either be represented as a string containing the procedural function to call, or a method within an object can be specified. When referencing a method within an object, an array is passed where the first entry is the instance of the object to call, and the second is a string containing the method name to call within that instance. Upon successful registration of the session handler, the session_set_save_handler() function returns a Boolean true or a false on failure.

The MySQLi Session Handler

Now that we have an idea of how to implement a custom session handler, let's take a look at how I will implement a custom MySQLi session handling system. For this system, I will create a class, MySQLiSession, which will represent our MySQL-based session. This class will contain a total of seven methods: the six handlers for each operation described in the previous section and a constructor. However, before we get into the class, let's take a look at the table that will store the following session data as a CREATE TABLE SQL statement:

CREATE TABLE session_data (id varchar(32) primary key,
                           data text,
                           last_updated timestamp);

This session_data table will be used to store all our session-related information.

With our table defined, the first step in the creation of our MySQLiSession handler is the constructor. The constructor is responsible for connecting to the database, registering its methods as the handlers for each session event, assigning the name of the session, and starting the session itself, as shown in Listing 24.15:

Listing 24.15. The Constructor for the MySQLiSession Class
<?php

class MySQLiSession {

    const USERNAME = "user";
    const PASSWORD = "secret";
    const HOST = "localhost";
    const DATABASE = "unleashed";
    const TABLE = "session_data";
    const SESS_NAME = "UNLEASHED";
    const SESS_EXPIRE = 3600;       /* Seconds */

    private $link;
    private $name;
    private $table;

    function __construct($user = null, $pass = null,
                         $host = null, $db = null,
                         $table = null, $sess_name = null) 
    {

        $user = (is_null($user)) ? self::USERNAME : $user;
        $pass = (is_null($pass)) ? self::PASSWORD : $pass;
        $host = (is_null($host)) ? self::HOST : $host;
        $db = (is_null($db)) ? self::DATABASE : $db;
        $this->table = (is_null($table)) ? self::TABLE : $table;
        $this->name = (is_null($sess_name)) ? self::SESS_NAME : $sess_name;

        $this->link = mysqli_connect($host, $user, $pass, $db);

        if(!$this->link) {
            throw new Exception("Could not connect to the database!");
            return;
        }

        mysqli_select_db($this->link, $db);

        session_set_save_handler(array($this, "handler_open"),
                                 array($this, "handler_close"),
                                 array($this, "handler_read"),
                                 array($this, "handler_write"),
                                 array($this, "handler_destroy"),
                                 array($this, "handler_garbage"));   

        session_name($this->name);
        session_start();

    }

    /* Remainder of class omitted */

}
?>

As you can see, the __construct() construction method is a very straightforward one. It is, however, important because without it the database link would never be established and the session itself would never be started.

Now that we have constructed the object, let's take a look at the actual session handlers that perform the work of the class. If you'll recall earlier, when I first introduced these handlers, you may have noted that some of them may not be necessary under every circumstance. For instance, the value of the session.save_path configuration directive passed to the "open" session-handling function is largely useless if the session data is not being saved to the file system. In fact, both the open and close handlers are trivial methods in our class, as shown in Listing 24.16:

Listing 24.16. The handler_open and handler_close Methods of the MySQLiSession Class
public function handler_open($path, $sess_name)
{
    $this->name = $sess_name;
    return true;              
}

public function handler_close()
{
    return true;
}

Although two of the six session-handler methods are trivial, the remaining four are responsible for the entirety of the remaining functionality of the session. The first two of these handlers are the read/write handlers, which are responsible for writing to and retrieving from the stored session data values associated with a given session. To begin, let's examine the handler_write() function, which is responsible for writing new or updated session data (see Listing 24.17):

Listing 24.17. The handler_write() Method of the MySQLiSession Class
public function handler_write($sess_id, $data)
{
    $query = "INSERT INTO {$this->table} (id, data)
               VALUES (?, ?) 
               ON DUPLICATE KEY 
               UPDATE data = ?, 
                      last_updated=NULL";

    $stmt = mysqli_prepare($this->link, $query);

    mysqli_bind_param($stmt, "sss", $sess_id, $data, $data);

    return mysqli_execute($stmt);

}

As you can see in Listing 24.17, the handler_write() function is basically a wrapper for a SQL query against the MySQL database. The query being executed is the following:

INSERT INTO <tablename> (id, data) VALUES(?, ?)
ON DUPLICATE KEY UPDATE data = ?, last_updated = NULL

This query is a rather interesting one because it takes advantage of a number of features available only in MySQL version 4.1 and later. To begin, from a PHP perspective this query is obviously designed to function as a prepared statement, as described earlier in the chapter. However, note the use of the ON DUPLICATE KEY condition. This condition allows us to write a single query that either creates a new row or updates an existing row if the data being inserted conflicts with a primary key. This is a particularly useful feature for a session handler, where the data must be inserted once per session and henceforth updated constantly. Using the ON DUPLICATE KEY condition, we are able to perform an insert or an update with a single query. Note, however, that because we have two independent placeholders in our query for a single column (one for if the insert succeeds; one for if an update occurs), our call to mysqli_bind_param() must provide two copies of the data as well.

Now that data is being written to the session table in the database, it must be read upon request from PHP. This process is handled in the reader function of the session handlerthe handler_read() function of our MySQLiSession class, as shown in Listing 24.18:

Listing 24.18. The handler_read() Function in the MySQLiSession Class
public function handler_read($sess_id)
{
    $query = "SELECT data 
                FROM {$this->table}         
               WHERE id = ?
                 AND UNIX_TIMESTAMP(last_updated) + " . 
                 self::SESS_EXPIRE . " > UNIX_TIMESTAMP(NOW())";

    $stmt = mysqli_prepare($this->link, $query);

    mysqli_bind_param($stmt, "s", $sess_id);

    if(mysqli_execute($stmt)) {
        mysqli_bind_result($stmt, $retval);

        mysqli_fetch($stmt);

        if(!empty($retval)) {
            return $retval;
        }

    }

    return "";
}

As was the case in the handler_write() function, for our MySQLiSession class the handler_read() function serves largely as a wrapper for a simple SQL query, as follows:

SELECT data FROM <tablename> 
   WHERE id = ?
     AND UNIX_TIMESTAMP(last_updated) + <expire> > UNIX_TIMESTAMP(NOW())

As the query responsible for retrieving valid session data from the database, it must meet two criteria. First, the session ID associated with the session must match the current session ID. Second, the data must have been updated within the specified time frame (the session cannot be expired). In this function we bind both a single parameter (the session ID) and then the resulting value (the actual session data matching our query). Because this function is expected to return a string representing the whole of the session data, it must return either the data from the database or an empty string on failure.

With opening, closing, and reading and writing of sessions and their corresponding data covered, it's now time to deal with removing the data from the database. In PHP, there are two ways sessions are destroyed when using a custom session handler. The first is by explicitly destroying the data by calling the session_destroy() function. When this function is called, the corresponding handler method handler_destroy() is called, shown in Listing 24.19:

Listing 24.19. The handler_destroy() Method of the MySQLiSession Class
public function handler_destroy($sess_id)
{
    $query = "DELETE FROM {$this->table} WHERE id = ?";

    $stmt = mysqli_prepare($this->link, $query);

    mysqli_bind_param($stmt, "s", $sess_id);

    return mysqli_execute($stmt);
}

As you can see, the handler_destroy() method is a very straightforward one that simply deletes a given row from the session data table identified by the session ID.

Although sessions can be explicitly destroyed and handled using the handler_destroy() method, what of the sessions that simply expire, which is a much more common situation? In these circumstances, PHP provides a custom session handler for something called "garbage collection." This function is called randomly at a frequency determined by the session.gc_probability php.ini configuration directive. When called, this function is expected to remove all stale and expired session records.

NOTE

Why is the garbage collection function called only at a random interval by PHP? The reason is because calling the function during every request is an incredibly expensive operation and would slow down your websites considerably. However, not deleting session data would surely result in a complete exhaustion of your hard drive space. The compromise is to call the function only at a random interval, slowing down one in many session requests.


The garbage collection handler method for the MySQLiSession class is called handler_garbage(), shown in Listing 24.20:

Listing 24.20. The handler_garbage() Method of the MySQLiSession Class
public function handler_garbage($max_life)
{
    $query = "DELETE FROM {$this->table} 
                    WHERE UNIX_TIMESTAMP(last_updated) + " .
                    self::SESS_EXPIRE . " <= UNIX_TIMESTAMP(NOW())";

    mysqli_query($this->link, $query);

    return;
}

Similar to the handler_destroy() method discussed in Listing 24.19, rather than deleting a single record based on a specific session ID, the handler_destroy() method deletes a variable number of session table rows based on the last_updated timestamp associated with each recordspecifically, the query:

DELETE FROM <tablename>
      WHERE UNIX_TIMESTAMP(last_updated) + <expire> <= UNIX_TIMESTAMP(NOW())

The result is a query that deletes all rows that have not been updated within the specified time frame. Also note that because we are allowing the user to specify an expiration in seconds, we must convert both the last_updated column (a DATETIME column) and the result of the MySQL function NOW() (which returns the current date/time in DATETIME format) into Unix timestamps representing the number of seconds since January 1, 1970 That is all there is to it! For your reference, Listing 24.21 contains the MySQLiSession class in its entirety so that you can see it as a single entity:

Listing 24.21. The Complete MySQLiSession Class
<?php
class MySQLiSession {

    const USERNAME = "user";
    const PASSWORD = "secret";
    const HOST = "localhost";
    const DATABASE = "unleashed";
    const TABLE = "session_data";
    const SESS_NAME = "UNLEASHED";
    const SESS_EXPIRE = 3600;       /* Seconds */

    private $link;
    private $name;
    private $table;

    function __construct($user = null, $pass = null,
                         $host = null, $db = null,
                         $table = null, $sess_name = null) 
    {

        $user = (is_null($user)) ? self::USERNAME : $user;
        $pass = (is_null($pass)) ? self::PASSWORD : $pass;
        $host = (is_null($host)) ? self::HOST : $host;
        $db = (is_null($db)) ? self::DATABASE : $db;
        $this->table = (is_null($table)) ? self::TABLE : $table;
        $this->name = (is_null($sess_name)) ? self::SESS_NAME : $sess_name;

        $this->link = mysqli_connect($host, $user, $pass, $db);

        if(!$this->link) {
            throw new Exception("Could not connect to the database!");
            return;
        }

        mysqli_select_db($this->link, $db);

        session_set_save_handler(array($this, "handler_open"),
                                 array($this, "handler_close"),
                                 array($this, "handler_read"),
                                 array($this, "handler_write"),
                                 array($this, "handler_destroy"),
                                 array($this, "handler_garbage"));   

        session_name($this->name);
        session_start();

    }

    public function handler_open($path, $sess_name)
    {
        $this->name = $sess_name;
        return true;              
    }

    public function handler_close()
    {
        return true;
    }

    public function handler_read($sess_id)
    {
        $query = "SELECT data 
                    FROM {$this->table}         
                   WHERE id = ?
                     AND last_updated + " . self::SESS_EXPIRE . " > NOW()";

        $stmt = mysqli_prepare($this->link, $query);

        mysqli_bind_param($stmt, "s", $sess_id);

        if(mysqli_execute($stmt)) {
            mysqli_bind_result($stmt, $retval);

            mysqli_fetch($stmt);

            if(!empty($retval)) {
                return $retval;
            }

        }

        return "";
    }

    public function handler_write($sess_id, $data)
    {
        $query = "INSERT INTO {$this->table} (id, data)
                   VALUES (?, ?) 
                   ON DUPLICATE KEY 
                   UPDATE data = ?, 
                          last_updated=NULL";

        $stmt = mysqli_prepare($this->link, $query);

        mysqli_bind_param($stmt, "sss", $sess_id, $data, $data);

        return mysqli_execute($stmt);

    }

    public function handler_destroy($sess_id)
    {
        $query = "DELETE FROM {$this->table} WHERE id = ?";

        $stmt = mysqli_prepare($this->link, $query);

        mysqli_bind_param($stmt, "s", $sess_id);

        return mysqli_execute($stmt);
    }

    public function handler_garbage($max_life)
    {
        $query = "DELETE FROM {$this->table} 
                        WHERE UNIX_TIMESTAMP(last_updated) + " .
                        self::SESS_EXPIRE . " <= UNIX_TIMESTAMP(NOW())";

        mysqli_query($this->link, $query);

        return;
    }

}
?>

To use our new session handler, create an instance of the MySQLiSession class in place of what would normally be a call to the session_start() function, as shown in Listing 24.22:

Listing 24.22. Using the MySQLiSession Class to Create MySQLi-Based Sessions
<?php
    require_once("mysqlisession.class.php");

    $sess = new MySQLiSession();

    if(!empty($_GET['reset'])) {
        $_SESSION['count'] = 0;
    }

    echo "Count is: ".@++$_SESSION['count'];
?>
&nbsp;[<A HREF="?<?php echo SID; ?>">increment</A>]
&nbsp;[<A HREF="?<?php echo SID; ?>&reset=1">reset</A>]

    Team LiB
    Previous Section Next Section