Sunday, January 27, 2008

Try Zend Framework vol.11 "Save session data into DB"

How do you handle session in your site? Usually you may not need to change it from default.
Let's think your site is on the several web servers and need to use unique session. You probably want to store session into database.
Zend Framework provides interface of session save handler. so you just need to implement the interface when you create your own session save handler.
Then I tried to write my own session save handler here.
But before writing it, I draw upon Zend Developer Zone tips below.

Trick-Out Your Session Handler
To know how to write session save handler.

Zend_Session_SaveHandler_DbTable
To know the concept of the session save handler for Zend Framework.
But I did not implement all of them but some of them.

According to "Trick-Out Your Session Handler", I should write opening database connection in "open()" method, and closing database connection in "close()" method. But in my class I just made it return "true". :P

First, we are going to store session into this table(in this case I used PostgreSQL, you may change timestamp to datetime with MySQL)
CREATE TABLE sessions (
id char(32) default NULL,
lifetime timestamp default NULL,
data text
);

OK, here this is a session save handler of database.
library/Hoge/Session/SaveHandler/Db.php
<?php
require_once 'Zend/Db/Table/Abstract.php';

/**
 * Hoge_Session_SaveHandler_Db
 */
class Hoge_Session_SaveHandler_Db extends Zend_Db_Table_Abstract implements Zend_Session_SaveHandler_Interface
{
    /**
     * constatns
     */
    // session table name
    const TABLE_NAME         = 'sessions';
    // primary key column name
    const COLUMN_PRIMARY_KEY = 'id';
    // lifetime column name
    const COLUMN_LIFETIME    = 'lifetime';
    // data column name
    const COLUMN_DATA        = 'data';

    /**
     * primary key
     * @var string
     */
    protected $_primary = self::COLUMN_PRIMARY_KEY;

    /**
     * table name
     * @var string
     */
    protected $_name = self::TABLE_NAME;

    /**
     * table columns.
     * @var array
     */
    protected $_columnMap = array(
        self::COLUMN_PRIMARY_KEY => self::COLUMN_PRIMARY_KEY,
        self::COLUMN_LIFETIME    => self::COLUMN_LIFETIME,
        self::COLUMN_DATA        => self::COLUMN_DATA
    );

    /**
     * session maxlifetime
     * @var null|intger
     */
    protected $_lifetime = null;

    /**
     * constructor
     * @param string $table      Session table name
     * @param array  $columnMap  Session table column names
     */
    public function __construct($table = null, $columnMap = null)
    {
        // set table name
        if ($table) {
            $this->_name = $table;
        }

        if ($columnMap) {
            // set primary key name
            if (isset($columnMap[self::COLUMN_PRIMARY_KEY])) {
                $this->_columnMap[self::COLUMN_PRIMARY_KEY] = $columnMap[self::COLUMN_PRIMARY_KEY];
                $this->_primary = $columnMap[self::COLUMN_PRIMARY_KEY];
            }
            // set lifetime column name
            if (isset($columnMap[self::COLUMN_LIFETIME])) {
                $this->_columnMap[self::COLUMN_LIFETIME] = $columnMap[self::COLUMN_LIFETIME];
            }
            // set session data column name
            if (isset($columnMap[self::COLUMN_DATA])) {
                $this->_columnMap[self::COLUMN_DATA] = $columnMap[self::COLUMN_DATA];
            }
        }

        parent::__construct();
    }

    /**
     * Set session max lifetime.
     * @param $lifetime
     */
    public function setLifetime($lifetime)
    {
        $this->_lifetime = $lifetime;
    }

    /**
     * Open Session - retrieve resources
     *
     * @param string $save_path
     * @param string $name
     */
    public function open($save_path, $name)
    {
        return true;
    }

    /**
     * Close Session - free resources
     *
     */
    public function close()
    {
        return true;
    }

    /**
     * Read session data
     *
     * @param string $id
     * @return string
     */
    public function read($id)
    {
        $return = '';
        $where = $this->getAdapter()->quoteInto($this->_columnMap[self::COLUMN_PRIMARY_KEY] . "=?", $id);
        if ($row = $this->fetchRow($where)) {
            $return = $row->{$this->_columnMap[self::COLUMN_DATA]};
        }
        return $return;
    }

