Rosina Bignall

Creative Software Engineer

Making ZF2 and ZF1 Database Session Handling Work Together

| 0 comments

Background

I am developing an API as part of the next generation of the software I work on.  The current system is written using ZF1 (Zend Framework 1.12).  The API will be written using the Slim Framework.  The backend uses an Oracle database.  As a result, I am using Zend\Db\Table from ZF2 (Zend Framework 2) for accessing the database.  The fact that ZF2 is modular is allowing me to pull in the bits that I need, without taking all of Zend Framework, and it is turning out to be handy.

Database Sessions

Now that the database is out of the way, the next problem presented itself.  The ZF1 app stores the sessions in the (oracle) database as well.  So back to ZF2 I went and pulled in zend session handling. In composer.json:


{
   "repositories": [
       {
            "type": "composer",
            "url": "http://packages.zendframework.com/"
        }
    ]

    , "require": {
        , "zendframework/zend-db": "2.*"
        , "zendframework/zend-stdlib": "2.*"
        , "zendframework/zend-session": "2.*"
        , "zendframework/zend-eventmanager": "2.*"
    }
}

I add this to my Slim app:


$app->container->singleton('session', function () use ($app) {
    $options = $app->config('session');

    $config = new Zend\Session\Config\SessionConfig();
    $config->setOptions($options);

    $saveHandlerOptions = $app->config('session.saveHandler.options');
    $tableGateway = new Zend\Db\TableGateway\TableGateway($app->config('session.tablename'), $app->db);
    $saveHandler  = new Zend\Session\SaveHandler\DbTableGateway($tableGateway, new Zend\Session\SaveHandler\DbTableGatewayOptions($saveHandlerOptions));

    $manager = new Zend\Session\SessionManager($config);
    $manager->setSaveHandler($saveHandler);

    return $manager;
});
$app->session->start();

The database options are given by session.saveHandler.options in my config and defines the correct columns for the database table (Oracle requires all uppercase):


'session.saveHandler.options' => array(
    	'idColumn' => "ID"
    	, 'nameColumn' => 'NAME'
    	, 'modifiedColumn' => "MODIFIED"
    	, 'dataColumn' => "DATA"
    	, 'lifetimeColumn' => "LIFETIME"
	)		

Crash and burn! Low and behold, ZF2 and ZF1 handle sessions differently!

ZF1 and ZF2 Session Table Definitions and Differences

Specifically, ZF2 stores the Session Name and uses that plus the session id as the primary key.  ZF1 does NOT.  ZF1 uses only the session id.  You can see this in the different database definitions.

ZF1 Database definition:


 CREATE TABLE `session` (
    `id` char(32),
    `modified` int,
    `lifetime` int,
    `data` text,
    PRIMARY KEY (`id`)
 );
 

Which we had previously translated into Oracle as


 CREATE TABLE SYSTEM_SESSION (
    ID CHAR(32) NOT NULL ,
    MODIFIED INTEGER,
    LIFETIME INTEGER,
    DATA CLOB,
    CONSTRAINT SESSION_ID_PK PRIMARY KEY(ID)
 );
 

ZF2 Database definition:


 CREATE TABLE `session` (
    `id` char(32),
    `name` char(32),
    `modified` int,
    `lifetime` int,
    `data` text,
    PRIMARY KEY (`id`, `name`)
 );
 

Translated into Oracle as


 CREATE TABLE SYSTEM_SESSION (
    ID VARCHAR2(32) NOT NULL ,
    NAME VARCHAR2(32),
    MODIFIED INTEGER,
    LIFETIME INTEGER,
    DATA VARCHAR2(4000),
    CONSTRAINT SESSION_ID_PK
 );
 

You’ll notice 2 significant difference between ZF1 and ZF2 definitions.  And 3 additional differences in the Oracle translations:

  1. Addition of the NAME column in the ZF2 definition
  2. Primary Key is ID, NAME in ZF2
  3. CHAR(32) becomes VARCHAR2(32) – this really should have been VARCHAR2 from the start, but CHAR worked fine with ZF1, it does NOT work with ZF2 because ZF2 uses variable binding always whereas ZF1 does not for basic table access.  CHAR would need to be padded to 32 characters to work with binding.
  4. Change DATA column from CLOB to VARCHAR2 – ZF2 seems to handle CLOBs differently from how ZF1 handles them, so the data returned for the session was a reference to the CLOB not the value of the CLOB which then the ZF2 session couldn’t figure out so it just ignored it.  Eventually I’ll have to figure out CLOBs in ZF2, but for now I can ignore it since a VARCHAR2 will work just as well and be much faster.  If session data turns out to be more the 4,000 characters (the limit for Oracle VARCHAR2), then I’ll have to rethink this – or more likely rethink what is being stored in the session :).
  5. Primary Key for Oracle is only ID – this is to stay compatible with ZF1 since the whole point is to work with both applications.

