Jul 092011
 

I found myself in an environment where a framework wasn’t going to be option. We had a cluster of web servers and I needed to run sessions across a database. Sure, I could have just written a simple function for doing so, but I thought it would be better to PHP’s session_set_save_handler() function.

Using session_set_save_handler() will allow you preserve and use the built in PHP sessions functions and variables, while attaching your own custom class to handle anything from Database sessions, to reading file sessions from one source. You can even get fancy with things and write in your own custom tracking. There really are no limits.

The first thing you’ll need to do is design a class with a few specific public functions. Those are:

  • open
  • close
  • read
  • write
  • destroy
  • gc

I this lesson, we’ll be connecting our sessions to a database.  So I’ll need to also setup a constructor to handle the database connect, and I also like to throw in destructor and call a session_write_close().  There are also ini_set items you can throw at the top of your code to setup certain session items the way you like them:

ini_set( 'session.gc_probability', 50 );
ini_set( 'session.save_handler', 'user' );
ini_set( 'session.gc_maxlifetime', 28800 );

gc_probability is something I had to play around with. This is used to manage the probability that the garbage collection process is started. I found that 50 worked best for me.

save_handler simply defines the name of the handler. The default is ‘files’.

gc_maxlifetime tells PHP how many seconds old something is before it is considered garbage. I set our’s for 8 hours to accommodate the shift length of the people using the system I was building. They didn’t like to be logged out for idling. This is usually something you’ll want to match your company’s IT policy.

Let’s now take a look at the Sessions class.

class Session
{
    /**
     * a database connection resource
     */
    private $_sess_db;

    function __construct( $db )
    {
        $this->_sess_db = $db;

        return true;
    }

    function __destruct()
    {
        session_write_close();
    }

    /**
     * Open the session
     */
    public function open()
    {
        return true;
    }

    /**
     * Close the session
     */
    public function close()
    {
        return true;
    }

    /**
     * Read the session
     */
    public function read( $id )
    {
        $sql  = "EXEC GetSessionData @SessionID=N'" . $id . "'";

        $results = $this->_sess_db->prepare( $sql );
        $data    = $this->_sess_db->fetchrow( $results );

        return $data['SessionData'];
    }

    /**
     * Write the session
     */
    public function write( $id, $data )
    {
        $sql = "EXEC WriteSession @SessionID = N'" . $id . "', @SessionData = N'" . $data . "'";
        $res  = $this->_sess_db->prepare( $sql );

        return true;
    }

    /**
     * Destoroy the session
     */
    public function destroy( $id )
    {
        $sql = "EXEC DestroySession @SessionID = N'" . $id . "'";

        if ( $res = $this->_sess_db->execute( $sql ) )
        {
            return true;
        }
        else
        {
            return false;
        }
    }

    /**
     * Garbage Collector
     */
    public function gc( $max )
    {
        $back = date( "Y-m-d H:m:s A", mktime( date("H")-(($max/60)/60) , 0, 0, date("m"), date("d"), date("Y") ) );

        $sql = "EXEC DeleteOldSessions @Timeout = N'" . $back . "'";

        $this->_sess_db->execute( $sql );

        return true;
    }
}

I wrote a database class for this particular system, so I have passed that into my constructor. You can do the same, or simply put your database connection code in there. I used the tables ‘SessionID, SessionData, TimeIn, LastActivity, and TimeOut’. You can use most any method you want, as long as you track your data and the PHP Session ID, and Time In and Out columns. I used LastActivity to determine if the session had been abandoned or not.

As you go through the class, you’ll quickly notice that my open() and close() functions do nothing more than return true. You can put custom code in here to track things or log things if you want. You can even have it e-mail you a picture of a duck. The point is, these functions are all open for you to customize how you see fit. I’d suggest playing around with them to get a feel for what you can do.

Now onto the read and write functions. PHP is always going to handle the Session ID. It will pass it into these functions so you can use them. For me, I was using it to plug into a Stored Procedure to read and write my sessions. In here you can do the same, or read/write to flat files, or any other method you maybe want/need to use.

Next is destroy. This is where end a session. For my particular destroy, I would simple add a timestamp to the TimeOut field in my database.

Finally my gc function. This would do some math to see how long a session has been closed for, or idling for, and based on that, I would erase the session from the database. You can choose to leave the sessions in for as you’d like. Perhaps you want to use the Sessions table to track history, and make it a much more beefy database table. That’s fine, but in my application, we did activity tracking in a different place. Either way is fine.

Now, how do I tell PHP to use this?

session_set_save_handler( array( $session, 'open'    ),
                          array( $session, 'close'   ),
                          array( $session, 'read'    ),
                          array( $session, 'write'   ),
                          array( $session, 'destroy' ),
                          array( $session, 'gc'      )  );

You’ll see that each function included is in an array, with a variable before its name. That variable is my session class. $session = new Session( $db );

From here its pretty much PHP 101. session_start() to start your session on each page. The $_SESSION array will still hold all of your session data. This makes things nice if you ever have to revert back to standard sessions, and ditch your class and handler. You won’t have to rewrite all of your session handling code. You’ll just have to remove your class instantiation, and your session_set_save_handler call.

So if you find yourself in a situation where you can’t use a framework, or don’t want to use a framework, but still need a way to better control your sessions, or through them into a database, consider the session_set_save_handler method.