Page 1 of 2

Session Handler and Registry

Posted: Mon Aug 21, 2006 3:36 am
by Luke
I built this session class in response to a recent thread. This is my first attempt at handling my own session data storage. Much of the session_set_save_handler functionality was borrowed from Jenk's post in that thread. If anything I did was wrong, please correct me. Recommendations, advice, etc. are welcome. Also, none of these classes have been tested... because I have never tested any classes before. I am going to attempt to test them myself, but if I have questions, I intend on asking them in this thread. Thanks for any help given!

Code: Select all

<?php
/**
* Registry class for storing just about anything
*
* This class is basically just a wrapper for an associative array. It
* can be used to store any type of data that an associative array can.
* The most common use for this class is to extend it to allow storage
* in almost any type of class that needs to store data such as
* a session class, an object registry, etc.
*
* @abstract
* @author       Luke Visinoni <luke.visinoni@gmail.com>
* @copyright    Luke Visinoni August 20th 2006
*/
class Registry{

	/**
	* Container for all registered data passed to this class
	* @access protected
	*/
	protected $entries = array();
	
	/**
	* Add a value to registry data
	* @access public
	* @param string $key 	Associative data array key
	* @param mixed	$value	Associative data array value
	*/
	public function register($key, $value){
		$this->entries[$key] = $value;
	}
	
	/**
	* Remove a value from registry data
	* @access public
	* @param string $key 	Associative data array key
	*/
	public function unregister($key){
		unset($this->entries[$key]);
	}
	
	/**
	* Retrieve a value from registry data
	* @access public
	* @param string $key 	Associative data array key
	* @return mixed
	*/
	public function get($key){
		return isset($this->entries[$key]) ? $this->entries[$key] : null;
	}
	
	/**
	* Check that a key exists within registry data
	* @access public
	* @param string $key 	Associative data array key
	* @return bool
	*/
	public function has($key){
		return $name ? isset($this->entries[$key]) : false;
	}
	
	/**
	* Destroy all information stored in this registry
	* @access public
	*/
	public function flush(){
		$this->entries = array();
	}
	
	/**
	* Magic wrapper for get()
	* @access public
	* @param string $key 	Associative data array key
	* @return mixed
	*/
	/*
	public function __get($key){
		return $this->get($key);
	}*/
	
	/**
	* Magic wrapper for register()
	* @access public
	* @param string $key 	Associative data array key
	* @param string $value 	Associative data array value
	*/
	/*
	public function __set($key, $value){
		$this->register($key, $value);
	}*/
}

/**
* Session handler
*
* A class for working with a session. This class is extended
* to provide a particular object or area of an application with its
* own namespace within the session data. This allows for different
* classes to play nicely together and not overwrite or delete session
* data they aren't supposed to.
*
* @author       Luke Visinoni <luke.visinoni@gmail.com>
* @copyright    Luke Visinoni August 20th 2006
*/
class Session extends Registry{
	
	/**
	* Container for session id
	* @access protected
	* @var string
	* @static
	*/
	protected static $id = null; 
	
	/**
	* This particular instance's namespace
	* @access public
	* @var string
	*/
	protected $namespace;			
	
	/**
	* Set to true if you would like for session's id to be regenerated
	* @access protected
	* @var bool
	*/
	protected $regenerate;
	
	/**
	* Constructor
	* @param string $namespace
	* @param bool $regenerate
	*/
	public function __construct($namespace='Session', $regenerate=null) {
		
        $this->namespace = $namespace;
        $this->regenerate = $regenerate;
		$this->start();
		
    }
    
	/**
	* Initialize the session
	* 
	* This method starts the session. Once the session is started, it tries to correct
	* a known IE problem. (Need to find out more about this problem). Now it checks
	* regenerates the session id, if specefied, and associates entries with our session.
	* 
	* @access public
	* @todo research ie problem that the session_cache_limiter is supposed to fix
	*/
    public function start(){
		if(session_id() == ''){
			session_start();
			if (strstr($_SERVER['HTTP_USER_AGENT'], 'MSIE')){
				session_cache_limiter('must-revalidate');
			}
			self::$id = session_id();
		}
		if ($this->regenerate){
			session_regenerate_id();
		}
		$this->entries =& $_SESSION[$this->namespace];
    }
    
	//public function __destruct(){
	//	session_write_close();
	//}
	
	public function __get($key){
		return $this->get($key);
	}
	
	public function __set($key, $value){
		$this->register($key, $value);
	}
}

interface SessionSaveHandler{
	public static function open();
	public static function close();
	public static function read($id);
	public static function write($id, $data);
	public static function destroy($id);
	public static function clean($max);
}

/**
* Session Handler for storing data in a mysql database
*
* @author       Luke Visinoni <luke.visinoni@gmail.com>
* @copyright    Luke Visinoni August 20th 2006
* @todo adjust session_set_save_handler methods so that database type and other factors may be dealt with before write/read/etc.
* @todo Allow for use with other than mysql
*/
class MysqlSession extends Session implements SessionSaveHandler{
	
	/**
	* Holds a mysql connection resource link
	* @access private
	* @var mysql connection resource link
	* @todo Set up error handling to use exceptions
	*/
	private static $db;
	
	public function __construct($namespace, $regenerate, $db){
		if(is_null($namespace)) $namespace = 'Session';
		if(is_null($regenerate)) $regenerate = false;
		if(!is_resource($db)){
			// I still need to learn exception handling, but this works for now.
			die('Third parameter must be a valid mysql resource link.');
		}
		self::$db =  $db;
		session_set_save_handler(array(&$this,"open"),
								 array(&$this,"close"),
								 array(&$this,"read"),
								 array(&$this,"write"),
								 array(&$this,"destroy"),
								 array(&$this,"clean"));
		//register_shutdown_function('session_write_close');
		parent::__construct($namespace, $regenerate);
	}
	public static function open(){
		return self::$db;
	}
	public static function close(){
		// No need to close my database connection, since it is shared with the rest of the app
		return true;
	}
	
	/**
	* Reads data from the data source
	* @access public
	* @static
	* @returns string
	*/
	public static function read($id){
		$id = mysql_real_escape_string($id, self::$db); 
	 
		$sql = "SELECT `data` 
				FROM   `session` 
				WHERE  `session_id` = '" . $id . "'"; 
	 
		if ($result = mysql_query($sql, self::$db)) { 
			if (mysql_num_rows($result)) { 
				$record = mysql_fetch_assoc($result); 
				return $record['data']; 
			} 
		} 
		return ''; 
	}
	
	/**
	* Writes serialized data to data source
	* @access public
	* @static
	* @returns bool
	*/
	public static function write($id, $data){
		$access = time(); 
	 
		$id = mysql_real_escape_string($id, self::$db); 
		$access = mysql_real_escape_string($access, self::$db); 
		$data = mysql_real_escape_string($data, self::$db); 

		$sql = "REPLACE 
				INTO `session` (`session_id`, `access`, `data`)
				VALUES('" . $id . "', " . $access . ", '" . $data . "')";
		return mysql_query($sql, self::$db);
	}
	
	/**
	* Delete session data associated with this id
	* @access public
	* @static
	* @returns bool
	*/
	public static function destroy($id){
		$id = mysql_real_escape_string($id, self::$db); 
	 
		$sql = "DELETE 
				FROM   `session` 
				WHERE  `session_id` = '" . $id . "'"; 
	 
		return mysql_query($sql, self::$db);
	}
	
	/**
	* Delete old session data
	* @access public
	* @static
	* @returns bool
	*/
	public static function clean($max){
		$old = time() - $max; 
		$old = mysql_real_escape_string($old, self::$db); 
	 
		$sql = "DELETE 
				FROM   `session` 
				WHERE  `access` < '" . $old . "'"; 
	 
		return mysql_query($sql, self::$db);
	}
}



// Usage
$DB = new Database('mysql://user:xxxx@localhost/my_database');
// This is kind of a fix for the optional params problem... 90% of the time, I would supply the first argument anyway, so I think this will work just fine
$Session = new MysqlSession(null, null, $DB->getLink());
$Session->register('foo', 'bar');
$Session->magic = "This variable was magically registered";
?>

Posted: Mon Aug 21, 2006 3:43 am
by Jenk
I'm not too sure on the internals of session_set_save_handler, but you may want to change the array params to:

Code: Select all

array(get_class($this) => 'write')
as the session handling stuff happens after all objects have been destroyed, and even though the manual and other blogs/articles say that you can use objects, I just couldn't get it to work for what I presume is the above reason. Hence why you can't use __destruct()

but session_write_close may negate all that and I may need to read the manual before posting.


EDIT: btw, I stole pretty much my entire class from Chris Shiflett's article on using session_set_save_handler, so credit where it is due. :)

Posted: Mon Aug 21, 2006 3:44 am
by Luke
it seems to be working alright.

Posted: Mon Aug 21, 2006 5:16 am
by Oren
Jenk wrote:EDIT: btw, I stole pretty much my entire class from Chris Shiflett's article on using session_set_save_handler, so credit where it is due. :)
I believe we all did that :P

Posted: Tue Aug 22, 2006 5:28 am
by Adesso
Just a note:

Code: Select all

if(!is_a($db, 'ADOConnection')){
This operater of your extended class is depricated, and should be replaced with instanceof if you want this to be true PHP5. I alos find that the Exception handling can be improved. I am currently working at writing a similar class...

Posted: Tue Aug 22, 2006 5:36 am
by Jenk
FYI: you can scratch your @todo for function clean() because $max is the time in miliseconds that a session expires, the PHP engine provides this time.

this is set in the php.ini session.max_lifetime directive :)

Also, for the above post you can now type hint objects (and arrays, but not primitives as yet :():

So this:

Code: Select all

public function __construct($namespace='Session', $regenerate=null, $db){ 
                if(!is_a($db, 'ADOConnection')){ 
                        // I still need to learn how to handle exceptions properly, so forgive me if this is not right 
                        throw new Exception('Third parameter must be of type: "ADOConnection"'); 
                }
can become simply this:

Code: Select all

public function __construct($namespace='Session', $regenerate=null, ADOConnection $db){
:)

And furthermore.. when specifying optional arguments like you have done, you should always declare mandatory arguments first:

Code: Select all

public function __construct(ADOConnection $db, $namespace='Session', $regenerate=null){
otherwise your optional args are no longer optional :)

Posted: Tue Aug 22, 2006 5:56 am
by Adesso
I like the suggestion of using hints, but am not sure how exceptions can be handled in such cases...

Posted: Tue Aug 22, 2006 6:08 am
by Jenk
They don't, you receive a fatal error if you pass an argument for $db that isn't of type ADOConnection in that case.

thrown exceptions go with try {} catch () {} blocks, or set_exception_handler()

try/catch blocks being my preference - and it really is down to preference whether you chose to try/catch every exception, or set a handler.

It would also be better to create sub classed exceptions instead of using Exception class everytime so you can indentify what type of exception is thrown.

PHP5 now has a number of predefined exception types available for use, see this page for more info (php manual on exceptions) and execute:

Code: Select all

<?php

$classes = get_declared_classes();
$exceptions = "<b>PHP predefined exceptions list:</b><br />\n";

foreach ($classes as $class) {
    if (substr($class, -9) == 'Exception') {
        $exceptions .= $class . "<br />\n";
    }
}

echo $exceptions;
?>
to see a list of predefined exceptions.

Posted: Tue Aug 22, 2006 7:11 am
by Adesso
Point made.. I am with you on the try catch, as it just makes more sense fro me..

I must say that the more I dismantel this class, the more I dislike it.. There is some nice ideas, but some really bad mistakes too. I think that maybe in future a copy and paste and claiming it as your own is not a good way to start a class...

I think I am starting again from scratch.. :roll:

Posted: Tue Aug 22, 2006 7:31 am
by Jenk
Just to nitpick, Ninja did give credit that he took ideas from elsewhere.. and this is the coding critique forum - the place where you post code with the intent of receiving feedback both good and bad :)

Posted: Tue Aug 22, 2006 10:12 am
by Luke
Adesso wrote:copy and paste and claiming it as your own
Are you saying that's whay I did? If so, I think you are confused. The only copy and pasted part of this is the session handler, and there really isn't a more 'inventive' way to do it. I suppose I could completely remove $_SESSION from the class and handle the entire session inside my class... but I have found that PHP handles sessions pretty well, so why reinvent the wheel.

Posted: Tue Aug 22, 2006 11:38 am
by julian_lp
Just to know where to put attention...

Are you updating the original class (first comment in this thread) or should we look for new posts below?

Posted: Tue Aug 22, 2006 11:46 am
by Luke
julian_lp wrote:Just to know where to put attention...

Are you updating the original class (first comment in this thread) or should we look for new posts below?
Good question... I think I will update the original post so as not to clutter the thread with a hundred updates. Thanks for asking!

Posted: Tue Aug 22, 2006 11:51 am
by julian_lp
The Ninja Space Goat wrote:
julian_lp wrote:Just to know where to put attention...

Are you updating the original class (first comment in this thread) or should we look for new posts below?
Good question... I think I will update the original post so as not to clutter the thread with a hundred updates. Thanks for asking!
Great. You could write a new comment just saying "hey, I've edited the class" so us all will be mailed ;)

BTW: Does ADOdb handle functions like mysql_real_escape internally?

Posted: Tue Aug 22, 2006 11:54 am
by Luke
julian_lp wrote:
The Ninja Space Goat wrote:
julian_lp wrote:Just to know where to put attention...

Are you updating the original class (first comment in this thread) or should we look for new posts below?
Good question... I think I will update the original post so as not to clutter the thread with a hundred updates. Thanks for asking!
Great. You could write a new comment just saying "hey, I've edited the class" so us all will be mailed ;)

BTW: Does ADOdb handle functions like mysql_real_escape internally?
No, I don't think so. I think you have to use ADOConnection::qstr() I can't quite recall. For now I am going to remove the adodb dependancy and just rely on mysql since it's using REPLACE anyway which is mysql-only. I will bring ADOdb back into the class once I find the time. I will be updating it probably late tonight to my new version.