The Oracle column type differences only serve to handle the differences between other databases and Oracle.  The bigger issue was the use of the Session Name.

Working around using the Session Name in the data store

Since compatibility with the ZF1 app is paramount here, I had to do some work to make the ZF2 session handling not worry about the NAME column.  So I started a ZF1 compatibility layer.  I extended Zend\Session\SaveHandler\DbTableGateway, replacing the read, write, and destroy methods.  This should work whatever your database is, but I have only tested it with our oracle database.  Of course, you’ll want to add the NAME column to your database as well and keep the primary key as only ID.  Here is the result:


php 
/**  
 * Based on Zend\Session\SaveHandler\DbTableGateway  
 *  
 * This allows you to use ZF1 session db handling and ZF2 session db handling together  
 *  
 * Note: ZF2 stores the session name, ZF1 does not.  This is the primary difference between the two.  
 * Therefore, this implementation ignores the session name for reading and only uses the ID.   
 * It does store the session name so it is available after the first time the session is updated.
 *  
 */ 
namespace Api\library\Zf1Compat\Zend\Session\SaveHandler; 

use Zend\Db\TableGateway\TableGateway; 
use Zend\Session\SaveHandler\DbTableGateway;

/**  
 * DB Table Gateway session save handler  
*/ class Zf1DbTableGateway extends DbTableGateway 
{     
   /**      
    * Read session data      
    *      
    * @param string $id      
    * @return string      
    */     
   public function read($id)     
   {         
       $rows = $this->tableGateway->select(array(
            $this->options->getIdColumn()   => $id,
//            $this->options->getNameColumn() => $this->sessionName,  -- ignore the name column because ZF1 doesn't use it
        ));

        if ($row = $rows->current()) {
            if ($row->{$this->options->getModifiedColumn()} +
                $row->{$this->options->getLifetimeColumn()} > time()) {
                return $row->{$this->options->getDataColumn()};
            }
            $this->destroy($id);
        }
        return '';
    }

    /**
     * Write session data
     *
     * @param string $id
     * @param string $data
     * @return bool
     */
    public function write($id, $data)
    {
        $data = array(
            $this->options->getModifiedColumn() => time(),
            $this->options->getDataColumn()     => (string) $data,
            $this->options->getNameColumn()     => $this->sessionName,
        );

        $rows = $this->tableGateway->select(array(
            $this->options->getIdColumn()   => $id,
//            $this->options->getNameColumn() => $this->sessionName,
        ));

        if ($row = $rows->current()) {
            return (bool) $this->tableGateway->update($data, array(
                $this->options->getIdColumn()   => $id,
//                $this->options->getNameColumn() => $this->sessionName,
            ));
        }
        $data[$this->options->getLifetimeColumn()] = $this->lifetime;
        $data[$this->options->getIdColumn()]       = $id;
        $data[$this->options->getNameColumn()]     = $this->sessionName;
        return (bool) $this->tableGateway->insert($data);
    }

    /**
     * Destroy session
     *
     * @param  string $id
     * @return bool
     */
    public function destroy($id)
    {
        return (bool) $this->tableGateway->delete(array(
            $this->options->getIdColumn()   => $id,
//            $this->options->getNameColumn() => $this->sessionName,
        ));
    }

}

If you compare this with the original Zend\Session\SaveHandler\DbTableGateway you’ll notice that the primary difference is to ignore the NAME column when looking up the session and use only the ID (in fact these lines are just commented out).  When writing the session to the database we include the NAME in the record being written.

Adding this to my Slim app is really simple, just one line of code difference


    $saveHandler  = new Api\library\Zf1Compat\Zend\Session\SaveHandler\Zf1DbTableGateway($tableGateway, new Zend\Session\SaveHandler\DbTableGatewayOptions($saveHandlerOptions));

ZF1 Compatibility Layer Package

As a result of all of this I decided to start a ZF1 compatibility package for ZF2. Find it on Github. Or add it to your project with composer:


"require": {
    "bignall/zf1compat-for-zf2": "dev-master"
}

Usage:


$tableGateway = new Zend\Db\TableGateway\TableGateway(...);
$saveHandler  = new ZF1CompatForZF2\Zend\Session\SaveHandler\Zf1DbTableGateway($tableGateway, new Zend\Session\SaveHandler\DbTableGatewayOptions($saveHandlerOptions));
$manager      = new Zend\Session\SessionManager();
$manager->setSaveHandler($saveHandler);

Any comments about this?  Thoughts? Ideas? Improvements?  Post a comment below 🙂

Author: Rosina Bignall

I've been on the internet since the 90's. Check out my professional blog at http://rosinabignall.com and my personal blog at http://rosina.me.

Leave a Reply

Required fields are marked *.