    /**
     * Write Session - commit data to resource
     *
     * @param string $id
     * @param mixed $data
     * @return bool
     */
    public function write($id, $data)
    {
        $return = false;
        $dataSet = array(
            $this->_columnMap[self::COLUMN_PRIMARY_KEY] => $id,
            $this->_columnMap[self::COLUMN_LIFETIME]    => date("Y-m-d H:i:s", mktime()),
            $this->_columnMap[self::COLUMN_DATA]        => $data
        );
        $where = $this->getAdapter()->quoteInto($this->_columnMap[self::COLUMN_PRIMARY_KEY] . "=?", $id);

        if ($this->fetchRow($where)) {
            $return = ($this->update($dataSet, $where)) ? true : false;
        } else {
            $return = ($this->insert($dataSet)) ? true: false;
        }

        return $return;
    }

    /**
     * Destroy Session - remove data from resource for
     * given session id
     *
     * @param string $id
     * @return bool
     */
    public function destroy($id)
    {
        $where = $this->getAdapter()->quoteInto($this->_columnMap[self::COLUMN_PRIMARY_KEY] . "=?", $id);
        return ($this->delete($where)) ? true : false;
    }

    /**
     * Garbage Collection - remove old session data older
     * than $maxlifetime (in seconds)
     *
     * @param int $maxlifetime
     * @return bool
     */
    public function gc($maxlifetime)
    {
        $lifetime = ($this->_lifetime) ? $this->_lifetime : $maxlifetime;
        $expiry = date("Y-m-d H:i:s", mktime() - $lifetime);
        $where = $this->getAdapter()->quoteInto($this->_columnMap[self::COLUMN_LIFETIME] . "<=?", $expiry);
        return ($this->delete($where)) ? true : false;
    }
}

Well, Let's use that session save handler.
Basically all you need to do is initialize that save handler and set it to Zend_Session_SaveHandler.

www/index.php
require_once 'Hoge/Session/SaveHandler/Db.php';
$saveHandler = new Hoge_Session_SaveHandler_Db();
Zend_Session::setSaveHandler($saveHandler);

If you want to change table name and column name, set it on constructor.
$table = 'hoge';
$columns = array('id'=>'foo', 'lifetime'=>'bar', 'data'=> 'buz');
$saveHandler = new Hoge_Session_SaveHandler_Db($table, $columns);

If you want to set session max life time, use lifetime setter.
$saveHandler = new Hoge_Session_SaveHandler_Db();
$saveHandler->setLifetime(60*60*12);

That's it. But Zend Framework release version 1.5 soon or later.
It may includes "Zend_Session_SaveHandler_DbTable".
May be it much flexible and useful :)
But I didn't want to wait it. Just wanted to write it like somebody talked to me "Don't wait somebody write code, just write as you need!" :)
(weight 88.6kg BMI 31%)

Labels:

Saturday, January 12, 2008

Try Zend Framework vol.10 "Mailing an error log"

Zend Framework has great logging system like "Zend_Log_Writer_Stream", "Zend_Log_Writer_Db".
But if some trouble happens, you might want to know it ASAP or don't want to know it forever, don't you :P

Let's make a log writer for mail first.

library/Hoge/Log/Writer/Mail.php
<?php
/** Zend_Log_Writer_Abstract */
require_once 'Zend/Log/Writer/Abstract.php';

/** Zend_Log_Formatter_Simple */
require_once 'Zend/Log/Formatter/Simple.php';

class Hoge_Log_Writer_Mail extends Zend_Log_Writer_Abstract
{
/**
* Mailer
* @var Zend_Mail
*/
private $_mail;

/**
* Class constructor
*
* @param Zend_Mail $mail Mail instance
*/
public function __construct($mail)
{
$this->_mail = $mail;
$this->_formatter = new Zend_Log_Formatter_Simple();
}

/**
* Write a message to the log.
*
* @param array $event event data
* @return void
*/
protected function _write($event)
{
$line = $this->_formatter->format($event);
$this->_mail->setBodyText($line);
$this->_mail->send();
}

}


Settings for Mail Logger.
www/index.php
<?php
require_once 'Zend/Registry.php';
require_once 'Zend/Mail.php';
require_once 'Zend/Log.php';
require_once 'Hoge/Log/Writer/Mail.php';

