Part 3 – making sessions work with database using Zend_Db_Table

Please, note that this text is written in May 2008. Since then Zend Framework have evolved and now provides Zend_Application class. It is good idea to use it. The user registration and login stuff, presented here is still actual though. Also there are many other ideas presented, which I hope will be usefull for the readers.

Before we start with the new stuff for today – we should fix what we have found to be wrong in the first 2 parts :) In our bootstrap.php file just before sending the response to the client we set a header:

$response->setHeader('Content-Type', 'text/html; charset=UTF-8', true);

Thanks to Luke Richards who pointed that if we later want to return different content – JSON, XML or something we are stuck with this header. In fact having

<meta http-equiv="content-type" content="text/html;charset=utf-8" />

in the layout template is enough to make sure our content is displayed correctly in the client browser, so we just drop the

$response->setHeader('Content-Type', 'text/html; charset=UTF-8', true);

line from our bootstrap.php.

Now to the new stuff – as planned earlier now I want to implement session handler, which works with MySQL database and not with files. Two are the main reasons we want this – security and scalability. The security part – when in files, anyone with access to the webserver filesystem can steal or even manipulate session data. And while this is not an issue when we manage our own web server – the other reason – scalability applies strong here. If we ever need to have two or more webservers serving our application because of the great amount of visitors we have – we will need to have a solution for the sessions. Sticky sessions are one solution to this, but not guarantee to us that the servers will be equally loaded. Zend are offering session management in their application server – this is another solution. Storing the sessions in common cache cluster – Memcached for example – is also an option. We will take different approach and just put our session data in the database.
session_set_save_handler function makes this possible.

We will need to construct a table for the session info – here is the SQL statement for it:

CREATE TABLE `session` (
  `session_id` varchar(32) NOT NULL,
  `session_data` text NOT NULL,
  `t_created` datetime NOT NULL,
  `t_updated` datetime NOT NULL,
  PRIMARY KEY  (`session_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

In t_created field we will save the timestamp when the session is first created and in t_updated we will save the timestamp when the session is last updated (last accessed).
EDIT: (My_Session_Manager used to be with static functions and we used direct session_set_save_handler function.)
To comply with Zend Framework convnetions we will write a class My_Session_Manager which implements Zend_Session_SaveHandler_Interface – defining functions for open, close, read, write, destroy and gc handlers. If we want our class to be loadable from Zend_Loader we should follow Zend naming convention, so we create directory ‘library/My/Session‘ and put in there file called Manager.php, so:
library/My/Session/Manager.php

< ?php
 
require_once 'Zend/Session/SaveHandler/Interface.php';
 
class My_Session_Manager implements Zend_Session_SaveHandler_Interface
{
    /**
     * This is instance of My_Session_data, which extends Zend_Db_Table and manages the database connection
     *
     * @var My_Session_Data
     */
    private static $sessionData;
 
    private static $thisIsOldSession = false;
	private static $originalSessionId = '';
 
    public function open($save_path, $name)
    {
        self::$sessionData = new My_Session_Data();
        return true;
    }
 
    public function close()
    {
 
        return true;
    }
 
    public function read($id)
    {
        $rows = self::$sessionData->find($id);
        $row = $rows->current();
        if ($row)
        {
            self::$thisIsOldSession = true;
			self::$originalSessionId = $id;
            return $row->session_data;
        }
        else
        {
            return '';
        }
    }
 
    public function write($id, $sessionData)
    {
        $data = array
        (
            'session_data' => $sessionData,
            't_updated' => new Zend_Db_Expr('NOW()'),
        );
 
		if (self::$thisIsOldSession &amp;&amp; self::$originalSessionId != $id)
        {
            // session ID is regenerated, so set $thisIsOldSession to false, so we insert new row
            self::$thisIsOldSession = false;
        }
 
        if (self::$thisIsOldSession)
        {
            self::$sessionData->update
            (
            $data,
            self::$sessionData->getAdapter()->quoteInto('session_id = ?', $id)
            );
        }
        else
        {
            //no such session, create new one
            $data['session_id'] = $id;
            $data['t_created'] = new Zend_Db_Expr('NOW()');
            self::$sessionData->insert($data);
        }
 
        return true;
    }
 
    public function destroy($id)
    {
        self::$sessionData->delete(self::$sessionData->getAdapter()->quoteInto('session_id = ?', $id));
        return true;
    }
 
    public function gc($maxLifetime)
    {
        $maxLifetime = intval($maxLifetime);
        self::$sessionData->delete("DATE_ADD(t_updated, INTERVAL $maxLifetime SECOND) < NOW()");
        return true;
    }
}

and may be you have noticed we here use class My_Sessoin_Data – this is very simple class indeed – look at its declaration:

library/My/Session/Data.php‘:

< ?php
 
class My_Session_Data extends Zend_Db_Table_Abstract
{
    protected $_name = 'session';
}

so, what's happening here? My_Session_Data extends Zend_Db_Table_Abstract and we use it to access our sessions table in our database. My_Session_Manager defines several static methods - our session handlers. They use the functionality provided by Zend_Db_Table to perform the SQL commands to the database, I won't explain in details, because in Zend_DB chapter in the Reference Guide this is explained better than I could do it :)
open() just creates new object from My_Session_Data and saves it to private member variable of the class. close() removes the reference to the My_Session_Data object just returns true. read() is also very simple - we search in the database table for row with the id of the current session. If we find such - we return the data written there. Else - we return empty string. write() is the most complex method here, although not big deal either - we construct $data array, holding the data we want to update in the database table and then we call the update() method of My_Session_Data.

self::$sessionData->getAdapter()->quoteInto('session_id = ?', $id)

is quite clumsy way to state that we want to update the row where session_id is equal to $id, but I couldn't think of shorter and better :( - If someone have some better idea I'll be happy to hear it.
If there is no updated row, then we do not have such session - then we just create it. We add to the $data array info about the primary key - session_id and when the session is created - NOW(). BUGFIX here - in the old code we checked for the number of updated rows from the update query, which we ran every time - and if 0 rows were updated we inserted the new session. But if the session is in fact old and saved again in the same second, so 't_updated' is still the same - then 0 rows are updated in spite of being old session. So we have a fix here - we introduce static variable $thisIsOldSession which is set to true in read() if session is found - then in write() we use this to determine if we should do update or insert.
destroy() should delete a session, so we delete the row in the database about it. And finally - the garbage collector gc() - here we delete all rows that are not updated more than $maxLifetime seconds. And that's all. So now how we make that php uses our class for session handling?
We go back in our bootstrap.php file, and right after we have initialized the database connection (we need it for the session management) we add some code, telling the Zend_Session to use our just built session manager class:

// Construct the database adapter class, connect to the database and store the db object in the registry
$db = Zend_Db::factory($configuration->db);
$db->query("SET NAMES 'utf8'");
Zend_Registry::set('db', $db);
// set this adapter as default for use with Zend_Db_Table
Zend_Db_Table_Abstract::setDefaultAdapter($db);
 
// Now set session save handler to our custom class which saves the data in MySQL database
$sessionManager = new My_Session_Manager();
Zend_Session::setOptions(array(
    'gc_probability' => 1,
    'gc_divisor' => 5000
    ));
Zend_Session::setSaveHandler($sessionManager);

We can fine tune the garbage collector, but I think 1 in 5000 page hits is good enough. The value of 1440 seconds for max lifetime of sessions is the default one - we can change this too if needed.

Now it _should_ work. Let's test it. Create new action controller:
'application/modules/default/controllers/TestController.php':

< ?php
 
class TestController extends Zend_Controller_Action
{
    public function indexAction()
    {
        $defaultNamespace = new Zend_Session_Namespace('Default');
        if (isset($defaultNamespace->numberOfPageRequests)) {
            $defaultNamespace->numberOfPageRequests++; // this will increment for each page load.
        }
        else
        {
            $defaultNamespace->numberOfPageRequests = 1; // first time
        }
 
        $this->view->xxx = $defaultNamespace->numberOfPageRequests;
    }
}

and template script for the index action:
'application/modules/default/views/scripts/test/index.phtml':

< ?php echo "Page requests this session: ", $this->xxx; ?>

Now when we reload several times 'http://localhost/test' we see that our session handler works.

See you again in Part 4. Here is the code so far: part3.rar

Be Sociable, Share!