// Create mail object.
$mail = new Zend_Mail();
$mail->addTo('foo@example.com', 'System members');
$mail->setSubject('Error Occured!');
$mail->setFrom('foo@example.com');

// Set writer
$writer = new Hoge_Log_Writer_Mail($mail);

// Set logger and writer
$logger = new Zend_Log();
$logger->addWriter($writer);

// Set in registry
Zend_Registry::set('logger', $logger);


To use it. Get logger from registry.
application/controllers/HogeController.php
<?php
class HogeController extends Zend_Controller_Action
{
function loggerAction()
{
$logger = Zend_Registry::get('logger');
$logger->log('Information log', Zend_Log::INFO);
$logger->emerg('Emerg log');
exit;
}
}


Ok, that's it. From now, you cannot escape from error mail!
If you don't want to get error mail just create system without bugs!! :)
(weight 88.7kg BMI 30%)

Labels:

Thursday, January 03, 2008

CakePHP 1.2 is released!!

Yahoooooooooooooooooooooooooooooooooooooo!!

CakePHP 1.2 is released today! :p

And so happy to forget how fatty I am!!
(weight I don't kg BMI care%)

Labels:

Tuesday, January 01, 2008

Try Zend Framework vol.9 "Adding Json method on Rowset class"

Recently I started to study javascript and it's pretty interesting to use ajax.
When you deal with ajax, you probably want to get data as JSON. Don't you?

Zend Framework has data set handling class which is called
"Zend_Db_Table_Row" and "Zend_Db_Table_Rowset". And they have data conversion method "toArray()"

If they have JSON conversion method like "toJson()", it's helpful. isn't it? :)

Ok, Let's say you have data like below. We are going to convert this data to JSON.
mysql> select * from data;
+----+---------+
| id | data |
+----+---------+
| 1 | data 1 |
| 2 | data 2 |
| 3 | data 3 |
| 4 | data 4 |
| 5 | data 5 |
+----+---------+


Add a new method toJson() on Row class.
library/Hoge/Db/Table/Row.php
<?php
/**
* @see Zend_Db_Table_Row
*/
require_once 'Zend/Db/Table/Row.php';

class Hoge_Db_Table_Row extends Zend_Db_Table_Row
{
/**
* Returns the column/value data as json.
*
* @return string
*/
public function toJson()
{
require_once 'Zend/Json.php';
return Zend_Json::encode($this->_data);
}
}


For Rowset class.
library/Hoge/Db/Table/Rowset.php
<?php
/**
* @see Zend_Db_Table_Rowset
*/
require_once 'Zend/Db/Table/Rowset.php';

class Hoge_Db_Table_Rowset extends Zend_Db_Table_Rowset
{
/**
* Returns all data as a json.
*
* @return string
*/
public function toJson()
{
require_once 'Zend/Json.php';
return Zend_Json::encode($this->_data);
}
}


Ok, Let's use these extended classes.
There are several ways to specify these new classes.

1.Set names of these classes via setter methods.
application/controllers/HogeController.php

function jsonAction() {
require_once 'Hoge/Db/Table/Data.php';
$data = new Data();
$data->setRowClass('Hoge_Db_Table_Row');
$data->setRowsetClass('Hoge_Db_Table_Rowset');
$res = $data->fetchAll()->toJson();
print_r($res);
exit;
}


2.Set names of these classes in the Table_Abstract class.
library/Hoge/Db/Table/Abstract.php
<?php
require_once 'Zend/Db/Table/Abstract.php';

abstract class Hoge_Db_Table_Abstract extends Zend_Db_Table_Abstract
{

public function __construct()
{
$config = array(
'rowClass' => 'Hoge_Db_Table_Row',
'rowsetClass' => 'Hoge_Db_Table_Rowset'
);
parent::__construct($config);
}
}


To use toJson() method. The usage is same as toArray() method.
application/controllers/HogeController.php

function jsonAction()
{
require_once 'Hoge/Db/Table/Data.php';
$data = new Data();
$res = $data->fetchAll()->toJson();
print_r($res);
exit;
}


It returns JSON data like below.
[{"id":"1","data":"data 1"},{"id":"2","data":"data 2"},{"id":"3","data":"data 3"},{"id":"4","data":"data 4"},{"id":"5","data":"data 5"}]

(weight 89.0kg BMI 31%)

Labels: